2. Define Boundary Conditions

Several additional layers of information regarding the boundary conditions need to be added to give the Pattern a structural meaning:

  • identification of the supports

  • treatment of the openings and open edges

2.a: Supports

Support is defined as a vertex of the structure that is fixed and can have external horizontal reactions. By definition, only the vertices on the boundary of a Pattern can be defined as supports.

A barrel shell is simply part of a cylindrical surface. Two transverse boundary edges support the shell. Here vertices along these two edges are anchored.

# ==============================================================================
# Anchors
# ==============================================================================
# anchor the vertices on the left and right boundary
x_min = min(set(pattern.vertices_attribute('x')))
x_max = max(set(pattern.vertices_attribute('x')))

anchors = []
anchors.extend(list(pattern.vertices_where({'x': x_min})))
anchors.extend(list(pattern.vertices_where({'x': x_max})))
pattern.vertices_attribute('is_anchor', True, keys=anchors)

These anchor points can be visualized in red color.

artist.draw_vertices(vertices=anchors, color=(255, 0, 0))

2.b: Relax Boundary Openings

An opening is a chain of edges at the boundary of a Pattern in-between two support vertices. In general, openings cannot be straight unless there are no internal forces in the non-boundary edges at the openings (i.e., a single-curved barrel vault).

Here a curvature along the openings will be introduced to control the shape of the geometry. The relaxation of the boundary and computing the right curvature uses the force density method. To accelerate the calculation speed, this numerical method is called through the proxy. By default, the force density q in all edges are 1.0. This is what the pattern looks like when q = 1.0 in all edges:

from compas.rpc import Proxy

# ==============================================================================
# Relax Boundaries
# ==============================================================================
proxy = Proxy()
proxy.package = 'compas_tna.utilities'
patterndata = proxy.relax_boundary_openings_proxy(pattern.to_data(), anchors)
pattern = Pattern.from_data(patterndata)

If all force densities are the same (by default 1), the openings curve inwards quite extreme. However, this is the footprint of the shell, so in order to pull them more outwards, increase the force densities on the boundaries.

# to pull out the openings more, increase the force density in the boundary edges before the relaxation
boundary = pattern.edges_on_boundary()
for u, v in boundary:
    pattern.edge_attribute((u, v), 'q', 4)

2.c Pattern Serialization

Pattern can also be serialized in the same way as Mesh. After you have modified your pattern, you can save it to .json and load it in the latter part of the tutorial to keep your code tidy.

# ==============================================================================
#  Serialization
# ==============================================================================
FILE_O = os.path.join(HERE, 'data', 'barrel_vault_pattern.json')
pattern.to_json(FILE_O)

Bonus

The sag function gives you more control to the curved openings. It calculates the amount of curvature based on the percentage of the length of the opening. The qqs for the boundary edges are automatically updated based on the target sag values, which are then used for the force density method.

Here is the relaxed pattern when the target sag is 0.15.

You can copy this part of the code and change the target_sag.

# ==============================================================================
# Target sag percentage
# ==============================================================================
target_sag = 0.15

# ==============================================================================
# Sag Helper
# ==============================================================================
def split_boundary(pattern):
    boundaries = pattern.vertices_on_boundaries()
    exterior = boundaries[0]
    opening = []
    openings = [opening]
    for vertex in exterior:
        opening.append(vertex)
        if pattern.vertex_attribute(vertex, 'is_anchor'):
            opening = [vertex]
            openings.append(opening)
    openings[-1] += openings[0]
    del openings[0]
    openings[:] = [opening for opening in openings if len(opening) > 2]
    return openings


def compute_sag(pattern, opening):
    u, v = opening[0]
    if pattern.vertex_attribute(u, 'is_fixed'):
        a = pattern.vertex_attributes(u, 'xyz')
        aa = pattern.vertex_attributes(v, 'xyz')
    else:
        a = pattern.vertex_attributes(v, 'xyz')
        aa = pattern.vertex_attributes(u, 'xyz')
    u, v = opening[-1]
    if pattern.vertex_attribute(u, 'is_fixed'):
        b = pattern.vertex_attributes(u, 'xyz')
        bb = pattern.vertex_attributes(v, 'xyz')
    else:
        b = pattern.vertex_attributes(v, 'xyz')
        bb = pattern.vertex_attributes(u, 'xyz')
    span = distance_point_point_xy(a, b)
    apex = intersection_line_line_xy((a, aa), (b, bb))
    if apex is None:
        rise = 0.0
    else:
        midspan = midpoint_point_point_xy(a, b)
        rise = 0.5 * distance_point_point_xy(midspan, apex)
    sag = rise / span
    return sag


openings = split_boundary(pattern)
# convert the list of vertices to a list of segments
openings = [list(pairwise(opening)) for opening in openings]

targets = []
for opening in openings:
    targets.append(target_sag)

# compute current opening Qs
Q = []
for opening in openings:
    q = pattern.edges_attribute('q', keys=opening)
    q = sum(q) / len(q)
    Q.append(q)
    pattern.edges_attribute('q', q, keys=opening)


# ==============================================================================
# Relax Boundaries
# ==============================================================================
patterndata = proxy.relax_boundary_openings_proxy(pattern.to_data(), anchors)
pattern = Pattern.from_data(patterndata)

TOL2 = 0.001 ** 2

# update Qs to match target sag
count = 0
while True and count < 10:
    count += 1
    sags = [compute_sag(pattern, opening) for opening in openings]
    if all((sag - target) ** 2 < TOL2 for sag, target in zip(sags, targets)):
        break
    for i in range(len(openings)):
        sag = sags[i]
        target = targets[i]
        q = Q[i]
        q = sag / target * q
        Q[i] = q
        opening = openings[i]
        pattern.edges_attribute('q', Q[i], keys=opening)
    patterndata = proxy.relax_boundary_openings_proxy(pattern.to_data(), anchors)
    pattern = Pattern.from_data(patterndata)

Last updated