C1. Materialise Cables

Materialise the cables of the equilibrium geometry into sections and unstressed lengths.

Objectives

You will learn how to materialise the cables of the equilibrium geometry into cross-sections and determine their unstressed lengths. For this, we will use Hooke's law.

Procedure

o. Paths

We will build upon the form found mesh data structure from the advanced form-finding section.

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

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

a1. Material Attributes

First, we must define the mechanical properties of the steel to be used in the cable net. We choose the common steel S235 with a yield strength of 235 MPa and an E-modulus of 210 GPa. We will set it as a default edge attribute.

# ==============================================================================
# Create the form found cablenet in equilibrium
# ==============================================================================

mesh = Mesh.from_json(FILE_I)

# set new default edge attributes for materialization
dea = {
    'yield': 235.0,
    'E': 210.0,
}
mesh.update_default_edge_attributes(dea)

a2. Required Sections

Now, we will compute the required cross-sections of uniquely sized steel ties.

Because the stress σ is defined as follows by the force and radius and the stress must be less or equal to the yield strength S

σ=FA=Fπr2Sσ = \frac{F} {A} = \frac{F}{π * r^2} ≤ S

the required radius can be calculated as follows

r=FπSr = \sqrt \frac{F}{π * S}

First convert all into the same units, e.g. N and m.

The procedure is thus as follows:

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


def required_sections(mesh, safety_forces=1.0, safety_material=1.0):
    # input: mesh
    # input: safety_forces, safety_material

    # for each edge in mesh where edge is True
    for (u, v), attr in mesh.edges(True):

        # retrieve edge attributes: force, yield_strength and convert units
        f = attr['f'] * 1e3  # in kN to N
        yieldstress = attr['yield'] * 1e6  # in MPa to N/m^2

        # compute force_factored in N: force * safety_forces
        f_factored_N = f * safety_forces

        # compute strength_factored in N/m^2: yield_strength * safety_material
        S_factored_N_m2 = yieldstress * safety_material

        # compute required radius: sqrt(force_factored / (PI *  strength_factored)) in m
        # because stresses = forces/area = forces / (PI * r2) < strength
        radius_m = sqrt(f_factored_N / (pi * S_factored_N_m2))

        # update edge attribute of radius in m
        attr['r'] = radius_m

        print(radius_m)
# ==============================================================================
# Visualize
# ==============================================================================

baselayer = "CSD2::C1::MaterializeCables"

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()

# visualize the sections of the cable edges
scale = 3
radii_max = max(mesh.edges_attribute('r')) * scale
print(radii_max)
sections = []
for (u, v), attr in mesh.edges(True):

    sp, ep = mesh.edge_coordinates(u, v)
    # scale up for visualisation
    radius = attr['r'] * scale
    color = i_to_rgb(radius/radii_max)

    sections.append({
        'start': sp,
        'end': ep,
        'radius': radius,
        'color': color,
        'name': "{}.section.{}.{}-{}".format(mesh.name, radius, u, v)
    })

compas_rhino.draw_cylinders(sections, layer=baselayer+"::Sections", clear=True)

b1. Unify Sizing

It is practically not possible to realise the cable net is the exact minimum required sections with various sizes. Thus we want to choose from a list of available sections to unify the sizing. To do so, we update the edge attribute of the actual radius by rounding up the radius to the next radius from the list of available sections.

def unify_sizing(mesh, radii_available):
    """Unify the sizing of the individual cable segments to practical available sections.
    """
    # sort radius_available ascendent
    radii_available.sort()

    # for each edge in mesh where edge is True
    for (u, v), attr in mesh.edges(True):

        # retrieve radius attribute in m
        radius = attr['r']

        # for each radius_available in radius_available
        for radius_available in radii_available:
            # check if radius is smaller or equal to radius_available
            if radius <= radius_available:
                # if True update radius to radius_possible and break
                radius = radius_available
                break
        # if no break, because never true, raise Exception that section is not available
        else:
            Exception('Section not available')

        # update edge attribute of radius
        attr['r'] = radius

        print(radius)
        
...

# ==============================================================================
# Parameters
# ==============================================================================

SECTIONS = [0.002, 0.003, 0.004, 0.005, 0.06]

...

The visualisation remains the same.

b2. Unify Sizing of Continuous Cables

If the cable net is not assembled from separate segments but continuous cables, the section in a cable must be constant. We must thus unify the sizing in a cable to the largest required/unified radius.

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

def cables_continuous(mesh, direction='short'):
    """All continuous cables from one direction, either short or long.
    """
    # 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):
        if direction == 'short':
            start = edgeA
        elif direction == 'long':
            start = edgeB
    else:
        if direction == 'short':
            start = edgeB
        elif direction == 'long':
            start = edgeA

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

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

    return cables
# ==============================================================================
# Materialize
# ==============================================================================

required_sections(mesh)

unify_sizing(mesh, radii_available=SECTIONS)

# for all continuous cables find the largest cross section and unify them all
cables_long = cables_continuous(mesh, direction='long')
cables_short = cables_continuous(mesh, direction='short')
cables = cables_long + cables_short

for cable in cables:
    r_max = []
    for edge in cable:
        r = mesh.edge_attribute(edge, 'r')
        r_max.append(r)
    r_max = max(r_max)
    for edge in cable:
        r = mesh.edge_attribute(edge, 'r', r_max)

print(set(mesh.edges_attribute('r')))

The visualisation remains the same.

c. Unstressed Lengths

When the cable net is in its loaded state (tensioned into its shape and loaded by the concrete), the forces in the cables cause a slight elongation of the cables. What we see now in the model is the stressed length. But for the fabrication, we must know the unstressed lengths from which the cable net is assembled.

The unstressed lengths can be calculated with Hooke's law:

σ=Eεσ = E * ε

and with

σ=FAσ = \frac{F} {A}

it follows that the strain is:

ε=FEAε = \frac{F}{E * A}

From the stressed length l in relation to the unstressed length l0 and strain

l=l0+εl0l = l_0 + ε * l_0

it can be followed that the initial length is:

l0=l1+εl_0 = \frac{l}{1+ε}

This can be implemented with the following procedure:

First, to store the strain for visualisation later, add the strain as a default edge attribute.

# set new default edge attributes for materialization
dea = {
    'yield': 235.0,
    'E': 210.0,
    'strain': 0
}
mesh.update_default_edge_attributes(dea)

Don't forget the unit conversion at the beginning.

def unstressed_lengths(mesh):
    """Compute the unstressed lengths of the cable segments.
    """
    # for each edge in mesh where edge is True
    for (u, v), attr in mesh.edges(True):

        # retrieve edge attributes: force, yield_strength, E-modulus, radius, length plus unit conversion
        f = attr['f'] * 1e3  # in kN to N
        E = attr['E'] * 1e9  # in GPa to N/m^2
        r = attr['r']  # in m
        l = attr['l']  # NOQA E741  # in m

        # compute sectional area: PI * radius2
        A = pi * r ** 2

        # compute strain: forces / (E_modulus * area)
        x = f / (E * A)
        attr['strain'] = x

        # compute unstressed edge_length l0: edge_length / (1 + strain)
        # because l = l0 + dl = l0 + strain * l0
        l0 = l / (1 + x)

        # update edge attribute of unstressed_length
        attr['l0'] = l0

        print(l0)

Visualise the unstressed vs. stressed lengths and strain of the cable edges as edge labels.

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

baselayer = "CSD2::C1::MaterializeCables"

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

# visualize the unstressed vs. stressed lengths and strain of the cable edges as labels
strain_max = max(mesh.edges_attribute('strain'))
edgecolor = {(u, v): i_to_rgb(attr['strain']/strain_max) for (u, v), attr in mesh.edges(True)}
edgetext = {(u, v): str(round(attr['l0'], 3)) for (u, v), attr in mesh.edges(True)}

artist.draw_faces()
artist.draw_edgelabels(text=edgetext, color=edgecolor)

# visualize the sections of the cable edges
scale = 3
radii_max = max(mesh.edges_attribute('r')) * scale
print(radii_max)
sections = []
for (u, v), attr in mesh.edges(True):

    sp, ep = mesh.edge_coordinates(u, v)
    # scale up for visualisation
    radius = attr['r'] * scale
    color = i_to_rgb(radius/radii_max)

    sections.append({
        'start': sp,
        'end': ep,
        'radius': radius,
        'color': color,
        'name': "{}.section.{}.{}-{}".format(mesh.name, radius, u, v)
    })
compas_rhino.draw_cylinders(sections, layer=baselayer+"::Sections", clear=True)

Export the data structure.

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

mesh.to_json(FILE_O)

d1. Cables as Fabrication Polylines

Convert the information about unstressed lengths into polylines for the fabrication preparation. These can be laid out on the ground and then assembled.

# ==============================================================================
# Materialize
# ==============================================================================
...
# continous cables in both directions
cables_long = cables_continuous(mesh, direction='long')
cables_short = cables_continuous(mesh, direction='short')
cables = cables_long + cables_short
# ==============================================================================
# Visualize Rhino
# ==============================================================================

baselayer = "CSD2::C1::MaterializeCables"

# visualise in 2D for fabrication
for i, cable in enumerate(cables):
    start = (0, i/10, 0)
    polypoints = [start]

    for (u, v) in cable:
        l0 = mesh.edge_attribute((u, v), 'l0')
        start = add_vectors(start, (l0, 0, 0))
        polypoints.append(start)

    polyline = Polyline(polypoints)
    print(polyline.length)

    artist = PolylineArtist(polyline, color=None, layer=baselayer+"::Cables")
    artist.draw()

d2. with Cable Names at Intersections

In order to be able to associate at which node which cable must be intersected with which other cable, intersection tags are to be visualised. The naming convention is letters for long cables and number for short cables. So for example, the intersection of the long cable b and the short cable 3 is called b3. Visualise the short cables in blue and the long cables in green, respectively.

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

baselayer = "CSD2::C1::MaterializeCables"

# visualise in 3D for reference
edgecolor = {}
# red for long
for edge in flatten(cables_long):
    if edge not in mesh.edges():
        edge = (edge[1], edge[0])
    edgecolor[edge] = (0, 255, 0)
# blue for short
for edge in flatten(cables_short):
    if edge not in mesh.edges():
        edge = (edge[1], edge[0])
    edgecolor[edge] = (0, 0, 255)

# label the first edge of each continuous cable with a letter in the long direction and a number in the short
edgelabels = {}
for i, cable in enumerate(cables_short):
    edgelabels[cable[0]] = i

for j, cable in enumerate(cables_long):
    edgelabels[cable[0]] = string.ascii_lowercase[j]

artist = MeshArtist(mesh, layer=baselayer+"::MeshDirections")
artist.clear_layer()
artist.draw_edges(color=edgecolor)
artist.draw_edgelabels(text=edgelabels, color=None)
artist.draw()


# visualise in 2D for fabrication
for j, cable in enumerate(cables_long):
    cable_name = string.ascii_lowercase[j]
    start = (0, j/10-1, 0)
    polypoints = [start]

    # label all the intersections
    draw_labels([{'pos': start, 'text': cable_name+str(0)}])

    for i, (u, v) in enumerate(cable):
        l0 = mesh.edge_attribute((u, v), 'l0')
        start = add_vectors(start, (l0, 0, 0))
        polypoints.append(start)

        # label all the intersections
        draw_labels([{'pos': start, 'text': cable_name+str(i+1)}])

    polyline = Polyline(polypoints)
    print(polyline.length)

    artist = PolylineArtist(polyline, color=(0, 255, 0), layer=baselayer+"::Cables")
    artist.draw()

position = j/10 - 1 + 0.1
for i, cable in enumerate(cables_short):
    cable_name = str(i)
    start = (0, position+i/10, 0)
    polypoints = [start]

    # label all the intersections
    draw_labels([{'pos': start, 'text': cable_name+string.ascii_lowercase[0]}])

    for j, (u, v) in enumerate(cable):
        l0 = mesh.edge_attribute((u, v), 'l0')
        start = add_vectors(start, (l0, 0, 0))
        polypoints.append(start)

        # label all the intersections
        draw_labels([{'pos': start, 'text': cable_name+string.ascii_lowercase[j+1]}])

    polyline = Polyline(polypoints)
    print(polyline.length)

    artist = PolylineArtist(polyline, color=(0, 0, 255), layer=baselayer+"::Cables")
    artist.draw()

Congratulations, we now have all the data to assemble our cable net!

The materialization of the edges as individual cable segments is already available within the compas_fofin package with the mesh_materialize_cables function. Unlike the three steps a, b and c we that have taken here, this function does everything at once. (You can find a simple example of its use here.)

Last updated