I can’t say that I had the honor of making that acronym - that goes to Prof. Slocum - but I did have the honor of actually designing them.
FPGAs are bi-weekly assignments in 2.70 that involve building a contraption of our choice. The only restraint is that we we demonstrate some fundamental principle of engineering - and, of course, that we do the math.
FPGA 1: Finicky Preload with Great Applications
Have you ever counter-rotated two nuts to make a locknut? That question was rhetorical - everyone does that. But is it actually effective?
Our first FPGA was an attempt to quantify the actual strength of these preloaded “locknuts”. We first created an analytical model of two nuts held together.
(click to see code)
%% FPGA 1
%% parameters
E = 200 * 10^9; %steel 200 GPa
l = 1/20 * 0.0254; %lead = 1/20 in
L = 7/32*2 * 0.0254; %length of assemble (m)
u = 0.5; % coefficient of friction
r = 1/8 * 0.0254; %radius of bolt
%% derived parameters
A = (((7/16)/2)^2 *pi - ((1/4)/2)^2 *pi) * 0.0254^2; % m^2 front area of nut
%% Do calculation
f1 = @computeForce;
f2 = @computeTorque;
%% Function
% Put in equations form
function force = computeForce(original, final)
A = (((7/16)/2)^2 *pi - ((1/4)/2)^2 *pi) * 0.0254^2; % m^2 front area of nut
strain = ((final - original)/original)/2;
E = 190 * 10^9;
stress = strain*E;
force = stress * A;
end
function torque = computeTorque(preload)
u = 0.5;
r = 1/8 * 0.0254;
torque = preload*sind(75)*u*r;
end
We then put our tester-gadget into an Instron to see how our predictions lined up with reality. When using spring washers, our predictions were suprisingly accurate (within 22% in the worst trial) - but when directly mashing the nuts together, our model broke down. Our quest to fix this led to us realizing that bolt tensioning is a science within itself!
FPGA 2: Figure Pondering Grading of Assignment
Our second FPGA was an attempt to make a bistable switch - but not just any switch. We wanted it to be the most satisfying switch in existence.
Our bistable switch features a special 2D arrangement of springs that allows it to snap between two positions with hysterises in a predictable fashion. The static behavior of the system was calculated using a potential energy well based solver.
(click to see code)
import numpy as np
from mpl_toolkits.mplot3d import Axes3D
import matplotlib.pyplot as plt
from matplotlib import cm
from matplotlib.ticker import LinearLocator, FormatStrFormatter
# input parameters (all in mm | radians | N/mm)
p2s = 22 # pivot to slider distance
pl = 44 # pivot length
prom = np.pi / 6 # pivot range of motion (total)
lrom = 25 # slider range of motion (total)
ss = 44 # spring seperation
k = 1.3 / 30 # spring constant
srl = 30 # spring resting length
so = 15 # spring pivot seperation
precision = 100 # number of points to evaluate
# equation derived from matlab
def energy(pa, delta):
"""
equation source:
pc = [-pl*np.np.cos(pa), pl*np.np.sin(pa)] % pivot attachment point coords
tpc = [p2s, ss/2 + delta] % top spring attachment coords
bpc = [p2s, -ss/2 + delta] % bottom spring attachment coords
tsl = norm(pc - tpc) % top spring length
bsl = norm(pc - bpc) % bottom spring length
tse = (tsl - srl)*k % top spring energy
bse = (bsl - srl)*k % bottom spring energy
te = tse + bse % total energy
matlabs symbolic stuff is way easier lol
delta -> shift in spring origin
pa -> angle of pivot
"""
return (k*(srl - (abs(delta - ss/2 + so*np.cos(pa) - pl*np.sin(pa))**2 + abs(p2s + pl*np.cos(pa) + so*np.sin(pa))**2)**(1/2))**2)/2 + (k*(srl - (abs(delta + ss/2 - so*np.cos(pa) - pl*np.sin(pa))**2 + abs(p2s + pl*np.cos(pa) - so*np.sin(pa))**2)**(1/2))**2)/2
# create map of energy
energy_map_func = np.vectorize(energy)
deltas = np.linspace(-lrom / 2, lrom / 2, precision)
pas = np.linspace(-prom / 2, prom / 2, precision)
pas_Y, deltas_X = np.meshgrid(pas, deltas)
energies = energy_map_func(pas_Y, deltas_X)
# map forward stroke
# starting energy
def generate_path(flipped):
angles_indexes = [energies[0].argmin()]
if flipped:
angles_indexes = [energies[energies.shape[0] - 1].argmin()]
steps = range(1, energies.shape[0])
if flipped:
steps = reversed(steps)
for i in steps:
c_angle = angles_indexes[-1]
while (c_angle > 0):
if energies[i][c_angle] > energies[i][c_angle - 1]:
c_angle = c_angle - 1
else:
break
while (c_angle < len(energies[i]) - 1):
if energies[i][c_angle] > energies[i][c_angle + 1]:
c_angle = c_angle + 1
else:
break
angles_indexes.append(c_angle)
path_deltas = deltas
if flipped:
path_deltas = [i for i in reversed(deltas)]
path_pas = [pas[i] for i in angles_indexes]
path_energies = [energies[i][j]
for i, j in zip(range(precision), angles_indexes)]
if flipped:
path_energies = [energies[i][j] for i, j in zip(
reversed(range(precision)), angles_indexes)]
return (path_deltas, path_pas, path_energies)
fig = plt.figure()
ax = fig.gca(projection='3d')
# Plot the surface.
surf = ax.plot_surface(deltas_X, pas_Y, energies, alpha=0.5,
cmap=cm.coolwarm, linewidth=0, antialiased=False)
line_forward = ax.plot(*generate_path(False), linewidth=3, label="Forward Crank Path")
line_backward = ax.plot(*generate_path(True), linewidth=3, label="Backwards Crank Path")
# Customize the z axis.
ax.zaxis.set_major_locator(LinearLocator(6))
ax.zaxis.set_major_formatter(FormatStrFormatter('%.01f'))
ax.set_xlabel("Slider Position")
ax.set_ylabel("Crank Angle")
ax.set_zlabel("Elastic Energy Stored")
ax.legend()
# Add a color bar which maps values to colors.
fig.colorbar(surf, shrink=0.5, aspect=5)
plt.show()
This gave us a nice idea of how the switch would move over its range of motion. The final test showed that our model had only 16% error - most of which came from errors in measuring the resting length of our springs.
FPGA 3: Flexures Put in Ghastly Applications
Flexures are wonderful, but they’re not always easy to make. Their very nature means that they tend to move around as you machine them. Most people solve this by simply…not machining them. But what if you did?
For this FPGA, we tried to quantify how bad tolerances would be if you tried to machine a pocket in a folded-stage flexure. A lot of tuning was needed to get the damn thing to cut in the first place. A lot of punishment was taken every time we messed up :c
Our final model predicted our tolerances to be off by 0.08mm - and our final measurements confirmed this to be accurate (although our measurement techniques could surely be improved).
FPGA 4: Freaky Pulleys Going Axially
Overhead cranes typically use large steel I-beams upon which steel rollers glide. Why not make it more like a zipline? Well, that would just result in the rope sagging. Why not use rope sagging to your advantage?
The basic concept was to have two nonlinear winches synchronized together by gears. When one winch releases rope, the other winch takes up rope at the exact rate needed to keep a hanging object moving along a straight line. Tricky, but possible.
A giant pile of python code and math was used to generate a parametric path through space that represented the shape of the winch.
(click to see code)
import numpy as np
import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d import Axes3D
# input parameters
a = 15 # ratio of pulley angle (radians) to x distance traveled (mm)
H = 100 # distance between pulley and object (mm)
W = 300 # distance between pulleys (mm)
x_padding = 50 # padding between x travel and pulley spacing (mm)
pulley_height = 25 # width of pulley (mm)
# resolution
num_points = 75
# derived figures
x_min = x_padding
x_max = W - x_padding
xs = np.linspace(x_min, x_max, num_points)
thetas = xs / a
pitch = pulley_height / (thetas[-1] - thetas[0])
ds = np.sqrt(np.square(2 * xs * a) / (np.square(xs) + np.square(H)) - np.square(pitch))
# exporting to solidworks curve format
p_x_left = np.cos(thetas) * ds
p_x_right = -np.cos(thetas) * ds
p_y_left = np.sin(thetas) * ds
p_y_right = np.sin(thetas) * ds
p_z_left = np.linspace(0, pulley_height, num_points)
p_z_right = np.linspace(pulley_height, 0, num_points)
# previewing helix shape
fig = plt.figure()
ax = fig.gca(projection='3d')
ax.plot(p_x_left, p_y_left, p_z_left, label='left_pulley')
# ax.plot(p_x_right, p_y_right, p_z_right, label='right_pulley')
ax.legend()
plt.show()
# saving path to file
with open('output_left.sldcrv', 'w') as file:
output = '\n'.join(' '.join((str(x), str(y), str(z)))
for x, y, z in np.vstack((p_x_left, p_y_left, p_z_left)).T)
file.write(output)
with open('output_right.sldcrv', 'w') as file:
output = '\n'.join(' '.join((str(x), str(y), str(z)))
for x, y, z in np.vstack((p_x_right, p_y_right, p_z_right)).T)
file.write(output)
The final pulley system - despite all of our expectations - somehow worked as expected! Only a few nonlinearities were observed (mostly due to improper assembly).
FPGA 5: Figure Pulling Great mAss
When hit, a tetherball initially starts wrapping around the poll slowly but speeds up as it winds inwards. This is due to a decreasing moment of inertia and conserved energy. What would a flywheel built to mimick this look like?
A small arm with two counterbalanced weights spins on a pivot. The spinning of the arm causes the two weights to be simultaneously winched together. Predicting how the speed would change as a result was suprisingly difficult and required my partner to write a custom Euler-Lagrange simulator.
(click to see code)
function simulate_leg()
%% Definte fixed paramters (obtained from CAD)
m1 =.02 + .210; m2 =.0225;
m3 = .004; m4 = .017;
I1 = 45.389 * 10^-6; I2 = 22.918 * 10^-6;
I3 = 3.2570 * 10^-6; I4 = 22.176 * 10^-6;
l_OA=.011; l_OB=.042;
l_AC=.096; l_DE=.096;
l_O_m1=0.0364; l_B_m2=0.040;
l_A_m3=1/2 * l_AC; l_C_m4=1/2 * (l_DE+ l_OB-l_OA);
g = 9.81;
%% Parameter vector
p = [m1 m2 m3 m4 I1 I2 I3 I4 l_O_m1 l_B_m2 l_A_m3 l_C_m4 l_OA l_OB l_AC l_DE g]';
%% Perform Dynamic simulation
tspan = [0 2];
z0 = [-pi/4; pi/2; 0; 0];
opts = odeset('AbsTol',1e-8,'RelTol',1e-6);
sol = ode45(@dynamics,tspan,z0,opts,p);
%% Compute Energy
E = energy_leg(sol.y,p);
figure(1); clf
plot(sol.x,E);xlabel('Time (s)'); ylabel('Energy (J)');
%% Compute foot position over time
rE = zeros(2,length(sol.x));
for i = 1:length(sol.x)
rE(:,i) = position_foot(sol.y(:,i),p);
end
w =100;
% Plot desired and actuatl foot trajectories
figure(2); clf;
plot(sol.x,rE(1,:),'r','LineWidth',2)
hold on
plot(sol.x,0.025 * cos(w*sol.x) ,'r--');
plot(sol.x,rE(2,:),'b','LineWidth',2)
plot(sol.x,-.125+0.025*sin(w*sol.x) ,'b--');
xlabel('Time (s)'); ylabel('Position (m)'); legend({'x','x_d','y','y_d'});
%% Animate Solution
figure(3); clf;
hold on
%% Optional, plot foot target information
% Target traj. Q 1.6
plot( .025*cos(0:.01:2*pi), -.125+.025*sin(0:.01:2*pi),'k--');
animateSol(sol,p);
end
function tau = control_law(t,z,p)
% Controller gains, Update as necessary for Problem 1
K_x = 40; % Spring stiffness X
K_y = 40; % Spring stiffness Y
D_x = 4; % Damping X
D_y = 4; % Damping Y
% Desired position of foot is a circle
w = 30;
rEd = [0 -.125 0]' + .025*[cos(w*t) sin(w*t) 0]'; % Desired position of foot
vEd = .025*[-sin(w*t)*w cos(w*t)*w 0]'; % Desired velocity of foot
rE = position_foot(z,p);
vE = velocity_foot(z,p);
J = jacobian_foot(z,p);
% Compute virtual foce
f = [K_x * (rEd(1) - rE(1) ) - D_x * (vE(1) - vEd(1) ) ;
K_y * (rEd(2) - rE(2) ) - D_y * (vE(2) - vEd(2) ) ];
% Map to joint torques
tau = J' * f;
end
function tau = control_law_extended(t,z,p)
% Controller gains, Update as necessary for Problem 1
K_x = 40; % Spring stiffness X
K_y = 40; % Spring stiffness Y
D_x = 4; % Damping X
D_y = 4; % Damping Y
% Desired position of foot is a circle
w = 100;
rEd = [0 -.125 0]' + .025*[cos(w*t) sin(w*t) 0]'; % Desired position of foot
vEd = .025*[-sin(w*t)*w cos(w*t)*w 0]'; % Desired velocity of foot
aEd = [-.025*w^2*cos(t*w) -.025*w^2*sin(w*t)]';
rE = position_foot(z,p);
vE = velocity_foot(z,p);
J = jacobian_foot(z,p);
dJ = jacobian_dot_foot(z,p);
%Compute dynamic coefficients
M = A_leg(z,p);
lambda = inv(J')*M*inv(J);
V = Corr_leg(z,p);
mu = inv(J')*V-lambda*dJ*z(3:4);
G = Grav_leg(z,p);
rho = inv(J')*G;
% Compute virtual foce
f = [K_x * (rEd(1) - rE(1) ) - D_x * (vE(1) - vEd(1) ) ;
K_y * (rEd(2) - rE(2) ) - D_y * (vE(2) - vEd(2) ) ];
f_new = f + lambda*aEd + rho;
% Map to joint torques
tau = J' * f_new;
end
function dz = dynamics(t,z,p)
% Get mass matrix
A = A_leg(z,p);
% Compute Controls
tau = control_law_extended(t,z,p);
% Get b = Q - V(q,qd) - G(q)
b = b_leg(z,tau,p);
% Solve for qdd.
qdd = A\b;
dz = 0*z;
% Form dz
dz(1:2) = z(3:4);
dz(3:4) = qdd;
end
function animateSol(sol,p)
% Prepare plot handles
hold on
h_OB = plot([0],[0],'LineWidth',2);
h_AC = plot([0],[0],'LineWidth',2);
h_BD = plot([0],[0],'LineWidth',2);
h_CE = plot([0],[0],'LineWidth',2);
xlabel('x'); ylabel('y');
h_title = title('t=0.0s');
axis equal
axis([-.2 .2 -.3 .1]);
%Step through and update animation
for t = 0:.01:sol.x(end)
% interpolate to get state at current time.
z = interp1(sol.x',sol.y',t)';
keypoints = keypoints_leg(z,p);
rA = keypoints(:,1); % Vector to base of cart
rB = keypoints(:,2);
rC = keypoints(:,3); % Vector to tip of pendulum
rD = keypoints(:,4);
rE = keypoints(:,5);
set(h_title,'String', sprintf('t=%.2f',t) ); % update title
set(h_OB,'XData',[0 rB(1)]);
set(h_OB,'YData',[0 rB(2)]);
set(h_AC,'XData',[rA(1) rC(1)]);
set(h_AC,'YData',[rA(2) rC(2)]);
set(h_BD,'XData',[rB(1) rD(1)]);
set(h_BD,'YData',[rB(2) rD(2)]);
set(h_CE,'XData',[rC(1) rE(1)]);
set(h_CE,'YData',[rC(2) rE(2)]);
pause(.01)
end
end
Once it was built, we used Tracker to see whether our observations matched up with our predictions. Parasitic frictional losses ended up thwarting our model… but we could still clearly see the device speeding up over time!
FPGA 6: Fixturing for Precise Gadget Alignment
Kinematic couplings are great for creating repeatable fixtures, but things get weird when you try to have multiple orientations. Undercontrained/overcontrained/undesired orientations crop up when you try to place multiple KCs around one axis. How do you get around this?
If the coupler locations are offset properly, you can get a coupler that locks in place at an arbitrary number of locations (with no invalid configurations!)
We predicted that there would be some amount of angular error in our coupler (specifically, a max deviation of 1.61e-7 m). We tried testing this using a laser pointer mounted to the top and aimed down a 127ft hallway…
…and we couldn’t measure anything! We believe that the brinelling of the KC grooves might have made this more repeatable than we originally thought!