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.
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 materializationdea ={'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
σ=AF=π∗r2F≤S
the required radius can be calculated as follows
r=π∗SF
First convert all into the same units, e.g. N and m.
The procedure is thus as follows:
# ==============================================================================# Helpers# ==============================================================================defrequired_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 Truefor (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_mprint(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 edgesscale =3radii_max =max(mesh.edges_attribute('r'))* scaleprint(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.
defunify_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 Truefor (u, v), attr in mesh.edges(True):# retrieve radius attribute in m radius = attr['r']# for each radius_available in radius_availablefor radius_available in radii_available:# check if radius is smaller or equal to radius_availableif radius <= radius_available:# if True update radius to radius_possible and break radius = radius_availablebreak# if no break, because never true, raise Exception that section is not availableelse:Exception('Section not available')# update edge attribute of radius attr['r']= radiusprint(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# ==============================================================================defcables_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)iflen(loopA)<=len(loopB):if direction =='short': start = edgeAelif direction =='long': start = edgeBelse:if direction =='short': start = edgeBelif 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 allcables_long =cables_continuous(mesh, direction='long')cables_short =cables_continuous(mesh, direction='short')cables = cables_long + cables_shortfor 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∗ε
and with
σ=AF
it follows that the strain is:
ε=E∗AF
From the stressed length l in relation to the unstressed length l0 and strain
l=l0+ε∗l0
it can be followed that the initial length is:
l0=1+εl
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 materializationdea ={'yield':235.0,'E':210.0,'strain':0}mesh.update_default_edge_attributes(dea)
Don't forget the unit conversion at the beginning.
defunstressed_lengths(mesh):"""Compute the unstressed lengths of the cable segments. """# for each edge in mesh where edge is Truefor (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']= l0print(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 labelsstrain_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 edgesscale =3radii_max =max(mesh.edges_attribute('r'))* scaleprint(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)
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 directionscables_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 fabricationfor i, cable inenumerate(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 referenceedgecolor ={}# red for longfor edge inflatten(cables_long):if edge notin mesh.edges(): edge = (edge[1], edge[0]) edgecolor[edge]= (0,255,0)# blue for shortfor edge inflatten(cables_short):if edge notin 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 shortedgelabels ={}for i, cable inenumerate(cables_short): edgelabels[cable[0]]= ifor j, cable inenumerate(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 fabricationfor j, cable inenumerate(cables_long): cable_name = string.ascii_lowercase[j] start = (0, j/10-1,0) polypoints = [start]# label all the intersectionsdraw_labels([{'pos': start, 'text': cable_name+str(0)}])for i, (u, v) inenumerate(cable): l0 = mesh.edge_attribute((u, v), 'l0') start =add_vectors(start, (l0, 0, 0)) polypoints.append(start)# label all the intersectionsdraw_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.1for i, cable inenumerate(cables_short): cable_name =str(i) start = (0, position+i/10,0) polypoints = [start]# label all the intersectionsdraw_labels([{'pos': start, 'text': cable_name+string.ascii_lowercase[0]}])for j, (u, v) inenumerate(cable): l0 = mesh.edge_attribute((u, v), 'l0') start =add_vectors(start, (l0, 0, 0)) polypoints.append(start)# label all the intersectionsdraw_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.)