Skip to content

Graph Loops

One of the fundamental limitations of parametric modeling using the visual programming (VP) paradigm is that computation is represented in the form of a directed acyclic graph (DAG). The wires between components have a source-to-target directionality, where data can only flow one way, and the graph overall can only represent trees of data flows without feedback loops.

However, for several iterative processes, especially those involving optimization, this becomes a major limitation. If all computation can be encapsulated within a scripted component, then it is trivial to introduce feedback loops via iterative constructs such as for and while loops or ever recursion. If built-in or plug-in components are used then this is impossible.

Feedback cycles are forbidden because they cause never-ending loops which eventually exhaust system resources. In order to break out of the DAG structure we need thus: (1) to communicate information outside of the graph creating a notional wire that connects the outputs of process to its input, and (2) to introduce control logic to prevent infinite loops.

Minimal Setup

The minimal feedback system requires: (1) a controller that either resets the data flow to some default values or loads and forwards data from the last cycle, (2) some regular VP graph that receives some input parameters and outputs some results, (3) the other half of the controller which just stores the result of the current cycle such that they can be loaded for the next. The Trigger component is used for enabling and disabling the cycling process.

The minimal load state controller resets data to a default value, if the push button is pressed, otherwise it loads data from Rhino's document notes, decodes the data from string type into dictionary, using the built-in json library, and outputs the values so they can be used in the subsequent VP logic graph.

Load State
import Rhino, json

if( Reset ):
    Rhino.RhinoDoc.ActiveDoc.Notes = ''
    Iteration = 0
else:
    data = json.loads( Rhino.RhinoDoc.ActiveDoc.Notes )
    Iteration = data['Iteration']

The minimal save state controller receives all data that needs to persist the cycle from its input parameters, it packages them into a dictionary and used the json library to encode them into a string stored into Rhino's document notes.

Save State
import json, Rhino

data = {
    'Iteration': Iteration + 1,
}

Rhino.RhinoDoc.ActiveDoc.Notes = json.dumps( data )

Basic Setup

The stages of a cyclical computation consist of: (1) initializing or loading the state variables of the current cycle, (2) updating of state variables with new values, and (3) persisting the updated state so it can be loaded in the next cycle.

Initialization

The data that flow along the cycles are defined as input parameters of the load-state controller. Their values represent the starting point or the default conditions. The controller must export the data via one or more output parameters so they can be used subsequently. In the reset condition the incoming initial / default values are exported. In the normal / in-loop condition the data are just deserialized and passed along.

Load State
import Rhino, json

if( Reset ):
    Rhino.RhinoDoc.ActiveDoc.Notes = ''
    Iteration = 0
    TickCount = Default
else:
    data = json.loads( Rhino.RhinoDoc.ActiveDoc.Notes )
    Iteration = data['Iteration']
    TickCount = data['TickCount']

Processing

The incoming data from the load-state controller are used as usual either via regular components or scripts. The responsibility of this logical section is to perform the requisite computations, to update the screen and eventually to output the new or next state variables for the following loop.

Processing
from Rhino.Geometry import Point3d, Circle, Line

circle = Circle( Point3d.Origin, 1.0 )
angle  = ( ThisTick / 60 ) * ( 2.0 * 3.1415 )
Handle = Line( Point3d.Origin, circle.PointAt( angle ) )

NextTick = ( ThisTick + 1 ) % 60

Persistence

The objective of this logical section is to receive the updated data from the processing stage and store them for the upcoming iteration. Even though it is possible to modify the data in this stage, for instance the iteration variable is mutated, it is not recommended. It makes more sense to keep all computation in the processing stage.

Save State
import json, Rhino

data = {
    'Iteration': Iteration + 1,
    'TickCount': TickCount
}

Rhino.RhinoDoc.ActiveDoc.Notes = json.dumps( data )

Finite State Machines

Often a computation involves several stages where different actions take place. This process can be modelled using a finite state machine.

Initialization

The aim of this version of the load state component is to generalize its purpose so there is no need to edit it again. We need a convention to achieve this, whereby only two parameters will be used (1) the name of current state of computation and (2) a list of all associated data required.

import Rhino, json

if( Reset ):
    Rhino.RhinoDoc.ActiveDoc.Notes = ''
    Iteration = 0
    State     = 'Initialize'
    Values    = []
else:
    data = json.loads( Rhino.RhinoDoc.ActiveDoc.Notes )
    Iteration = data['Iteration']
    State     = data['State']
    Values    = data['Values']

Processing

The finite state machine's job is: (1) to perform some computations based on the current state and data values, and (2) to emit the following state and the updated data.

if( State == 'Initialize' ):
    NewValues = [0.0]
    NewState  = 'Phase One'

elif( State == 'Phase One' ):
    NewValues = [value + 1 for value in Values]
    NewState  = 'Phase Two'

elif( State == 'Phase Two' ):
    NewValues = [value * 2 for value in Values]
    NewState = 'Phase One'

else:
    raise Exception( 'Unknown State' )

Persistance

This version of the save state component also aims to require not more changes from this point onwards. Therefore, all it needs to do is to store the new state and data.

import json, Rhino

data = {
    'Iteration': Iteration + 1,
    'State':     State,
    'Values':    Values
}

Rhino.RhinoDoc.ActiveDoc.Notes = json.dumps( data )

Gradient Descent

The objective of this demonstration is: (1) to present how the finite state machine template can be modified and reused, towards a very simple optimization task, and (2) to illustrate how to input loop-invariant parameters and output geometric elements.

In the initialization state the output Geometry is empty, the initial parameter of the surface point is set to u, v = 0.0, 0.0 and the new state is set to Follow Gradient. In that state, the u, v values are unpacked, the point and normal of the surface at the parameters u, v are evaluated, the direction of steepest descent is computed by a sandwich cross products with the vertical Z direction, the new position q is computed as a small step towards the right direction, and its parameters are recovered so they can be used in the next iteration. For visualization a sphere is emitted via the Geometry output parameter. Finally, the new values are packed and new state is set to the current state, that is to keep following the gradient.

from Rhino.Geometry import Vector3d, Sphere

if( State == 'Initialize' ):
    Geometry  = []
    NewValues = [0.0, 0.0]
    NewState  = 'Follow Gradient'

elif( State == 'Follow Gradient' ):
    u, v = Values

    p = Landscape.PointAt( u, v )
    n = Landscape.NormalAt( u, v )
    Z = Vector3d.ZAxis
    g = Vector3d.CrossProduct( Vector3d.CrossProduct( Z, n ), Z )
    q = p + g * 0.05
    _, u, v = Landscape.ClosestPoint( q )
    Geometry = [Sphere( p + n * 0.1, 0.1 )]

    NewValues = [u, v]
    NewState  = State

else:
    raise Exception( 'Unknown State' )

State Management

Serialization of state data via json and storage in Rhino's notes provides a layer of separation between cycles but introduces latencies, data conversion complications because not all native objects can be converted to json strings easily, and limits the number of components using the notes store concurrently. Avoiding all of the above can be achieved by passing along the python scripting engine from the load state component to the save state component.

Initialization

The initialization code defines a static data class that wraps around the concept of iteration data. It should not be edited but used verbatim in the copy-paste the whole component sense.

The components purpose is: (1) to package common data, namely the cycle number, the state identifier and user data, (2) to provide a mutation mechanism via the Update( ) method used by subsequent components, and (3) to bypass Grasshopper's automatic conversions, which tends to mess up the data flows.

''' The Current Scripting Engine
'''
Engine = ghenv.Engine

''' Static State Data Class
'''
class Static:
    def __init__( self,
             state = 'Initialize',
             value = None,
             cycle = 0 ):
        self.Cycle = cycle
        self.State = state
        self.Value = value

    def Update( self, state, value ):
        return Static( state, value, self.Cycle + 1 )

    def __repr__( self ):
        return 'Cycle: {}\nState: {}\nValue: {}'.format(
            self.Cycle, self.State, self.Value )

''' Peek / Poke Static Global State
'''
if( Reset ):
    Current = Static( )
    Engine.Runtime.Globals.SetVariable( '__Data__', Current )
else:
    Current = Engine.Runtime.Globals.GetVariable( '__Data__' )

Processing

The processing component is responsible for (1) responding to the initialization state condition by setting the Updated output parameter via the Update( ) method of the Current state data object. Optionally, any additional output parameters may be initialized here; and for (2) creating and responding any additional required states and data mutations. The data must be stored in the Updated output parameter via the Update( ) method. Any additional output parameters may also be set here.

The demonstration below presents a case where user-data are stored in the form of a dictionary. It is however possible and perhaps more desirable to define a data value class instead of using string:object key-value pairs. To avoid issues with passing live references between cycles which may cause crashes, it is advisable to copy or treat data as immutable when possible.

''' Import Requisite Libraries
'''
from Rhino.Geometry import Vector3d, Sphere, PolylineCurve

''' Handle Data Initialization
'''
if( Current.State == 'Initialize' ):
    ''' State and Data Update
    '''
    Updated = Current.Update(
        state = 'Follow Gradient',
        value = {
            'Params': [0.0, 0.0],
            'Points': []
        } )
    ''' Visualization (Optional)
    '''
    Geometry = []

''' Handle Specific States
'''
elif( Current.State == 'Follow Gradient' ):
    ''' Expand State Data Value
    '''
    u, v = Current.Value['Params']
    path = Current.Value['Points']

    ''' Perform Computation
    '''
    p = Landscape.PointAt( u, v )
    n = Landscape.NormalAt( u, v )
    Z = Vector3d.ZAxis
    g = Vector3d.CrossProduct( Vector3d.CrossProduct( Z, n ), Z )
    q = p + g * 0.05
    _, u, v = Landscape.ClosestPoint( q )

    ''' State and Data Update
    '''
    Updated = Current.Update(
        state = Current.State,
        value = {
            'Params': [u, v],
            'Points': path + [p]
        } )

    ''' Visualization (Optional)
    '''
    Geometry = []
    Geometry.append( Sphere( p + n * 0.1, 0.1 ) )
    if( len( path ) > 2 ):
        Geometry.append( PolylineCurve( path ) )

else:
    raise Exception( 'Unknown State' )

Serialization

There is no serialization actually taking place. The save state component silently mutates the static global state of the parent component. Since the data is pushed back into the scripting engine there is also no need for data conversions.

The output Log parameter is populated with a cropped version of the data state string dump, which may completely omitted. Note that the character limit is because long strings connected to text panels crash Grasshopper.

''' Poke Data State Back to Origin
'''
Engine.Runtime.Globals.SetVariable( '__Data__', Updated )

''' Optional Data Logging
'''
limited = str( Updated )
if( len( limited ) > 512 ):
    limited = limited[:512]
print( limited )

Resources

Grasshopper