B2. Variable Force Densities

Objectives

The self-weight caused the cable-net to sag downwards. However, we want to reach a target shape that is higher up and has a slight crease in the middle (see figure). Thus, you will learn how to form find the cable net towards a target geometry by controlling variable force densities.

Procedure

o. Paths

We will build upon the form found mesh data structure from the self-weight form-finding section. We will output the resulting mesh in a new JSON file.

# ==============================================================================
# Paths
# ==============================================================================

HERE = os.path.dirname(__file__)
DATA = os.path.abspath(os.path.join(HERE, '..', 'data'))
FILE_I = os.path.join(DATA, 'cablenet_fofin_sw.json')
FILE_O = os.path.join(DATA, 'cablenet_fofin_sw_q.json')

a. Clean up

For cleaning, put all the form finding with self-weight from B1. Selfweight into a function.

Since the initial self-weight corresponds to the already form-found shape (not considering the changed force densities yet) the iteration would stop immediately. But make sure the form-finding iteration runs at least once, so add the and not k == 0

# ==============================================================================
# Helpers
# ==============================================================================

...

def fofin_selfweight(mesh):
    # ==============================================================================
    # Fofin for  selfweight
    # ==============================================================================

    # define maximum iterations and tolerance for residuals
    tol = 0.001
    kmax = 10

    # store previous selfweight loads (zero)
    loads_previous = mesh.vertices_attributes(('px', 'py', 'pz'))
    # !!! here already not zero anymore but must at least run one iteration

    # compute selfweight for the current geometry
    selfweight(mesh)

    # for all k smaller than kmax
    for k in range(kmax):

        # recompute the residuals with difference of selfweight from updated to previous geometry
        residuals = update_residual(mesh, loads_previous)

        # stopping criteria if updated residual is smaller than tolerance or at least once
        print('k', k, 'residuals', residuals)
        if residuals < tol and not k == 0:
            print('Convergence!')
            break

        # form finding with selfweight loads and geomtry update
        fofin(mesh)

        # store previous selfweight loads (computed before the updated geometry)
        loads_previous = mesh.vertices_attributes(('px', 'py', 'pz'))

        # recompute selfweight for the updated geometry
        selfweight(mesh)
# ==============================================================================
# Fofin for selfweight
# ==============================================================================

fofin_selfweight(mesh)

# ==============================================================================
# Visualize
# ==============================================================================

baselayer = "CSD2::B2::FormFindingForceDensities"

artist = MeshArtist(mesh, layer=baselayer+"::Mesh")
artist.clear_layer()

artist.draw_vertices(color={vertex: (255, 0, 0) for vertex in mesh.vertices_where({'is_anchor': True})})
artist.draw_edges()
artist.draw_faces()

draw_reactions(mesh, baselayer=baselayer)
draw_residuals(mesh, baselayer=baselayer, scale=10)
draw_forces(mesh, baselayer=baselayer)
draw_loads(mesh, baselayer=baselayer, scale=10)

The geometry is identical to the output of B1. Selfweight. But we want to get closer to our target geometry!

b. Explore Higher Force Densities of All Edges

So we can try what happens if we increase the force densities of all edges. Play around with it!

With doubled force densities in all the edges, we get a geometry that lies in between the initial and target shape (and without a crease yet).

...

# ==============================================================================
# Variable Force Denisties
# ==============================================================================

# explore increased force denisties for all edges
mesh.edges_attribute('q', 2)

# ==============================================================================
# Fofin for selfweight
# ==============================================================================

fofin_selfweight(mesh)

Visualise the new geometry in a new layer, so that you can compare the difference.

# ==============================================================================
# Visualize
# ==============================================================================

baselayer = "CSD2::B2::FormFindingForceDensities_qall_2"

...

Remind yourself, that every time the entire iterative form-finding procedure under self-weight is computed.

c. Find a Continuous Cable

Find a single continuous cable starting from a starting edge. Select the middle edge at the support kink.

To find out the edge key, check its name in Rhino in the Properties: Objects window. (Alternatively, you could also select the edge in Rhino with a different code snippet).

# ==============================================================================
# Single Cable
# ==============================================================================

# find center cable to create crease
center_start = (6, 120)
center_cable = mesh.edge_loop(center_start)
# ==============================================================================
# Visualize Rhino
# ==============================================================================

edgecolor = {}
for edge in center_cable:
    if edge not in mesh.edges():
        edge = (edge[1], edge[0])
    edgecolor[edge] = (0, 255, 0)
edgecolor[center_start] = (255, 0, 0)

baselayer = "CSD2::B2::FormFindingForceDensities_singlecable"
artist = MeshArtist(mesh, layer=baselayer+"::Mesh")
artist.clear_layer()
artist.draw_vertices()
artist.draw_edges(color=edgecolor)
artist.draw()

d. Introduce the Crease

Increase the force densities in the middle cable to introduce the crease. Play with different magnitudes.

# ==============================================================================
# Variable Force Denisties
# ==============================================================================

...

# increase force densities of center cable to create crease
mesh.edges_attribute('q', 6, keys=center_cable)

Same here, visualise the new geometry in a new layer, so that you can compare the difference.

# ==============================================================================
# Visualize
# ==============================================================================

baselayer = "CSD2::B2::FormFindingForceDensities_qsingle_6"

...

e. Find all Cables in the Short Direction

We have now introduced the crease, however, the cable net is still below the target geometry. We could increase the force density of all edges even more. But this would lead to unnecessary high forces and reaction forces all over. Thus, we want to only increase the force densities in the cable in the short direction. Let's create a list of all edge in the short direction:

e1. Shorter Boundary

First, we have to understand which one is in the shorter direction. We check it on two boundaries starting from a corner vertex:


# ==============================================================================
# All Cables in the Short Direction
# ==============================================================================

# select starting corner
corners = list(mesh.vertices_where({'vertex_degree': 2}))
corner = corners[0]

# check in which direction the edge is shorter
corner_edges = mesh.vertex_neighbors(corner)

edgeA = (corner, corner_edges[0])
edgeB = (corner, corner_edges[1])

loopA = mesh.edge_loop(edgeA)
loopB = mesh.edge_loop(edgeB)

if len(loopA) <= len(loopB):
    loop = loopA
    start = edgeA
else:
    loop = loopB
    start = edgeB
# ==============================================================================
# Visualize Rhino
# ==============================================================================

edgecolor = {}
for edge in loop:
    if edge not in mesh.edges():
        edge = (edge[1], edge[0])
    edgecolor[edge] = (0, 255, 0)

if start not in mesh.edges():
    start = (start[1], start[0])
edgecolor[start] = (255, 0, 0)

vertexcolor = {}
vertexcolor[corner] = (255, 0, 0)

baselayer = "CSD2::B2::FormFindingForceDensities_short direction"
artist = MeshArtist(mesh, layer=baselayer+"::Mesh")
artist.clear_layer()
artist.draw_vertices(color=vertexcolor)
artist.draw_edges(color=edgecolor)
artist.draw()

e2. Parallel Starting Edges

Now we want all edges parallel to the starting edge for the new starting edges:

# get all edges parallel to the start edge
starts = mesh.edge_strip(start)
# ==============================================================================
# Visualize Rhino
# ==============================================================================

edgecolor = {}
for edge in starts:
    if edge not in mesh.edges():
        edge = (edge[1], edge[0])
    edgecolor[edge] = (0, 255, 0)

if start not in mesh.edges():
    start = (start[1], start[0])
edgecolor[start] = (255, 0, 0)

vertexcolor = {}
vertexcolor[corner] = (255, 0, 0)

baselayer = "CSD2::B2::FormFindingForceDensities_parallel edges"
artist = MeshArtist(mesh, layer=baselayer+"::Mesh")
artist.clear_layer()
artist.draw_vertices(color=vertexcolor)
artist.draw_edges(color=edgecolor)
artist.draw()

e3. Parallel Continuous Cables

Now from each of the parallel starts, we want all continuous cables:

# get all cables for all parallel starts
cables = []
for start in starts:
    cable = mesh.edge_loop(start)
    cables.append(cable)

These cables are edges in a list of lists, so if you want to iterate over them, you must flatten the list down.

from compas.utilities import flatten

...


# ==============================================================================
# Visualize Rhino
# ==============================================================================

# color the cables in green
edgecolor = {}
for edge in flatten(cables):
    if edge not in mesh.edges():
        edge = (edge[1], edge[0])
    edgecolor[edge] = (0, 255, 0)

# color the starts red
for edge in starts:
    if edge not in mesh.edges():
        edge = (edge[1], edge[0])
    edgecolor[edge] = (255, 0, 0)

baselayer = "CSD2::B2::FormFindingForceDensities_short cables"
artist = MeshArtist(mesh, layer=baselayer+"::Mesh")
artist.clear_layer()
artist.draw_vertices()
artist.draw_edges(color=edgecolor)
artist.draw()

f. Higher Force Densities in Edge in One Direction

So now, let's set the force densities of these short cables to a higher value:

Remember to use the compas flatten function for the nested list.

# ==============================================================================
# Variable Force Denisties
# ==============================================================================

...

# increase force densities more in short direction only
mesh.edges_attribute('q', 3, keys=flatten(cables))

Visualise the results including the forces and export the JSON file.

# ==============================================================================
# Fofin for selfweight
# ==============================================================================

fofin_selfweight(mesh)

# ==============================================================================
# Visualize
# ==============================================================================

baselayer = "CSD2::B2::FormFindingForceDensities_qshort_3"

artist = MeshArtist(mesh, layer=baselayer+"::Mesh")
artist.clear_layer()

artist.draw_vertices(color={vertex: (255, 0, 0) for vertex in mesh.vertices_where({'is_anchor': True})})
artist.draw_edges()
artist.draw_faces()

draw_reactions(mesh, baselayer=baselayer)
draw_residuals(mesh, baselayer=baselayer, scale=10)
draw_forces(mesh, baselayer=baselayer)
draw_loads(mesh, baselayer=baselayer, scale=10)

# ==============================================================================
# Export
# ==============================================================================

mesh.to_json(FILE_O)

With this, we receive our target geometry:

Yey!

Last updated