Bending active Structure

hello,
I have made physical model of one prototype with 2 canes with concept of bending active system. Now I am trying to convert it into digital medium. I have gone through the videos of related to this. But I am not able to get the desired solution with kangaroo solver.

help me to figure it out.
Thank you for your valuable time and consideration.


I’m not an expert in Kangaroo, far from it, but I see something in your model that I also need help with and think is a problem — the tie points. These are what I would call reciprocal constraints on the bending. That is, the tie points influence how and in what direction bending can occur, but the bending also changes the constraint’s location in absolute space. I think this introduces a degree of recursion that, if it is possible, is very very tricky.

My project, for example, is also based on a physical maquette. It uses multiple creases and cuts in a sheet material. As the creases are amplified, they bend the material and are therefore also moved by the material in absolute space. Also, after the creases are made, you can move them around in a way that is constrained by the material and this has a significant impact on the bending caused. The whole system is an interdependent loop. Even faking some of these constraints, I so far have failed to achieve a complete simulation.

These old project videos might inspire some ideas, or at least demonstrate how to model the input polylines:

I can link to some papers we wrote when I’m on my laptop again. Some fond bendy memories :upside_down_face:

2 Likes

I’ve come along my way but I didn’t figured out yet how the linear constraint stops the rod from converging into a circle in your first little box example. I’ve been using constantTension and Length for that with trying out to use the intersection points as anchors..

I posted some of the source code back here, might help:

Maybe you can add a screenshot? It’s been a loooong time :old_man:

Edit: Here’s a super stripped down version of the Smart Geometry 2016 pipeline (i.e. just the shaping):


201210_FAHS_InteractiveModellingPipeline_Minimal_00.gh (45.7 KB)

The code here was updated in 2020 to run in Rhino 6/7. But haven’t tested with 8. Also it’s ten year old code so there are surely many things I’d do differently today. Notably regarding drawing and dynamic/event-based stuff.

Edit II: Just tried running the definition in Rhino 8 and I’m getting a goal casting error. It does ring a bell, but afraid I can’t recall how to solve it. Thought it might be that I’m referencing the wrong Kangaroo2 assembly, but that doesn’t seem to be it. @DanielPiker might know why it broke?

2 Likes

And here are the primary publications on the projects above. That present quite explicitly how Kangaroo2 and K2 Engineering was implemented:

Deleuran et al_2015_The Tower Modelling , Analysis and Construction of Bending Active Tensile Membrane Hybrid Structures.pdf (2.1 MB)
Deleuran et al_2016_Exploratory Topology Modelling of Form-Active Hybrid Structures.pdf (1.8 MB)
Quinn et al_2016_Calibrated and Interactive Modelling of Form-Active Hybrid Structures.pdf (3.0 MB)
Tamke et al._2017_Lace Wall - Extending design intuition through Machine Learning.pdf (960.2 KB)

2 Likes

Just tried using this non-explicit method to reference the KangarooSolver dll instead. I’m not really which assembly is being loaded now, but it appears to solve the IGoal error:


201210_FAHS_InteractiveModellingPipeline_Minimal_Rhino8_00.gh (47.5 KB)

Also, none of the downstream drawing works in Rhino 8. Likely because the custom display class is deprecated and there’s been a lot of updates to the drawing pipeline in general.

2 Likes

Oh man, thank you for putting in that time! I was already browsing up and down why I dont have nor find nor be able to reference my .dll although using Rhino7.. Maybe it’s because of the new macOS versions but this file works!! Natively

Yeah I’m not really sure what’s going on (ping @DanielPiker). From what I can tell the clr.AddReferenceByName("KangarooSolver") method gets the exact same assembly as using the by file and path reference method. Could be some .NET/core stuff that broke things maybe:


K2VersionsRhino8.gh (5.1 KB)

But hey, if it works :man_shrugging:

1 Like

Yea it does haha. A very last question towards this topic: In the last script, the bending analysis, it is missing your very own module adh to visualize the bending stress, any chance to obtain that? :face_with_peeking_eye:

Appreciate your very much insightful help already!

1 Like

Ah yeah, sorry. Here’s the version from the latest workshop:

"""
Utility functions for writing GHPython scripts
Author: Anders Holden Deleuran
Version: 200811
"""

import time
import math
from scriptcontext import sticky as st
import Rhino as rc
import Grasshopper as gh
import GhPython
import datetime
import rhinoscriptsyntax as rs
from System.Drawing import Color
import scriptcontext as sc

class Timer(object):
    
    """ A simple profiler """
    
    def start(self):

        # Start the timer
        self.startTime = time.time()
        
    def stop(self):
        
        # Print an return elapsed time
        elapsedSeconds = time.time() - self.startTime
        elapsedMilliseconds = elapsedSeconds*1000
        print str(round(elapsedMilliseconds,3)) + " ms"
        return elapsedMilliseconds

def listToTree(nestedList):
    
    """ Convert a nested python iterable to a datatree """
    
    dt = gh.DataTree[object]()
    for i,l in enumerate(nestedList):
        dt.AddRange(l,gh.Kernel.Data.GH_Path(i))
        
    return dt

def customDisplay(toggle,component):
    
    """ Make a custom display which is unique to the component and lives in sticky """
    
    # Make unique name and custom display
    displayGuid = "customDisplay_" + str(component.InstanceGuid)
    if displayGuid not in st:
        st[displayGuid] = rc.Display.CustomDisplay(True)
        
    # Clear display each time component runs
    st[displayGuid].Clear()
    
    # Return the display or get rid of it
    if toggle:
        return st[displayGuid]
    else:
        st[displayGuid].Dispose()
        del st[displayGuid]
        return None

def killCustomDisplays():
    
    """ Clear any custom displays living in the Python sticky dictionary """
    
    for k,v in st.items():
        if type(v) is rc.Display.CustomDisplay:
            v.Dispose()
            del st[k]

def remapValues(values,targetMin,targetMax,srcMin,srcMax):
    
    """ Remaps a list of values to the new domain targetMin-targetMax """
    
    #srcMin = min(values)
    #srcMax = max(values)
    
    if srcMax-srcMin > 0:
        remappedValues = []
        for v in values:
            rv = ((v-srcMin)/(srcMax-srcMin))*(targetMax-targetMin)+targetMin
            remappedValues.append(rv)
    else:
        meanVal = (targetMin+targetMax)/2
        remappedValues = [meanVal for i in range(len(values))]
        
    return remappedValues

def valuesToColors(values):
    
    """ Make a list of HSL colors, meaning that 0.0: red and 0.6: blue """
    
    colors = []
    for v in values:
        rcColor = rc.Display.ColorHSL(v,1.0,0.5)
        colors.append(rcColor)
        
    return colors

def sampleColorSpectrum(colors,t,smooth):
    
    """ Interpolate along multiple colors by 0.00-1.00 parameter t """
    
    if t <= 0.0:
        return colors[0]
    elif t >= 1.0:
        return colors[-1]
    else:
        
        # Compute spectrum t, starting color, and local t
        tSpectrum = t * (len(colors)-1)
        colorID = int(math.floor(tSpectrum))
        tLocal = tSpectrum-colorID
        
        # Use cosine interpolation (see paulbourke.net/miscellaneous/interpolation)
        if smooth:
            tLocal = (1-math.cos(tLocal*math.pi))/2
            
        # Blend colors
        cA = rc.Display.Color4f(colors[colorID])
        cB = rc.Display.Color4f(colors[colorID+1])
        cC = cA.BlendTo(tLocal,cB)
        blendColor = cC.AsSystemColor()
        
        return blendColor

def colorMeshFaces_V5(mesh,colors):
    
    """ Unwelds and color the faces of a mesh """
    
    # Get faces/vertices
    faces = mesh.Faces
    vertices = mesh.Vertices
    
    # Make empty mesh and face-vertex ID counter 
    cMesh = rc.Geometry.Mesh()
    fID = 0
    for i in range(faces.Count):
        
        # Get face and color
        f = faces[i]
        c = colors[i]
        
        # Add face vertices and colors to empty cMesh
        cMesh.Vertices.Add(vertices[f.A])
        cMesh.Vertices.Add(vertices[f.B])
        cMesh.Vertices.Add(vertices[f.C])
        cMesh.VertexColors.Add(c)
        cMesh.VertexColors.Add(c)
        cMesh.VertexColors.Add(c)
        if f.IsQuad:
            cMesh.Vertices.Add(vertices[f.D])
            cMesh.VertexColors.Add(c)
            
       # Add face
        if f.IsQuad:
            cMesh.Faces.AddFace(fID,fID+1,fID+2,fID+3)
            fID += 4
        elif f.IsTriangle:
            cMesh.Faces.AddFace(fID,fID+1,fID+2)
            fID += 3
            
    return cMesh

def colorMeshFaces_V6(mesh,colors):
    
    """ Unwelds and color the faces of the mesh in place """
    
    mesh.VertexColors.CreateMonotoneMesh(System.Drawing.Color.Black)
    mesh.Unweld(0,False)
    for i in range(mesh.Faces.Count):
        mesh.VertexColors.SetColor(mesh.Faces[i],colors[i])

def makeLegendParams(capLower,capUpper,hueLower,hueUpper,count):
    
    """ Make a list of data following the Ladybug format,
    used for generating a CustomVectorLegend """
    
    hues = floatRange(hueLower,hueUpper,count-1)
    colors = valuesToColors(hues)
    lp = [capLower,capUpper,None,colors]
    return lp

def updateComponent(ghenv,interval):
    
    """ Updates this component, similar to using a grasshopper timer """
    
    # Define callback action
    def callBack(e):
        ghenv.Component.ExpireSolution(False)
        
    # Get grasshopper document
    ghDoc = ghenv.Component.OnPingDocument()
    
    # Schedule this component to expire
    ghDoc.ScheduleSolution(interval,gh.Kernel.GH_Document.GH_ScheduleDelegate(callBack))

def buildDocString_LEGACY(ghenv):
    
    """ Builds a documentation string by iterating the component
    (i.e. ghenv.Component) input and outputs parameters """
    
    # Add component description string
    ds = '"""\n'
    ds += "Write main component documentation here.\n"
    
    # Add input parameter type properties
    ds += "    Inputs:\n"
    for v in ghenv.Component.Params.Input:
        dataType = str(v.TypeHint.TypeName).lower()
        vd = list(v.VolatileData.AllData(False))
        if vd and dataType == "system.object":
            vdType = str(vd[0].GetType())
            vdType = vdType.split(".")[-1]
            dataType = vdType.strip("GH_").lower()
        ds += "        " + v.Name + ": {" + str(v.Access).lower() + "," + dataType + "}\n"
        
    # Add output parameter type properties
    ds += "    Outputs:\n"
    for v in ghenv.Component.Params.Output:
        ds += "        " + v.Name + ": \n"
        # print globals()[v.Name]
        
    # Add author, Rhino, and script version
    ds += "    Remarks:\n"
    ds += "        Author: Anders Holden Deleuran (BIG IDEAS)\n"
    ds += "        Rhino: " + str(rc.RhinoApp.Version) + "\n"
    ds += "        Version: " + str(date.today()).replace("-","")[2:] + "\n"
    ds += '"""'
    
    print ds
    return ds

def getParameterProperties(p,globalsDict):
    
    """ Extract a component parameter name, access, and data type """

    # Get name, data and set initial type
    pName = p.Name
    pData = globalsDict[pName]
    pDataType = None

    # Determine access and data type
    if type(pData) is list:
        pAccess = "list"
        if pData:
            pDataType = type(pData[0])
    elif type(pData) is gh.DataTree[object]:
        pAccess = "tree"
        if pData: 
            pDataType = type(pData.AllData()[0])
    else:
        pAccess = "item"
        if pData is not None:
            pDataType = type(pData)
        
    # Format data type string
    if pDataType:
        pDataType = pDataType.__name__.lower()
    else:
        pDataType = "?"
    
    return pName,pAccess,pDataType

def buildDocString(globalsDict):
    
    """ Builds a documentation string by iterating the ghenv.Component input
    and outputs parameters. Call with globals() as input at very end of script """

    # Get ghenv
    ghenv = globalsDict["ghenv"]
    
    # Add component description string
    ds = '"""\n'
    ds += "Write main component documentation here.\n"
    
    # Add input parameter type properties
    ds += "    Inputs:\n"
    for p in ghenv.Component.Params.Input:
        pName,pAccess,pDataType = getParameterProperties(p,globalsDict)
        ds += "        " + pName + ": {" + pAccess + "," + pDataType + "}\n"
        
    # Add output parameter type properties
    ds += "    Outputs:\n"
    for p in ghenv.Component.Params.Output:
        pName,pAccess,pDataType = getParameterProperties(p,globalsDict)
        ds += "        " + pName + ": {" + pAccess + "," + pDataType + "}\n"
        
    # Add author, Rhino, and script version
    ds += "    Remarks:\n"
    ds += "        Author: Anders Holden Deleuran (BIG IDEAS)\n"
    ds += "        Rhino: " + str(rc.RhinoApp.Version) + "\n"
    ds += "        Version: " + str(datetime.date.today()).replace("-","")[2:] + "\n"
    ds += '"""'
    
    print ds
    return ds

def setParametersToDrawName(ghenv):
    
    """ Set all canvas parameters to always draw name """
    
    for obj in ghenv.Component.OnPingDocument().Objects:
        if obj.GetType().Namespace == "Grasshopper.Kernel.Parameters":
            obj.IconDisplayMode = gh.Kernel.GH_IconDisplayMode.name
            obj.ExpireSolution(True)

def setNoTypeHint(ghenv):
    
    """ Set all input parameters to No Type Hint, DO NOT USE YET!!"""
    
    for v in ghenv.Component.Params.Input:
        v.TypeHint = GhPython.Component.NoChangeHint()
    ghenv.Component.ExpireSolution(False)

def setTemplateLayerColors():

    """ Set Rhino layer colors to the AHD style """

    # Set context to Rhino document and disable redraw
    sc.doc = rc.RhinoDoc.ActiveDoc
    rs.EnableRedraw(False)

    # Set layer colors
    for i,l in enumerate(rs.LayerNames()):
        if i == 0:
            rs.LayerColor (l,Color.FromArgb(255,105,105,105))   
        elif i == 1:
            rs.LayerColor (l,Color.FromArgb(255,255,0,90))    
        elif i == 2:
            rs.LayerColor (l,Color.FromArgb(255,70,190,190))   
        elif i == 3:
            rs.LayerColor (l,Color.FromArgb(255,0,85,255))
        elif i == 4:
            rs.LayerColor (l,Color.FromArgb(255,130,255,0))
        elif i == 5:
            rs.LayerColor (l,Color.FromArgb(255,190,190,190))

def capValues(values,lower,upper):
    
    """ Cap values that are smaller than lower and larger than upper """
    
    capped = []
    for v in values:
        if v < lower:
            capped.append(lower)
        elif v > upper:
            capped.append(upper)
        else:
            capped.append(v)
            
    return capped

def floatRange(start,stop,steps):
    
    """ Generate a range of floats, similar to Grasshoppers Range """
    
    stepSize = (stop-start)/steps
    values = [start]
    for i in range(1,steps):
        values.append(i*stepSize+start)
    values.append(stop)
    
    return values

def closestValue(v,values):
    
    """ Find the value that is closest to v in values """
    
    i = bisect.bisect_left(values,v)
    if i == len(values):
        return i-1
    elif values[i] == v:
        return i
    elif i > 0:
        j = i-1
        if values[i] - v > v - values[j]:
            return j
    return i

def discretiseValues(values,steps):
    
    """ Bin/bucket/discretise a list of values into N steps """
    
    # Calculate the bin values
    bins = floatRange(min(values),max(values),int(Steps))
    bins.sort()
    
    # Find the closest bin for each value
    closestBins = [closestValue(v,bins) for v in values]
    
    return closestBins

def meshGrid(points,ptsU,ptsV):
    
    """ Make a quad mesh from a grid of points """ 
    
    if len(points) == ptsU*ptsV:
        
        # Make mesh and add vertices
        mesh = rc.Geometry.Mesh()
        mesh.Vertices.AddVertices(rc.Collections.Point3dList(points))
        
        # Add faces
        for i in range(ptsU-1):
            for j in range(0,ptsU*(ptsV-1),ptsU):
                ij = i+j
                mesh.Faces.AddFace(ij, ij+ptsU, ij+ptsU+1, ij+1)
                
        # Compute normals and return
        mesh.Normals.ComputeNormals()
        
        return mesh

def ghSolutionRecompute(ghenv):
    
    """ Recomputes the Grasshopper solution (ala pressing F5) """
    
    def expireAllComponentsButThis(e):
        for obj in ghenv.Component.OnPingDocument().Objects:
            if not obj.InstanceGuid == ghenv.Component.InstanceGuid:
                obj.ExpireSolution(False)
                
    ghenv.Component.OnPingDocument().ScheduleSolution(1000,expireAllComponentsButThis)

Also, figured out why nothing was drawn (updated casting behaviour) and added quickie hot fix:


201210_FAHS_InteractiveModellingPipeline_Minimal_Rhino8_01.gh (43.1 KB)

That said, I would recommend ditching the custom display class in favour of using DrawViewportWires in SDK mode (much less janky and way more drawing methods).

2 Likes

I guess to implement the ahd I have to safe it as a .py file, and then reference through EditPythongScripts Search Paths right? While you nonchalantly fixed old codes light-handed it helped me finally soft launch into understanding the links in cross-platforming in ghcoding a little haha

I’m not entirely sure how it works on Mac, but yes. Any folder that is in the sys.path should work. I usually use the default Rhino scripts folder:

You can also use sys.path.append to dynamically reference stuff within the code.

1 Like

Ah okay good to know. I freestyled and did exactly what I thought would work from last comment and it worked, so.. :smiley:

Thank you very much for your time and insights, they were very helpful and I appreciate your sharing of knowledge.

1 Like

Hey there, I sadly did ran into another problem.. When I replace the internalized geometry of two cables and one polyline with my own one (In this case a more or less exact replica of your geometry) the solver spits out this polygon shape. And I cannot figure out exactly why. Does it have to do with my cables being line-like curves instead if linear curves? And otherwise it doesnt even show the DivideCurvepoints created. (Left side of rhino viewport is your geo, right one is mine. Also used entwine to fully replicate your given way)

If you were to spare a minute of your time and maybe help me figure out whats the problem with using own geometries I would really appreciate that, as im currently being stuck with this for my thesis.

Thanks in advance and otherwise a lovely Christmas time!

Think I accidentally marked the last reply as spam yesterday, my bad!

The beam polylines must be subdivided, see e.g. this section from one of the papers above:

Here’s a version of the pipeline that includes the discretisation (which was also since updated to automatically find and insert polyline vertices where the cables attach) and dimensioning stages. A Windows update just tanked my Rhino 8 install, so these bits are only known to work in Rhino 7:

201210_FAHS_InteractiveModellingPipeline_Minimal_01.gh (52.2 KB)

Be sure to post it if you’re allowed (or by PM maybe). Would be interesting to see how people are designing bendy things these days :slight_smile:

1 Like

Oh nice thank you again haha. And for sure I can share my outcome at the end :slight_smile:

Im doing my thesis about fog harvesting methods and how to imply that more into architecture. And my prof wants something ‘fancy’ for a prototype in 1:1, and by this extra parameter I cant just generate wild shapes, as they have to have a certain amount of vertical surface and some commons gutter points where the water droplets run to etc. Thats where im still tryna work out a good combo between water collection and good fitness values giving interesting shapes. I also got a simple EA running for that and rn as I’m thinking about it, combining that outputs with this pipeline could make an interesting case :smiley:

Wondering, I thought about if it were possible to somehow ‘reverse engineer’ this generated curve. The curvature is great regarding my fitness criteria and I placed a combined anchor point in the middle of the shape and then use cables to hold it exactly in place. But when now putting this through the Bending-active pipeline it bends the shape even more (logically)

So im wondering if unrolling that curve with having the exact length goals for each of these cables so that the curve will relax and bend into the shape back could work :thinking:

Have to say worked my way through the script and occasional errors but made it work at the end! So reverse engineering sounds like my best way to go for that

If you’re interested in the progress of me trying to reverse engineer that. After changing from a base circular curve to a curve thats similar shaped to my curve, and using the difference of 2D length from cable point to anchor to 3D length from cable point to anchor as the Cable TargetLengths, I near myself to a 70% similar replication. Just have to adjust the cable strengths I guess but hey it seems to work somehow. But I think in order for this to really work some use of stiffening would be needed to get the sharp edges e.g.

Or I will use steel stripes for the real model, would probably help in that case

Edit: Picture

I’m not quite sure I follow, but if the goal is to find a given bent elastica curve, I’d have a look at the Lace Wall paper. Where we developed this pipeline to generate pseudo-Steiner-tree cable networks that constrain two slender beams into a wall unit:

Which I hooked up to Galapagos and recorded good fit units:

That were aggregated into this wall and simulated (using K2Engineering):

Also, a merry Christmas to all :christmas_tree:

1 Like