home processing download documents tutorial python tutorial gallery source about
 Python Tutorials (back to the list of tutorials)

Network Growth Agent (requires iGeo version 0.9.0.3)

     Reproduction Based Agent and Branching

This tutorial describes design of agents who connect each other to form network linkage structure and also to seek emergence of clusters and network hubs.

We start with reroduction-base agents to design networking agents and later it's integrated with particle-based agents. The code below defines a reproduction-base agent who a new agent in a random location in a specified distance and make connection to it. One agent makes just one child agent and as a result, it becomes just a single string of agents. The agent inherets IPointAgent which has a property of point and a methodd pos().

add_library('igeo')

def setup() : 
    size(480,360,IG.GL)
    NodeAgent(IVec(0,0,0))

class NodeAgent(IPointAgent) : 
    def __init__(self, p) : 
        IPointAgent.__init__(self,p)
    
    def update(self) : 
        if self.time() == 0 : #just once when it's created
            dir = IRand.dir(10) #random direction with length 10
            pos2 = self.pos().cp().add(dir) #new position by adding dir
            ICurve(self.pos(), pos2) #link line
            NodeAgent(pos2) #child agent

The agent in the next code creates a child agent in a different timing from the previous one. The previous code creates a child agent just once in the first time frame after it's created. On the other hand next one tries to create a child agent every time frame in a random probability of 1%.

add_library('igeo')

def setup() : 
    size(480,360,IG.GL)
    NodeAgent(IVec(0,0,0))

class NodeAgent(IPointAgent) : 
    def __init__(self, p) : 
        IPointAgent.__init__(self,p)
    
    def update(self) : 
        if IRand.pct(1) : #random probability of 1%
            dir = IRand.dir(10)
            pos2 = self.pos().cp().add(dir)
            ICurve(self.pos(), pos2)
            NodeAgent(pos2)

As a result of this algorithm, one agent could create multiple child agents over time and branches of linked lines. The topological form of the network generated here is a tree structure and there is no loop in the network.

The next code generates the same network topology in a tree structure but the geometrical layout is controlled by orienting the direction to put a child agent towards the specified vector (1,0,0) (x-axis). The direction is calculated by the method IRand.dir(orientation, length, angle_range) and this method returns a vector towards a random direction within the angle range from the specified orientation and the vector length is also specified. As a result, it generates branches towards x-axis direction.

add_library('igeo')

def setup() : 
    size(480,360,IG.GL)
    NodeAgent(IVec(0,0,0))

class NodeAgent(IPointAgent) : 
    def __init__(self, p) : 
        IPointAgent.__init__(self,p)
    
    def update(self) : 
        if IRand.pct(1) : #random probability of 1%
            #random vector towards 1,0,0 within PI/4 range
            dir = IRand.dir(IVec(1,0,0), 10, PI/4) 
            pos2 = self.pos().cp().add(dir)
            ICurve(self.pos(), pos2)
            NodeAgent(pos2)


     Connecting Existing Adjacent Agents

The codes in the previous section produce only tree structure. The following code allow an agent to connect to other existing agents than its parent or new child agents, which creates loops in the network. To implement this behavior, it uses the interact method which receives all existing agents and an agent can influence other agents' states and/or it changes its state influenced by others.

In the interact method, an agent checks all existing agents and pick ones whose type is NodeAgent. Then after excluding itself, it measures the distance between the agent and the other. If it's closer than the parameter linkThreshold, then draws a line as a link in 0.1% probability.

add_library('igeo')

def setup() : 
    size(480,360,IG.GL)
    NodeAgent(IVec(0,0,0))

class NodeAgent(IPointAgent) : 
    linkLength = 10 #distance of child agent
    linkThreshold = 10 #distance threshold to connect existing agent
    def __init__(self, p) : 
        IPointAgent.__init__(self,p)
    
    # connecting to existing agent
    def interact(self, agents) : 
        for agent in agents : 
            if isinstance(agent, NodeAgent) : #check type of agent
                if agent is not this : #exclude itself
                    if agent.pos().dist(self.pos()) < NodeAgent.linkThreshold : #closer than threshold
                        if IRand.pct(0.1) : #in a probability of 0.1%
                            ICurve(agent.pos(), self.pos()).clr(1.0,0,0) #red line
    
    def update(self) : 
        if IRand.pct(1) : #random probability of 1%
            #random vector towards 1,0,0 within PI/4 range
            dir = IRand.dir(NodeAgent.linkLength)
            pos2 = self.pos().cp().add(dir)
            ICurve(self.pos(), pos2)
            NodeAgent(pos2)

The next code simply add the interact method to the branching agent oriented towards x-axis direction in the previous section.

add_library('igeo')

def setup() : 
    size(480,360,IG.GL)
    NodeAgent(IVec(0,0,0))

class NodeAgent(IPointAgent) : 
    linkLength = 10 #distance of child agent
    linkThreshold = 10 #distance threshold to connect existing agent
    def __init__(self, p) : 
        IPointAgent.__init__(self,p)
    
    # connecting to existing agent
    def interact(self, agents) :
        for agent in agents : 
            if isinstance(agent, NodeAgent) : #check type of agent
                if agent is not this : #exclude itself
                    if agent.pos().dist(self.pos()) < NodeAgent.linkThreshold : #closer than threshold
                        if IRand.pct(0.1) : #in a probability of 0.1%
                            ICurve(agent.pos(), self.pos()).clr(1.0,0,0) #red line
    
    def update(self) : 
        if IRand.pct(1) : #random probability of 1%
            #random vector towards 1,0,0 within PI/4 range
            dir = IRand.dir(IVec(1,0,0), NodeAgent.linkLength, PI/4)
            pos2 = self.pos().cp().add(dir)
            ICurve(self.pos(), pos2)
            NodeAgent(pos2)

The next code modifies the previous one a little bit by adding a new property dir for each agent and let it set the property when it's created at the constructor NodeAgent(IVec p, IVec d). Then this property dir is used as orientation of the direction to create a new child.

In setup() method, two instances of NodeAgent are created with different starting locations and opposite direction for the input of dir. When two networks grow out of two different parents towards opposite direction, two networks are interconnected by the algorithm defined in interact method.

add_library('igeo')

def setup() : 
    size(480,360,IG.GL)
    NodeAgent(IVec(0,0,0), IVec(1,0,0))
    NodeAgent(IVec(100,0,0), IVec(-1,0,0))

class NodeAgent(IPointAgent) : 
    linkLength = 10 #distance of child agent
    linkThreshold = 10 #distance threshold to connect existing agent
    def __init__(self, p, d) : 
        IPointAgent.__init__(self,p)
        self.dir = d
    
    # connecting to existing agent
    def interact(self, agents) :
        for agent in agents : 
            if isinstance(agent, NodeAgent) : #check type of agent
                if agent is not this : #exclude itself
                    if agent.pos().dist(self.pos()) < NodeAgent.linkThreshold : #closer than threshold
                        if IRand.pct(0.1) : #in a probability of 0.1%
                            ICurve(agent.pos(), self.pos()).clr(1.0,0,0) #red line
    
    def update(self) : 
        if IRand.pct(1) : #random probability of 1%
            #random vector towards 1,0,0 within PI/4 range
            dir2 = IRand.dir(self.dir, NodeAgent.linkLength, PI/4)
            pos2 = self.pos().cp().add(dir2)
            ICurve(self.pos(), pos2)
            NodeAgent(pos2, self.dir)


     Network Layout by Repulsion Force

The geometrical layout of the network produced so far was based on randomness. To have smoother network layout and nicely distributed network nodes, the node agent is changed to inherit IParticle class to move freely and generate repulsion force to other node agents. The link lines are also replaced with ITensionLine to cause tensile force between linked nodes. The code limit the time to let agents make new links and grow network by growthTime, to have time to smooth the network layout by repulsion and tensile forces.

add_library('igeo')

def setup() : 
    size(480, 360, IG.GL)
    NodeAgent(IVec(0, 0, 0))

class NodeAgent(IParticle) : 
    linkLength = 10
    linkThreshold = 8.5
    tension = 10 # tensile force of link
    repulsion = 200 # repulsion force of node
    growthTime = 500 # network growth stops at this time
    
    def __init__(self, p) : 
        IParticle.__init__(self, p)
        self.fric(0.1) # 10% friction
    
    def interact(self, agents) : 
        for agent in agents : 
            if isinstance(agent, NodeAgent) : 
                if agent is not self : 
                    if IG.time() < NodeAgent.growthTime : 
                        if agent.pos().dist(self.pos()) < NodeAgent.linkThreshold : 
                            if IRand.pct(0.1) : 
                                ITensionLine(agent, self, NodeAgent.tension).clr(1.0, 0, 0)
                        dif = agent.pos().dif(self.pos()) #vector to the other node
                        if dif.len() > 0 : 
                            dif.len(NodeAgent.repulsion/dif.len2() ) #the closer, the larger
                            agent.push(dif)
  
    def update(self) : 
        if IG.time() < NodeAgent.growthTime : #only for limited time
            if IRand.pct(1) : 
                dir = IRand.dir(NodeAgent.linkLength)
                pos2 = self.pos().cp().add(dir)
                child = NodeAgent(pos2)
                ITensionLine(self, child, NodeAgent.tension)

The code below is the directed version of node agent and two networks run against each other with their roots fixed by fix() method inside setup() method.

add_library('igeo')

def setup() : 
    size(480, 360, IG.GL)
    NodeAgent(IVec(0, 0, 0), IVec(1,0,0)).fix().clr(0,0,1.0)
    NodeAgent(IVec(100, 0, 0), IVec(-1,0,0)).fix().clr(0,0,1.0)

class NodeAgent(IParticle) : 
    linkLength = 20
    linkThreshold = 8.5
    tension = 10 # tensile force of link
    repulsion = 200 # repulsion force of node
    growthTime = 600 # network growth stops at this time
    
    def __init__(self, p, d) : 
        IParticle.__init__(self, p)
        self.dir = d
        self.fric(0.1) # 10% friction
    
    def interact(self, agents) : 
        for agent in agents : 
            if isinstance(agent, NodeAgent) : 
                if agent is not self : 
                    if IG.time() < NodeAgent.growthTime : 
                        if agent.pos().dist(self.pos()) < NodeAgent.linkThreshold : 
                            if IRand.pct(0.1) : 
                                ITensionLine(agent, self, NodeAgent.tension).clr(1.0, 0, 0)
                        dif = agent.pos().dif(self.pos()) #vector to the other node
                        if dif.len() > 0 : 
                            dif.len(NodeAgent.repulsion/dif.len2() ) #the closer, the larger
                            agent.push(dif)
    
    def update(self) : 
        if IG.time() < NodeAgent.growthTime : #only for limited time
            if IRand.pct(1) : 
                dir2 = IRand.dir(self.dir, NodeAgent.linkLength, PI/4)
                pos2 = self.pos().cp().add(dir2)
                child = NodeAgent(pos2, self.dir)
                ITensionLine(self, child, NodeAgent.tension)


     Preferential Attachment

Scientific research of networks in complex science revealed characteristics of networks observed in nature and human society, one of which is the power law distribution of degree of network nodes. This means that degree (number of connection) of most of nodes is very small but very small number of nodes have very high degree. This node with high degree is called 'hub' and provides important function in a network to connect various parts of network.

The procedure to generate this type of network is also researched and Barabasi-Albert model is known to produce the power law distribution in degree of nodes and it's based on preferential attachment process

We try to simulate the behavior to generate a network with similar degree distribution to those types of networks. For this purpose, we calculate the probability to make a connection by a degree. We also need to keep track of connected nodes to calculate a degree, which is a total number of connected nodes. Connected nodes and links are stored in ArrayList properties called children and links. The part of code to make a connection and to calculate a degree is implemented as methods of connect(NodeAgnet node) and degree().

add_library('igeo')

def setup() : 
    size(480, 360, IG.GL)
    NodeAgent(IVec(0, 0, 0))

class NodeAgent(IParticle) : 
    linkLength = 10
    linkThreshold = 25
    tension = 10
    repulsion = 200
    growthTime = 350
    
    def __init__(self, p) : 
        IParticle.__init__(self,p)
        self.fric(0.1)
        self.children = [] #to keep track of connected nodes
        self.links = [] #to keep track of links
    
    # adding a method to make connection
    def connect(self, node)  : 
        if node not in self.children : #only when not connected yet
            self.children.append(node)
            node.children.append(self)
            link = ITensionLine(self, node, NodeAgent.tension)
            self.links.append(link)
            node.links.append(link)
            self.clr(self.degree()*0.1, 0, 0)
            node.clr(node.degree()*0.1, 0, 0)
    
    # adding a method to return degree (number of connection)
    def degree(self) : 
        return len(self.children)
    
    def interact(self, agents) : 
        for agent in agents : 
            if isinstance(agent, NodeAgent) : 
                if agent is not self : 
                    if IG.time() < NodeAgent.growthTime : 
                        if agent.pos().dist(self.pos()) < NodeAgent.linkThreshold : 
                            probability = sqrt(agent.degree()-5)*0.05
                            if IRand.pct(probability) : #higher degree is more probable
                                self.connect(agent)
                    dif = agent.pos().dif(self.pos())
                    if dif.len() > 0 : 
                        dif.len(NodeAgent.repulsion*self.degree()/dif.len2())
                        agent.push(dif)
    
    def update(self) : 
        if IG.time() < NodeAgent.growthTime : 
            probability = sqrt(self.degree()+1) 
            if IRand.pct(probability) : #higher degree is more probable
                dir = IRand.dir(NodeAgent.linkLength)
                pos2 = self.pos().cp().add(dir)
                child = NodeAgent(pos2)
                self.connect(child)


     Reconstructing Network from File

When you generate a network by scripting, your network might not be fully connected but have some separated and fragmented pieces or there might be something you want to edit like removing / adding some specific links or nodes.

To do this, it's best to save the network in Processing into a Rhino file once as points and lines. Then you can edit it by removing / adding points and lines. The following script can read a Rhino 3DM file (version 4) and convert all points into NodeAgent and check all lines to find which nodes are connected. It can also check which node should be fixed by a layer name. If points are in "fix" layer, the node made out of these points are fixed. You can change the layer name in the script as well. Note that the script assumes all curve in the edited model are lines and if there are NURBS curves or polylines, the result might be unexpected. If you add new lines and if it contains polylines, they should be exploded in Rhino.

add_library('igeo')

def setup() : 
    size(480, 360, IG.GL)
    IG.open("network1.3dm")
    #put all lines, all points, and points in "fix" layer.
    buildNetwork(IG.curves(),IG.points(),IG.layer("fix").points())

def buildNetwork(lines, points, fixPoints) : 
    pnum = len(points)
    lnum = len(lines)
    fnum = len(fixPoints)

    agents = []
    # creating node agents by points
    for i in range(pnum) : 
        agents.append(NodeAgent(points[i].pos()))
    
    # linking agents by lines
    for i in range(lnum) : 
        pt1 = lines[i].cp(0)
        pt2 = lines[i].cp(1)
        tolerance = 0.1
        found = False
        for j in range(pnum) : 
            if found : 
                break
            if agents[j].pos().dist(pt1) < tolerance : 
                for k in range(pnum) : 
                    if found : 
                        break
                    if agents[k].pos().dist(pt2) < tolerance : 
                        agents[j].connect(agents[k]) 
                        found = True
        lines[i].del()
    
    # fixing agents
    for i in range(pnum) : 
        for j in range(fnum) : 
            if points[i] == fixPoints[j] : 
                agents[i].fix().clr(0,1.0,1.0)
        points[i].del()


class NodeAgent(IParticle) : 
    linkLength = 10
    linkThreshold = 25
    tension = 10
    repulsion = 200
    growthTime = 0 # no growth
    
    def __init__(self, p) : 
        IParticle.__init__(self,p)
        self.fric(0.1)
        self.children = [] #to keep track of connected nodes
        self.links = [] #to keep track of links
    
    # adding a method to make connection
    def connect(self, node) : 
        if node not in self.children : #only when not connected yet
            self.children.append(node)
            node.children.append(self)
            link = ITensionLine(self, node, NodeAgent.tension)
            self.links.append(link)
            node.links.append(link)
            self.clr(self.degree()*0.1, 0, 0)
            node.clr(node.degree()*0.1, 0, 0)
    
    # adding a method to return degree (number of connection)
    def degree(self) : 
        return len(self.children)
    
    def interact(self, agents) : 
        for agent in agents : 
            if isinstance(agent, NodeAgent) : 
                if agent is not self : 
                    if IG.time() < NodeAgent.growthTime : 
                        if agent.pos().dist(self.pos()) < NodeAgent.linkThreshold : 
                            probability = sqrt(agent.degree()-5)*0.05
                            if IRand.pct(probability) : #higher degree is more probable
                                self.connect(agent)
                    dif = agent.pos().dif(self.pos())
                    if dif.len() > 0 : 
                        dif.len(NodeAgent.repulsion*self.degree()/dif.len2())
                        agent.push(dif)
    
    def update(self) : 
        if IG.time() < NodeAgent.growthTime : 
            probability = sqrt(self.degree()+1)
            if IRand.pct(probability) : #higher degree is more probable
                dir = IRand.dir(NodeAgent.linkLength)
                pos2 = self.pos().cp().add(dir)
                child = NodeAgent(pos2)
                self.connect(child)

You can edit a network by deleting points, adding points, deleting lines , drawing lines, moving points in "fix" layer in Rhino. When you reconstruct the edited model in Processing, you see new geometric network form under the new topology you made.

The following example does not only reconstruct a network but also adds some forces to deform the geometric network form. The following code adds IGravity upwards to simulate influence of the force in the tensile line network.

add_library('igeo')

def setup() : 
    size(480, 360, IG.GL)
    IG.open("network2.3dm")
    #put all lines, all points, and points in "fix" layer.
    buildNetwork(IG.curves(),IG.points(),IG.layer("fix").points())
    
    IGravity(0, 0, 0.2); # upward gravity


def buildNetwork(lines, points, fixPoints) : 
    pnum = len(points)
    lnum = len(lines)
    fnum = len(fixPoints)

    agents = []
    # creating node agents by points
    for i in range(pnum) : 
        agents.append(NodeAgent(points[i].pos()))
    
    # linking agents by lines
    for i in range(lnum) : 
        pt1 = lines[i].cp(0)
        pt2 = lines[i].cp(1)
        tolerance = 0.1
        found = False
        for j in range(pnum) : 
            if found : 
                break
            if agents[j].pos().dist(pt1) < tolerance : 
                for k in range(pnum) : 
                    if found : 
                        break
                    if agents[k].pos().dist(pt2) < tolerance : 
                        agents[j].connect(agents[k])
                        found = True
        lines[i].del()
    
    # fixing agents
    for i in range(pnum) : 
        for j in range(fnum) : 
            if points[i] == fixPoints[j] : 
                agents[i].fix().clr(0,1.0,1.0)
        points[i].del()


class NodeAgent(IParticle) : 
    linkLength = 10
    linkThreshold = 25
    tension = 10
    repulsion = 200
    growthTime = 0 # no growth
    
    def __init__(self, p) : 
        IParticle.__init__(self,p)
        self.fric(0.1)
        self.children = [] #to keep track of connected nodes
        self.links = [] #to keep track of links
    
    # adding a method to make connection
    def connect(self, node)  : 
        if node not in self.children : #only when not connected yet
            self.children.append(node)
            node.children.append(self)
            link = ITensionLine(self, node, NodeAgent.tension)
            self.links.append(link)
            node.links.append(link)
            self.clr(self.degree()*0.1, 0, 0)
            node.clr(node.degree()*0.1, 0, 0)
    
    # adding a method to return degree (number of connection)
    def degree(self) : 
        return len(self.children)
    
    def interact(self, agents) : 
        for agent in agents : 
            if isinstance(agent, NodeAgent) : 
                if agent is not self : 
                    if IG.time() < NodeAgent.growthTime : 
                        if agent.pos().dist(self.pos()) < NodeAgent.linkThreshold : 
                            probability = sqrt(agent.degree()-5)*0.05
                            if IRand.pct(probability) : #higher degree is more probable
                                self.connect(agent)
                    dif = agent.pos().dif(self.pos())
                    if dif.len() > 0 : 
                        dif.len(NodeAgent.repulsion*self.degree()/dif.len2())
                        agent.push(dif)
    
    def update(self) : 
        if IG.time() < NodeAgent.growthTime : 
            probability = sqrt(self.degree()+1)
            if IRand.pct(probability) : #higher degree is more probable
                dir = IRand.dir(NodeAgent.linkLength)
                pos2 = self.pos().cp().add(dir)
                child = NodeAgent(pos2)
                self.connect(child)


     Adding Geometry on Nodes and Links

When you get a network modeled by those methods above, the result you get is lines and points. If you want to make a 3D model with surfaces and solids, you need to model those out of points and lines.

If you can model surfaces and solids only by points and lines without checking the connection, you don't need to reconstruct the network but if you use information of connection like number of connection in each node or direction of lines connected to a node, you need to reconstruct a whole network once and then use the node and connection information to model new geometries.

The following code add a sphere at each node and its radius is calculated by a number of connection of the node. A node with more connection gets a larger sphere. The lines are simply converted into cylindrical pipe surfaces with a constant pipe radius.

add_library('igeo')

def setup() : 
    size(480, 360, IG.GL)
    IG.open("network3.3dm")
    IG.duration(0); # no force simulation
    #put all lines, all points, and points in "fix" layer.
    buildNetwork(IG.curves(),IG.points(),IG.layer("fix").points())

def buildNetwork(lines, points, fixPoints) :
    pnum = len(points)
    lnum = len(lines)
    fnum = len(fixPoints)

    agents = []
    # creating node agents by points
    for i in range(pnum) : 
        agents.append(NodeAgent(points[i].pos()))
    
    # linking agents by lines
    for i in range(lnum) : 
        pt1 = lines[i].cp(0)
        pt2 = lines[i].cp(1)
        tolerance = 0.1
        found = False
        for j in range(pnum) : 
            if found : 
                break
            if agents[j].pos().dist(pt1) < tolerance : 
                for k in range(pnum) : 
                    if found : 
                        break
                    if agents[k].pos().dist(pt2) < tolerance : 
                        agents[j].connect(agents[k])
                        found = True
        lines[i].del()
    
    # fixing agents
    for i in range(pnum) : 
        for j in range(fnum) : 
            if points[i] == fixPoints[j] : 
                agents[i].fix().clr(0,1.0,1.0)
        points[i].del()
    
    # adding geometry at nodes
    for agent in agents : 
        ISphere(agent.pos(), agent.degree())
        agent.hide() # hide points
    # adding geometry on links
    for line in lines : 
        IG.pipe(line.cp(0), line.cp(1), 0.5)

class NodeAgent(IParticle) : 
    linkLength = 10
    linkThreshold = 25
    tension = 10
    repulsion = 200
    growthTime = 0 # no growth
    
    def __init__(self, p) : 
        IParticle.__init__(self,p)
        self.fric(0.1)
        self.children = [] #to keep track of connected nodes
        self.links = [] #to keep track of links
    
    # adding a method to make connection
    def connect(self, node) : 
        if node not in self.children : #only when not connected yet
            self.children.append(node)
            node.children.append(self)
            link = ITensionLine(self, node, NodeAgent.tension)
            self.links.append(link)
            node.links.append(link)
            self.clr(self.degree()*0.1, 0, 0)
            node.clr(node.degree()*0.1, 0, 0)
    
    # adding a method to return degree (number of connection)
    def degree(self) : 
        return len(self.children)
    

The following code add a polygon mesh polyhedron at each node. Each vertex of the polyhedron is on the connection line and the size of polyhedron is proportional to a number of connection. The lines have triangular mesh stick with a constant width.

add_library('igeo')

def setup() : 
    size(480, 360, IG.GL)
    IG.open("network3.3dm")
    IG.duration(0); # no force simulation
    #put all lines, all points, and points in "fix" layer.
    buildNetwork(IG.curves(),IG.points(),IG.layer("fix").points())

def buildNetwork(lines, points, fixPoints) :
    pnum = len(points)
    lnum = len(lines)
    fnum = len(fixPoints)

    agents = []
    # creating node agents by points
    for i in range(pnum) : 
        agents.append(NodeAgent(points[i].pos()))
    
    # linking agents by lines
    for i in range(lnum) : 
        pt1 = lines[i].cp(0)
        pt2 = lines[i].cp(1)
        tolerance = 0.1
        found = False
        for j in range(pnum) : 
            if found : 
                break
            if agents[j].pos().dist(pt1) < tolerance : 
                for k in range(pnum) : 
                    if found : 
                        break
                    if agents[k].pos().dist(pt2) < tolerance : 
                        agents[j].connect(agents[k])
                        found = True
        lines[i].del()
    
    # fixing agents
    for i in range(pnum) : 
        for j in range(fnum) : 
            if points[i] == fixPoints[j] : 
                agents[i].fix().clr(0,1.0,1.0)
        points[i].del()
    
    # adding geometry at nodes
    for agent in agents : 
        deg = agent.degree()
        if deg > 0 : 
            vertices = []
            for j in range(deg) : 
                childPos = agent.children[j].pos()
                # set vertex distance by degree
                vertices.append(childPos.dif(agent.pos()).len(deg).add(agent.pos()))
            
            if deg == 3 : 
                nml = vertices[0].nml(vertices[1], vertices[2])
                center = agent.pos().cp()
                center.add(nml.len(vertices[0].dist(center)/2))
                vertices2 = []
                for j in range(3) :
                    vertices2.append(vertices[j])
                vertices2.append(center)
                vertices = vertices2
            elif deg == 2 : 
                dif = vertices[1].dif(vertices[0])
                t = dif.cross(IG.zaxis)
                if t.len() == 0 : 
                    t = dif.cross(IG.yaxis)
                t.len(dif.len()/4)
                vertices2 = []
                for j in range(2) : 
                    vertices2.append(vertices[j])
                vertices2.append(agent.pos().cp().add(t))
                t.rot(dif, PI*2/3)
                vertices2.append(agent.pos().cp().add(t))
                t.rot(dif, PI*2/3)
                vertices2.append(agent.pos().cp().add(t))
                vertices = vertices2
            elif deg == 1 : 
                dif = vertices[0].dif(agent.pos())
                t = dif.cross(IG.zaxis)
                if t.len() == 0 :
                    t = dif.cross(IG.yaxis)
                t.len(dif.len()/2)
                vertices2 = []
                vertices2.append(vertices[0])
                vertices2.append(agent.pos().dup().add(t))
                t.rot(dif, PI*2/3)
                vertices2.append(agent.pos().dup().add(t))
                t.rot(dif, PI*2/3)
                vertices2.append(agent.pos().dup().add(t))
                vertices = vertices2
            
            IMesh.polyhedron(vertices)
        
        agent.hide() # hide points
    
    # adding geometry on links
    for line in lines : 
        IG.meshPolygonStick(line.cp(0), line.cp(1), 1, 3)

class NodeAgent(IParticle) : 
    linkLength = 10
    linkThreshold = 25
    tension = 10
    repulsion = 200
    growthTime = 0 # no growth
    
    def __init__(self, p) : 
        IParticle.__init__(self,p)
        self.fric(0.1)
        self.children = [] #to keep track of connected nodes
        self.links = [] #to keep track of links
    
    # adding a method to make connection
    def connect(self, node) : 
        if node not in self.children : #only when not connected yet
            self.children.append(node)
            node.children.append(self)
            link = ITensionLine(self, node, NodeAgent.tension)
            self.links.append(link)
            node.links.append(link)
            self.clr(self.degree()*0.1, 0, 0)
            node.clr(node.degree()*0.1, 0, 0)
    
    # adding a method to return degree (number of connection)
    def degree(self) : 
        return len(self.children)
    

The following code is very similar to the previous one but this one add a polygon mesh polyhedron whose vertices are at the location of other connected nodes.

add_library('igeo')

def setup() : 
    size(480, 360, IG.GL)
    IG.open("network3.3dm")
    IG.duration(0); # no force simulation
    #put all lines, all points, and points in "fix" layer.
    buildNetwork(IG.curves(),IG.points(),IG.layer("fix").points())

def buildNetwork(lines, points, fixPoints) :
    pnum = len(points)
    lnum = len(lines)
    fnum = len(fixPoints)

    agents = []
    # creating node agents by points
    for i in range(pnum) : 
        agents.append(NodeAgent(points[i].pos()))
    
    # linking agents by lines
    for i in range(lnum) : 
        pt1 = lines[i].cp(0)
        pt2 = lines[i].cp(1)
        tolerance = 0.1
        found = False
        for j in range(pnum) : 
            if found : 
                break
            if agents[j].pos().dist(pt1) < tolerance : 
                for k in range(pnum) : 
                    if found : 
                        break
                    if agents[k].pos().dist(pt2) < tolerance : 
                        agents[j].connect(agents[k])
                        found = True
        lines[i].del()
    
    # fixing agents
    for i in range(pnum) : 
        for j in range(fnum) : 
            if points[i] == fixPoints[j] : 
                agents[i].fix().clr(0,1.0,1.0)
        points[i].del()
    
    # adding geometry at nodes
    for agent in agents : 
        deg = agent.degree()
        if deg > 0 : 
            vertices = []
            for j in range(deg) : 
                childPos = agent.children[j].pos()
                # set vertex distance by degree
                vertices.append(childPos)
            
            if deg == 3 : 
                nml = vertices[0].nml(vertices[1], vertices[2])
                center = agent.pos().cp()
                center.add(nml.len(vertices[0].dist(center)/2))
                vertices2 = []
                for j in range(3) :
                    vertices2.append(vertices[j])
                vertices2.append(center)
                vertices = vertices2
            elif deg == 2 : 
                dif = vertices[1].dif(vertices[0])
                t = dif.cross(IG.zaxis)
                if t.len() == 0 : 
                    t = dif.cross(IG.yaxis)
                t.len(dif.len()/4)
                vertices2 = []
                for j in range(2) : 
                    vertices2.append(vertices[j])
                vertices2.append(agent.pos().cp().add(t))
                t.rot(dif, PI*2/3)
                vertices2.append(agent.pos().cp().add(t))
                t.rot(dif, PI*2/3)
                vertices2.append(agent.pos().cp().add(t))
                vertices = vertices2
            elif deg == 1 : 
                dif = vertices[0].dif(agent.pos())
                t = dif.cross(IG.zaxis)
                if t.len() == 0 : 
                    t = dif.cross(IG.yaxis)
                t.len(dif.len()/2)
                vertices2 = []
                vertices2.append(vertices[0])
                vertices2.append(agent.pos().dup().add(t))
                t.rot(dif, PI*2/3)
                vertices2.append(agent.pos().dup().add(t))
                t.rot(dif, PI*2/3)
                vertices2.append(agent.pos().dup().add(t))
                vertices = vertices2
            
            IMesh.polyhedron(vertices)
        
        agent.hide() # hide points
    
    # adding geometry on links
    for line in lines : 
        IG.meshPolygonStick(line.cp(0), line.cp(1), 1, 3)


class NodeAgent(IParticle) : 
    linkLength = 10
    linkThreshold = 25
    tension = 10
    repulsion = 200
    growthTime = 0 # no growth
    
    def __init__(self, p) : 
        IParticle.__init__(self,p)
        self.fric(0.1)
        self.children = [] #to keep track of connected nodes
        self.links = [] #to keep track of links
    
    # adding a method to make connection
    def connect(self, node) : 
        if node not in self.children : #only when not connected yet
            self.children.append(node)
            node.children.append(self)
            link = ITensionLine(self, node, NodeAgent.tension)
            self.links.append(link)
            node.links.append(link)
            self.clr(self.degree()*0.1, 0, 0)
            node.clr(node.degree()*0.1, 0, 0)
    
    # adding a method to return degree (number of connection)
    def degree(self) : 
        return len(self.children)
    

The following code uses an input Rhino file. The rhino file contains one polygon mesh and this mesh is copied to each node and scaled by a factor porportional to a number of connection, and rotated towards one of link directions. Triangular truss like geometries are modeled programatically out of input lines but the truss width is calculated also by a number of connection of end point nodes.

add_library('igeo')

def setup() : 
    size(480, 360, IG.GL)
    IG.open("network3.3dm")
    IG.open("mesh_part1.3dm")
    IG.duration(0); # no force simulation
    #put all lines, all points, and points in "fix" layer.
    buildNetwork(IG.curves(),IG.points(),IG.layer("fix").points(),IG.mesh(0))

def buildNetwork(lines, points, fixPoints, nodeGeometry) :
    pnum = len(points)
    lnum = len(lines)
    fnum = len(fixPoints)
    
    agents = []
    # creating node agents by points
    for i in range(pnum) : 
        agents.append(NodeAgent(points[i].pos()))
    
    # linking agents by lines
    linkedNodePairs = []
    for i in range(lnum) : 
        pt1 = lines[i].cp(0)
        pt2 = lines[i].cp(1)
        tolerance = 0.1
        found = False
        for j in range(pnum) : 
            if found : 
                break
            if agents[j].pos().dist(pt1) < tolerance : 
                for k in range(pnum) : 
                    if found : 
                        break
                    if agents[k].pos().dist(pt2) < tolerance : 
                        agents[j].connect(agents[k]) 
                        linkedNodePairs.append([agents[j], agents[k]])
                        found = True
        lines[i].del()
    
    # fixing agents
    for i in range(pnum) : 
        for j in range(fnum) : 
            if points[i] == fixPoints[j] : 
                agents[i].fix().clr(0,1.0,1.0)
        points[i].del()
    
    # adding geometry at nodes
    for agent in agents : 
        deg = agent.degree()
        if deg > 0 : 
            nodeMesh = nodeGeometry.cp()
            nodeMesh.add(agent.pos().dif(nodeGeometry.center()))
            nodeMesh.scale(agent.pos(), agent.degree()*1.5)
            angle = agent.children[0].pos().dif(agent.pos()).angle(IG.xaxis, IG.zaxis)
            nodeMesh.rot(agent.pos(), IG.zaxis, angle) 
            nodeMesh.clr(agent.degree()*0.03) 
        agent.hide() # hide points
    
    nodeGeometry.del()
    # adding geometry on links
    for nodePair in linkedNodePairs : 
        pt1 = nodePair[0].pos()
        pt2 = nodePair[1].pos()
        deg1 = nodePair[0].degree()
        deg2 = nodePair[1].degree()
        
        dir = pt2.dif(pt1)
        sideDir = dir.cross(IG.zaxis)
        if dir.isParallel(IG.zaxis) :
            sideDir = IVec(1,0,0)
        
        trussLineNum = 3
        radius1 = deg1*0.8
        radius2 = deg2*0.8
        trussLines = []
        for j in range(trussLineNum) : 
            linePt1 = pt1.cp().add(sideDir.cp().rot(dir,2*PI*j/trussLineNum).len(radius1))
            linePt2 = pt2.cp().add(sideDir.cp().rot(dir,2*PI*j/trussLineNum).len(radius2))
            trussLines.append(ICurve(linePt1, linePt2))
        
        trussSpacing = 10
        length = dir.len()
        trussSegNum = (int)(length/trussSpacing)
        if trussSegNum == 0 : 
            trussSegNum = 1
        trussRadius = 0.3
        for j in range(trussLineNum) : 
            for k in range(trussSegNum+1) : 
                trussPt1 = trussLines[j].pt( 1.0*k/trussSegNum )
                trussPt2 = trussLines[(j+1)%trussLineNum].pt( 1.0*k/trussSegNum )
                IG.meshSquareStick(trussPt1, trussPt2, trussRadius)
                if k < trussSegNum : 
                    trussPt3 = trussLines[j].pt( 1.0*(k+1)/trussSegNum )
                    IG.meshSquareStick(trussPt3, trussPt2, trussRadius)
            IG.meshSquareStick(trussLines[j].pt(0), trussLines[j].pt(1), trussRadius)


class NodeAgent(IParticle) : 
    linkLength = 10
    linkThreshold = 25
    tension = 10
    repulsion = 200
    growthTime = 0 # no growth
    
    def __init__(self, p) : 
        IParticle.__init__(self,p)
        self.fric(0.1)
        self.children = [] #to keep track of connected nodes
        self.links = [] #to keep track of links
    
    # adding a method to make connection
    def connect(self, node) : 
        if node not in self.children : #only when not connected yet
            self.children.append(node)
            node.children.append(self)
            link = ITensionLine(self, node, NodeAgent.tension)
            self.links.append(link)
            node.links.append(link)
            self.clr(self.degree()*0.1, 0, 0)
            node.clr(node.degree()*0.1, 0, 0)
    
    # adding a method to return degree (number of connection)
    def degree(self) : 
        return len(self.children)
    


     Swarm and Network

Besides making geometry out of network nodes and lines directly, you can also use network geometry as an environment for swarm to interact with and then let swarm agents make geomtry.

The next code reconstruct a network and convert network link lines as curve tangent fields. Boid agents are created at nodes which have many connections and at fixed nodes, and they fly around the network influenced by the force field made by the network.

add_library('igeo')

def setup() : 
    size(480, 360, IG.GL)
    IG.open("network3.3dm")
    IG.bg(0)
    #put all lines, all points, and points in "fix" layer.
    buildNetwork(IG.curves(),IG.points(),IG.layer("fix").points())


def buildNetwork(lines, points, fixPoints) :
    pnum = len(points)
    lnum = len(lines)
    fnum = len(fixPoints)

    agents = []
    # creating node agents by points
    for i in range(pnum) : 
        agents.append(NodeAgent(points[i].pos()))
    
    # linking agents by lines
    for i in range(lnum) : 
        pt1 = lines[i].cp(0)
        pt2 = lines[i].cp(1)
        tolerance = 0.1
        found = False
        for j in range(pnum) : 
            if found : 
                break
            if agents[j].pos().dist(pt1) < tolerance : 
                for k in range(pnum) : 
                    if found : 
                        break
                    if agents[k].pos().dist(pt2) < tolerance : 
                        agents[j].connect(agents[k])
                        found = True
        lines[i].del()
    
    # fixing agents
    for i in range(pnum) : 
        for j in range(fnum) : 
            if points[i] == fixPoints[j] : 
                agents[i].fix().clr(0,1.0,1.0)
        points[i].del()
    
    # making force field
    tangentField = ICompoundField()
    attractorField = ICompoundField()
    for line in lines : 
        tangentField.add(ICurveTangentField(line).intensity(200).gauss(50).bidirectional(True))
        attractorField.add(ICurveAttractorField(line).intensity(100).gauss(100))
        
    for agent in agents : 
        if agent.degree() > 6 : 
            particleNum = agent.degree()*5
            for j in range(particleNum) : 
                particlePos = agent.pos().cp().add(IRand.dir(2.0)) 
                particleVel = IRand.dir(30)
                particle = IParticleTrajectory(particlePos, particleVel)
                particle.fric(0.1).clr(1.0,0.5)
        elif agent.fixed() : 
            particleNum = agent.degree()*5
            for j in range(particleNum) : 
                particlePos = agent.pos().cp().add(IRand.dir(2.0)) 
                particleVel = IRand.dir(IG.zaxis, 50, PI/4)
                particle = IParticleTrajectory(particlePos, particleVel)
                particle.fric(0.1).clr(1.0,0.5)
        
        agent.del() # not to move


class NodeAgent(IParticle) : 
    linkLength = 10
    linkThreshold = 25
    tension = 10
    repulsion = 200
    growthTime = 0 # no growth
    
    def __init__(self, p) : 
        IParticle.__init__(self,p)
        self.fric(0.1)
        self.children = [] #to keep track of connected nodes
        self.links = [] #to keep track of links
    
    # adding a method to make connection
    def connect(self, node) : 
        if node not in self.children : #only when not connected yet
            self.children.append(node)
            node.children.append(self)
            link = ITensionLine(self, node, NodeAgent.tension)
            self.links.append(link)
            node.links.append(link)
            self.clr(self.degree()*0.1, 0, 0)
            node.clr(node.degree()*0.1, 0, 0)
    
    # adding a method to return degree (number of connection)
    def degree(self) : 
        return len(self.children)

The following code let swarm agents build more geometry by drawing lines to close neighbors.

add_library('igeo')

def setup() : 
    size(480, 360, IG.GL)
    IG.open("network3.3dm")
    #put all lines, all points, and points in "fix" layer.
    buildNetwork(IG.curves(),IG.points(),IG.layer("fix").points())


def buildNetwork(lines, points, fixPoints) :
    pnum = len(points)
    lnum = len(lines)
    fnum = len(fixPoints)

    agents = []
    # creating node agents by points
    for i in range(pnum) : 
        agents.append(NodeAgent(points[i].pos()))
    
    # linking agents by lines
    for i in range(lnum) : 
        pt1 = lines[i].cp(0)
        pt2 = lines[i].cp(1)
        tolerance = 0.1
        found = False
        for j in range(pnum) : 
            if found : 
                break
            if agents[j].pos().dist(pt1) < tolerance : 
                for k in range(pnum) : 
                    if found : 
                        break
                    if agents[k].pos().dist(pt2) < tolerance : 
                        agents[j].connect(agents[k])
                        found = True
        lines[i].del()
    
    # fixing agents
    for i in range(pnum) : 
        for j in range(fnum) : 
            if points[i] == fixPoints[j] : 
                agents[i].fix().clr(0,1.0,1.0)
        points[i].del()
    
    # making force field
    tangentField = ICompoundField()
    attractorField = ICompoundField()
    for line in lines : 
        tangentField.add(ICurveTangentField(line).intensity(200).gauss(50).bidirectional(True))
        attractorField.add(ICurveAttractorField(line).intensity(100).gauss(100))
        
    for agent in agents : 
        if agent.degree() > 6 : 
            particleNum = agent.degree()*5
            for j in range(particleNum) : 
                particlePos = agent.pos().cp().add(IRand.dir(2.0)) 
                particleVel = IRand.dir(30)
                particle = MyParticle(particlePos, particleVel)
                particle.fric(0.1).clr(1.0,0.5)
        elif agent.fixed() : 
            particleNum = agent.degree()*5
            for j in range(particleNum) : 
                particlePos = agent.pos().cp().add(IRand.dir(2.0)) 
                particleVel = IRand.dir(IG.zaxis, 50, PI/4)
                particle = MyParticle(particlePos, particleVel)
                particle.fric(0.1).clr(1.0,0.5)
        
        agent.del() # not to move


class MyParticle(IParticle) : 
    timeInterval = 2
    
    def __init__(self, pos, vel) : 
        IParticle.__init__(self,pos,vel) 
        self.prevPos = pos.cp()
        self.hide() # hide point
    
    def interact(self, agents) : 
        for agent in agents : 
            if isinstance(agent, MyParticle) : 
                if agent is not self : 
                    if self.time()%MyParticle.timeInterval==0 and self.time() > 10 : 
                        if agent.pos().dist(self.pos()) < 3 and agent.pos().dist(self.pos()) > 1 : 
                            ICurve(agent.pos().cp(), self.pos().cp()).clr(0.2,0.7,0)
    
    def update(self) : 
        if self.time()%MyParticle.timeInterval==0 and self.time() > 0 : 
            ICurve(self.prevPos, self.pos().cp()).clr(0,0.5,0)
            self.prevPos = self.pos().cp()


class NodeAgent(IParticle) : 
    linkLength = 10
    linkThreshold = 25
    tension = 10
    repulsion = 200
    growthTime = 0 # no growth
    
    def __init__(self, p) : 
        IParticle.__init__(self,p)
        self.fric(0.1)
        self.children = [] #to keep track of connected nodes
        self.links = [] #to keep track of links
    
    # adding a method to make connection
    def connect(self, node) : 
        if node not in self.children : #only when not connected yet
            self.children.append(node)
            node.children.append(self)
            link = ITensionLine(self, node, NodeAgent.tension)
            self.links.append(link)
            node.links.append(link)
            self.clr(self.degree()*0.1, 0, 0)
            node.clr(node.degree()*0.1, 0, 0)
    
    # adding a method to return degree (number of connection)
    def degree(self) : 
        return len(self.children)

You can put mesh pipes sticks but it would be heavy process to run at the same time and in this case, you'd separate the process by saving the lines once and run another script to put mesh geometry.

You can also combine other geometries produced by a network in a diffent way together.


(back to the list of tutorials)

HOME
FOR PROCESSING
DOWNLOAD
DOCUMENTS
TUTORIALS (Java / Python)
GALLERY
SOURCE CODE(GitHub)
ABOUT