The art of folding boats

The Farrier folding system is a popular design for folding trimarans. The system is elegant but tweaking the precise geometry of the linkages when adopting the system for your own design can be tricky. This is because the folded geometry is sensitive to the locations of the hinge points at the same time it is difficult to understand exactly how without some kinematic analysis.

from IPython.display import display, Image
display(Image("http://www.f-boat.com/media/F-27/F-27foldingw.jpg"))

The design parameters are the coordinates of the points shown in the following diagram

display(Image('folding_variables.png', width=600))

It is convenient to work with complex numbers. The kinematic constrains of the system are:

\[ | z_{1} - z_{3} | = | z_{10} - z_{30} | \]

\[ | z_{4} - z_{2} | = | z_{40} - z_{20} | \]

\[ z_{0} + (z_{30} - z_{00}) e^{i \theta} = z_{3} \]

\[ z_{0} + (z_{40} - z_{00}) e^{i \theta} = z_{4} \]

Solving this system for \(z_0\) for a given \(\theta\) will allow us to perform the transformation \(z \to z_0 + z e^{i\theta}\) where \(z\) is a coordinate expressed in the frame coordinates (centered at \(z_0\)) and get the coordinates expressed in the global coordinates.

In order to solve the kinematic equations I use the scipy.optimize.root function

%matplotlib inline
from matplotlib.pyplot import *
from numpy import exp, pi
from scipy.optimize import root

def transform(theta, z00, z10, z20, z30, z40, z, z0=None):
    """ Express points z, expressed in the beam-fixed reference frame, in
    in the boat-fixed reference frame. Optionally the user can supply
    a z0 in order to save time. In this case the user is responsible to 
    make sure the transformation is correct."""
    
    def equations(p):
        z0, z3, z4r, z4i = p
        z4 = z4r+1j*z4i
        eqs = (abs(z10-z3)-abs(z30-z10), 
               abs(z4-z20)-abs(z40-z20),
               z0+(z30-z00)*exp(1j*theta)-z3,
               z0+(z40-z00)*exp(1j*theta)-z4)
        return eqs
    
    def find_z0():
        sol = root(equations, (z00, z30, z40.real, z40.imag), method='krylov')
        if (sol.x[0]).imag <= z30.imag:
            sol = root(equations, (z00+300j, z30, z40.real, z40.imag), method='krylov')
        return sol.x[0]

    if z0 is None:
        z0 = find_z0()
    return z0 + z*exp(1j*theta) 

def plot_frame_raw(z0,z1,z2,z3,z4,z5):
    plot([z0.real,z5.real], [z0.imag,z5.imag], 'k')
    plot([z3.real,z4.real], [z3.imag,z4.imag], 'k')
    plot([z1.real,z3.real], [z1.imag,z3.imag], 'g')
    plot([z2.real,z4.real], [z2.imag,z4.imag], 'r')
    axis('scaled')
    
def float_raw(z00, z10, z20, z30, z40, z50):
    w = 700
    h = 900
    xn = np.linspace(-1.0,1.0,100)
    yn = xn**2-1.
    xf = 0.5*w*xn + ((z50-z00).real+0.5*w)
    yf = h*yn + (z50-z00).imag
    return xf, yf

def plot_frame(theta, z00, z10, z20, z30, z40, z50, verts=False):
    z0 = transform(theta, z00, z10, z20, z30, z40, 0.)
    z3 = transform(theta, z00, z10, z20, z30, z40, z30-z00, z0)
    z4 = transform(theta, z00, z10, z20, z30, z40, z40-z00, z0)
    z5 = transform(theta, z00, z10, z20, z30, z40, z50-z00, z0)
    plot_frame_raw(z0,z10,z20,z3,z4,z5)
    if verts:
        plot([z20.real], [z20.imag], 'wo')
        plot([z4.real], [z4.imag], 'wo')
        plot([z10.real], [z10.imag], 'wo')
        plot([z3.real], [z3.imag], 'wo')
    
def plot_float(theta, z00, z10, z20, z30, z40, z50):
    xf, yf = float_raw(z00, z10, z20, z30, z40, z50)
    zft = transform(theta, z00, z10, z20, z30, z40, xf + 1j*yf)    
    plot(zft.real, zft.imag, 'k')
    axis('scaled')

As a starting point around which to tweak I have chosen the following dimensions (measured from a common base point)

z00 = -700.0 + 1000.0j
z10 = -927.0 + 407.0j
z20 = -489.0 + 907.0j
z30 = -1796.0 + 850.0j
z40 = -1582.0 + 1066.0j
z50 = -3500.0 + 1000.0j

import pandas as pd
data = []
for v in ['z%d0' % i for i in range(0,6)]:
    x=eval(v).real
    y=eval(v).imag
    data.append([v, x, y])
    
df = pd.DataFrame(data, columns=['point', 'x (mm)', 'y (mm)'])
df = df.set_index('point')
display(df)
x (mm) y (mm)
point
z00 -700 1000
z10 -927 407
z20 -489 907
z30 -1796 850
z40 -1582 1066
z50 -3500 1000

6 rows Ɨ 2 columns

Let’s see how it looks when we fold this configuration. Lets plot this configuration for 50 values of \(\theta \in (0, \pi/2)\). We see that with this framework it is easy to make modern art.

figure(figsize=(10,9))
for theta in np.linspace(0, 1.0*pi/2, 50):
    plot_frame(theta, z00, z10, z20, z30, z40, z50)
    plot_float(theta, z00, z10, z20, z30, z40, z50)
axis('off')
gcf().tight_layout()
None

The notebook that is the basis of this post can be downloaded here.