Graph Agent Library
1. Graphs in agent-based modeling
Many commercial simulation models are agent-based models. Agent-based model is model that simulates the actions and interactions of autonomous agents in order to understand the behavior of the entire system. The agents live and interact in an environment that defines the rules and constraints for all agents.
The examples of agent-based models include:
-
Trucks delivering goods through the network of roads and facilities. Trucks here are agents. Roads, crossing points, and facilities constitute the environment where the trucks operate.
-
Underground mining model where operations in stopes are performed by mobile equipment units like drills, chargers, and loaders. These drills, chargers, and loaders are agents. The network of underground roads and significant points like stopes, ore passes and depots is the environment.
-
Model of a warehouse where forklifts move pallets over the network of passages between warehouse zones
In such models, the agents live and move in some type of network, or a graph. From simulation perspective, this graph always has some reasonable geometric representation in either 2D or 3D space. For example, this can be roads map or underground mine passages scheme.
It is important to note that for some systems the graph representation is not precise, like in the warehouse example above. Forklifts can actually move in the free space and don’t have to follow the same trajectories every time. However, for the majority of cases the graph representation of the transportation network is more convenient as it allows to simplify the modeling logic and speed up the execution at the expense of spatial accuracy. The spatial accuracy can be accounted for, for example, by creating a limited access areas, slowing down the agents that are close to each other, and other similar methods.
Some models can consist of several graphs with several types of agents living in them: a model of a mine can have both roads network and railway network. These two networks are obviously populated by different agent types.
2. Graphs
2.1. What is a graph an how it is represented in the library
Graph is an abstract structure that can represent different types of relationships between real-world objects, not necessarily networks for graph agents.
For example, graph can represent dependencies between tasks of a project.
So, Graph
class is universal and does not know anything about geometry or agents.
This is the simple example of a directed graph with 3 nodes - A, B, C and 3 arcs - AB, BA, AC:

This graph can be created using the following code:
Graph<String, String> graph = new Graph<>();
graph.addNode("A");
graph.addNode("B");
graph.addNode("C");
graph.addArc("AB", "A", "B");
graph.addArc("BA", "B", "A");
graph.addArc("AC", "A", "C");
The graph above contains just string labels in both its nodes and arcs.
The toString()
method of a graph returns a string containing the number of nodes and arcs.
For the graph we’ve just created it would be:
// This will print "3 nodes, 3 arcs"
System.out.println(graph);
The Graph
class has two inner classes - Node
and Arc
.
These 3 classes provide the basic API for graphs.
The code below illustrates retrieving lists of graph nodes and arcs:
graph.getNodes(); // This will return a list of nodes
graph.getArcs(); // This will return a list of arcs
Note that Graph
is a generic class with two type parameters that allow to specify types of the content of its nodes and arcs.
In the example above, we use String
as type of contents of both arcs and nodes.
The contents of a node or an arc is called its value.
The getNode
method returns the instance of Node
containing the specified value.
The Node
class, in its turn, has getArcsOut
method that returns the list of all arcs going out of this node:
// This will find a node with value "A" and return the number of outgoing arcs from this node, which is 2
graph.getNode("A").getArcsOut().size();
The code below uses list returned by the getArcsOut
method to access the specific arc and, ultimately, its value:
// This will return value of the first outgoing arc from the node with value "A", which is "AB"
graph.getNode("A").getArcsOut().get(0).getValue();
Similarly to getNode
, getArc
method returns the instance of Arc
containing the specified value.
Arc
class has getSource
and getDest
methods that allow to access its source and destination nodes:
// This will return reference to node that is source of arc "AC", i.e. the node with value "A"
graph.getArc("AC").getSource();
// This will return value of destination node of arc "AC", i.e. "C"
graph.getArc("AC").getDest().getValue();
containsNode
and containsArc
methods check if graph contains a node or an arc with some specified value:
// This will return false because there is no node with value "D" in the graph
graph.containsNode("D");
2.2. Geometric graphs
As can be seen from the examples above, agents move along the graph over the course of the simulation. This implies some type of spatial layouts to be assigned to graph nodes and arcs. The two most natural choices is using either a 2D or a 3D Euclidean space. We consider using a 2D space as the most practical approach because:
-
The 2 dimensions are sufficient in majority of cases
-
Having to always use the 3rd dimension often introduces unnecessary complexity
-
When needed, the third dimension can be introduced by adding a z attributes to nodes and arcs and relevant additional calculations
It is important to note that using 2d space as a base has some shortcomings that need to be addressed in some models:
-
It is very difficult to represent arcs going vertically, i.e. arcs connecting nodes with the same x and y coordinates and different z coordinate
-
Distance and speed calculations must always be adjusted for the true 3d environments, like underground mines
So, the basic space for our graph agents is a Euclidean 2d space. Graph with such 2d representation are called geometric graphs.
Any graph node a of a geometric graph corresponds to a point (x, y) in 2-dimensional space. An arc of such graph, in its turn, corresponds to a polyline, the first point of which is the same with the source node and the last point is the same as the destination node. Polyline has at least 2 points in the 2d space. Polylines consisting of less than two points are considered incorrect. Any polyline can be represented as an ordered list of points and segments, the number of segments always equals to the number of point minus 1.
2.3. Graphs directionality
At this point, we need to make a decision whether we should consider directed and undirected graphs separately. It turns out that for simulation purposes is it convenient to only have directed arcs because:
-
A directed arc is a more basic building block of the graph. Having a bi-directional connections between nodes can be represented with 2 directed arcs, while having a mono directional connection is much harder to represent with only having non-directed arcs at modeler’s disposal
-
Even when connections between nodes are inherently bidirectional, it is often needed to assign some specific properties to each direction. For example a road can have different number of lanes for each direction
However, it is important to note that using only directed arcs makes modelers explicitly specify the relations between the two directed arcs constituting a single bi-directional relationship. So, bidirectional arcs are represented with two directed arcs with the source and destination nodes swapped. The two such arcs are reverse arcs for each other. These arcs also require that their polylines are reverse polylines. A reverse polyline is the one that consists of points of the original polyline in the reverse order.
2.4. Graphs for simulation agents
Graph agent is the type of agent that lives in environment consisting of a directed graph with geometric representation in 2D space called graph environment. Graph environment has the following features:
-
Graph nodes correspond to points in 2D space
-
Graph arcs correspond to directed polylines in 2D space connecting the corresponding nodes
Graph agents can be located either at graph nodes or on graph arcs.
A specific position in either node or arc is called a geometric graph position, or simply a position. Graph agents move between positions only along graph nodes and arcs.
Agents are represented as zero-size points located in the graph and moving along this graph. For brevity, we will say that agents live in the graph.
Geometric size of agents can be both important or not important for the simulation logic depending on the chosen level of modeling abstraction. For example, when modeling intercity delivery network, trucks size is almost always not important. Oppositely, the size of cars does matter for microscopic urban traffic models.
Regardless of whether agent’s size does matter or not, it is convenient to associate every agent with a certain single point in the 2d space so that a modeler always has a simple answer to the question “Where is the agent now?”. If necessary, spatial relationships between agents are considered with the additional calculations that take their geometric characteristics into account.
2.5. Graphs with 2D geometry
In the previous section, we have learned how to create simple graphs. But those graphs did not know anything about 2D geometry, neither did they know about agents.
Let us now create a graph that corresponds to the following transportation network:

There are 3 significant points in the network above - these are points A, B, and C. A and C are connected by one-way direct line, whereas A and B are connected by a two-way polyline with one bend point.
Such scheme can represent, for example, a shop floor layout with machines located in points A, B, and C. The polylines represent trajectories of moving between the corresponding points.
Note that the scheme above resembles the graph that we had in the previous section. But this scheme represents the geometric locations of points and trajectories, not their logical connectedness.
In simulations, it is useful to combine the graph representation with geometric representation of our transportation network. Here is the way to do it:
-
Create a graph representing the logical structure of our network.
-
Use
AgentGraphNodeImpl
class as type of node values. This class hasPoint
as one of its parameters. So, every graph node will correspond to some 2D point. -
Use
AgentGraphArcImpl
class as type of arc values. This class hasPolyline
as one of its parameters. So, every graph arc will correspond to some 2D polyline.
The transportation network that we want to create needs to be suitable for agents to live in it.
The code below creates such graph:
GraphEnvironment<AgentGraphNodeImpl, AgentGraphArcImpl, GraphAgent<?, ?>> environment = new GraphEnvironment<>();
Graph<AgentGraphNodeImpl, AgentGraphArcImpl>.Node nodeA = environment.addNode(new AgentGraphNodeImpl(new Point(0, -100)));
Graph<AgentGraphNodeImpl, AgentGraphArcImpl>.Node nodeB = environment.addNode(new AgentGraphNodeImpl(new Point(150, 0)));
Graph<AgentGraphNodeImpl, AgentGraphArcImpl>.Node nodeC = environment.addNode(new AgentGraphNodeImpl(new Point(50, -200)));
Polyline polylineAB = new Polyline(new Point(0, -100), new Point(100, 0), new Point(150, 0));
environment.addArc(nodeA.getValue(), nodeB.getValue(), new AgentGraphArcImpl(polylineAB), new AgentGraphArcImpl(polylineAB.getReversed()));
Polyline polylineAC = new Polyline(new Point(0, -100), new Point(50, -100));
environment.addArc(nodeA.getValue(), nodeC.getValue(), new AgentGraphArcImpl(polylineAC));
Let us go through the code above line by line.
The following line creates an instance of GraphEnvironment
with AgentGraphNodeImpl
as type of node values and AgentGraphArcImpl
as type of arc values:
GraphEnvironment<AgentGraphNodeImpl, AgentGraphArcImpl, GraphAgent<?, ?>> environment = new GraphEnvironment<>();
Next, we add a node of type AgentGraphNodeImpl
at point with coordinates of (0, -100):
Graph<AgentGraphNodeImpl, AgentGraphArcImpl>.Node nodeA = environment.addNode(new AgentGraphNodeImpl(new Point(0, -100)));
In the next 2 lines, we add nodes B and C with the similar code:
Graph<AgentGraphNodeImpl, AgentGraphArcImpl>.Node nodeB = environment.addNode(new AgentGraphNodeImpl(new Point(150, 0)));
Graph<AgentGraphNodeImpl, AgentGraphArcImpl>.Node nodeC = environment.addNode(new AgentGraphNodeImpl(new Point(50, -200)));
The following code creates a Polyline
from node A to node B with one bend point:
Polyline polylineAB = new Polyline(new Point(0, -100), new Point(100, 0), new Point(150, 0));
After the trajectory polyline between nodes A and B, and the nodes added to the environment, we can create a bidirectional arc:
environment.addArc(nodeA.getValue(), nodeB.getValue(), new AgentGraphArcImpl(polylineAB), new AgentGraphArcImpl(polylineAB.getReversed()));
There are several aspects to be noted in the code above:
-
addArc
method with 4 parameters allows to add bidirectional arcs by specifying the node values, and the values of forward and backward arcs. -
Trajectory of backward arc is calculated by reversing the forward trajectory with
getReversed
method ofPolyline
class.
In the last two lines of the code, we add a monodirectional arc using addArc
method with 3 parameters:
Polyline polylineAC = new Polyline(new Point(0, -100), new Point(50, -100));
environment.addArc(nodeA.getValue(), nodeC.getValue(), new AgentGraphArcImpl(polylineAC));
Code that constructs the graph environment has a lot of generics making type declarations cumbersome.
This code can be slightly simplified with var
keyword introduced in Java 9:
var environment = new GraphEnvironment<AgentGraphNode, AgentGraphArc, GraphAgent<?, ?>>();
var nodeA = environment.addNode(new AgentGraphNodeImpl(new Point(0, -100)));
var nodeB = environment.addNode(new AgentGraphNodeImpl(new Point(150, 0)));
var nodeC = environment.addNode(new AgentGraphNodeImpl(new Point(50, -200)));
var polylineAB = new Polyline(new Point(0, -100), new Point(100, 0), new Point(150, 0));
var pairOfArcsAB = environment.addArc(nodeA.getValue(), nodeB.getValue(), new AgentGraphArcImpl(polylineAB), new AgentGraphArcImpl(polylineAB.getReversed()));
var arcAB = pairOfArcsAB.first;
var arcBA = pairOfArcsAB.second;
var polylineAC = new Polyline(new Point(0, -100), new Point(50, -100));
var arcAC = environment.addArc(nodeA.getValue(), nodeC.getValue(), new AgentGraphArcImpl(polylineAC));
3. Positions in the graph
3.1. Concept of a graph position
To describe a agent’s position in a 2d Euclidean space, it is sufficient to use a (x, y) point. Describing a similar position in a graph requires some additional attributes. It is important to note that a graph position can be used to describe not only position of some agent, but also some specific place in a graph without immediate relevance to an agent.
So, what are the possible positions in a graph with 2d representation we described above? There are two main types of positions:
-
Position at node
-
Position on arc
A position in graph is represented with GeometricGraphPosition
class.
Its key methods are getNode
, getArc
, and getArcAbsOffset
.
These methods allow to determine the node or arc of a position.
In the case of position being on an arc, getArcAbsOffset
method returns the absolute offset of the position on the arc’s polyline.
The following listing shows how to create GeometricGraphPosition
instances and use their basic methods:
var positionAtNode = new GeometricGraphPosition<>(nodeA);
var positionOnArc = new GeometricGraphPosition<>(nodeA.getArcsOut().get(0), 50);
// The code below will print out the node
System.out.println(positionAtNode.getNode());
// The code below will print out null
System.out.println(positionAtNode.getArc());
// The code below will print out NaN
System.out.println(positionAtNode.getArcAbsOffset());
// The code below will print out null
System.out.println(positionOnArc.getNode());
// The code below will print out the arc
System.out.println(positionOnArc.getArc());
// The code below will print out 50
System.out.println(positionOnArc.getArcAbsOffset());
Any graph position corresponds to some point in the 2D-space.
This point can be retrieved by getPoint
method of GeometricGraphPosition
class.
3.2. Relations between graph positions
3.2.1. Equal positions
Two instances of GeometricGraphPosition
class pointing to the same node or to the same offset of the same arc are equal.
The standard equals
method will return true
for such instances:
var position1 = new GeometricGraphPosition<>(nodeA.getArcsOut().get(0), 50);
var position2 = new GeometricGraphPosition<>(nodeA.getArcsOut().get(0), 50);
// The code below will print out true
System.out.println(position1.equals(position2));
3.2.2. Reverse positions
The two positions pointing to the same point on arcs that are opposite to each other, are called reverse positions.
In other words, a reverse position is a position of an agent immediately after turning around on a bi-directional arc.
The equalsOrReverse
method checks if any given position is equal to or a reverse of the current position:
// We create the two reverse positions in the middle of reverse arcs AB and BA
var position1 = new GeometricGraphPosition<>(arcAB, arcAB.getValue().getLength() / 2);
var position2 = new GeometricGraphPosition<>(arcBA, arcBA.getValue().getLength() / 2);
// The code below will print out true, as the two positions are reverse of each other
System.out.println(position1.equalsOrReverse(position2, environment.getGraph()));
// The code below will print out false, as the two positions are not equal to each other
System.out.println(position1.equals(position2));
Note how we pass the graph as a second argument to the equalsOrReverse
method.
This is necessary because the arcs themselves do not know anything about their reverse arcs, and the information about reverse arcs is stored inside the graph.
3.2.3. Adjacent positions
The positions between which there is zero distance along the graph are called adjacent. Examples of adjacent positions include:
-
Equal positions
-
Reverse positions
-
A position at a node and a position at the very beginning of an arc going from this node
-
A position at the very end of an arc coming to some node and a position at the very beginning of an arc going from this node
The equalsOrAdjacent
method checks if any given position is equal or adjacent to the current position:
// This position points to the very beginning of arcAB that goes from node A
var position1 = new GeometricGraphPosition<>(arcAB, 0);
// This position points to node A
var position2 = new GeometricGraphPosition<>(nodeA);
// This position points to the very end of arcBA that is the reverse arc of arcAB
var position3 = new GeometricGraphPosition<>(arcBA, arcBA.getValue().getLength());
// The code below will print out true, as the two positions are adjacent
System.out.println(position1.equalsOrAdjacent(position2, environment.getGraph()));
// The code below will print out true, as the two positions are adjacent
System.out.println(position1.equalsOrAdjacent(position3, environment.getGraph()));
// The code below will print out false, as the two positions are not equal to each other
System.out.println(position1.equals(position2));
Similarly to equalsOrReverse
, equalsOrAdjacent
needs a reference to a graph passed as a second argument.
3.2.4. Coincident positions
Sometimes there are positions that correspond to the same point in the 2D-space, but not necessarily adjacent to each other.
Such positions can be called coincident.
There is no specific method in GeometricGraphPosition
class to check for coincidence.
However, coincidence can be checked by comparing the results of getPoint
methods of any two positions:
// This position points to the very beginning of arcAB that goes from node A
var position1 = new GeometricGraphPosition<>(arcAB, 0);
// This position points to node A
var position2 = new GeometricGraphPosition<>(nodeA);
// The code below will print out true, as the two positions are both conicident and adjacent
System.out.println(position1.getPoint().equals(position2.getPoint()));
4. Graph Agent basic functionality
4.1. Creating a graph agent
In one of the previous section we have created a graph environment and set up several nodes and arcs. The environment was created with this code:
var environment = new GraphEnvironment<AgentGraphNode, AgentGraphArc, GraphAgent<?, ?>>();
Now we can create a graph agent and place it into the environment:
Engine engine = new Engine();
GraphAgent<AgentGraphNode, AgentGraphArc> graphAgent = new GraphAgent<>(engine);
graphAgent.setGraphEnvironment(environment);
Not how the instance of Engine class is necessary to create an a graph agent. This engine will be used to simulate the agent’s actions.
After creation we must place the agent into an environment by calling its setGraphEnvironment
method.
Trying to call GraphAgent’s `jumpTo
or moveTo
methods before placing the agent into an environment will result in an exception.
The agent can then be removed from the graph environment by calling its removeFromGraphEnvironment
method and placed to the same or a different environment again.
4.2. Graph agent states
Although there is no explicit instance of a StateMachine
inside a graph agent, its states can be represented by the following state diagram:

Immediately after creation, an agent is not in any graph environment.
It can be assigned to a graph environment by calling setGraphEnvironment
method.
Agent can be removed from the graph environment by calling removeFromGraphEnvironment
method.
Agent can only belong to a single graph environment, so before assigning a new graph environment to an agent it needs to be removed from its current one.
After having been assigned to some environment, an agent is still not in its environment’s graph.
In other words, its position within the graph is not yet determined.
jumpTo
method places the agent into the specific node or arc of its environments’s graph.
Methods like moveTo
, moveAlongPath
, and moveToClosest
actually assign some path to an agent and start its movement.
In case of moveTo
and moveToClosest
methods, the path is determined automatically by the graph environment, whereas moveAlongPath
method allows the user to specify the path directly.
During movement, an agent might need to stop one or several times.
This can be done by setting a zero velocity using setVelocity
method.
Setting a zero velocity does not result in agent "forgetting" its path: setting a positive velocity value will make it continue moving to its destination.
4.3. Placing an agent into the graph
Now that we created an agent and placed it to the environment, let us place it to some specific position in a graph.
This can be done with one of the several overloads of GraphAgent
's jumpTo
method.
These methods immediately place the agent to the specified position:
// The call below places the agent to the nodeA
graphAgent.jumpTo(nodeA);
// The call below places the agent to the position pointing to the nodeA.
// The effect of this call is the same as the effect of the previous call
graphAgent.jumpTo(new GeometricGraphPosition<>(nodeA));
// The call below places the agent on arcAB at the offset of 10 length units from
// its beginning.
graphAgent.jumpTo(arcAB, 10);
// The call below does the same action, but we wrap the jumping destination
// into GeometricGraphPosition instance
graphAgent.jumpTo(new GeometricGraphPosition<>(arcAB, 10));
4.4. Moving an agent inside the graph
Moving agents inside graphs is the most commonly used functionality of the library. The simple code below shows how to make an agent move from one node to another:
graphAgent.jumpTo(nodeA);
graphAgent.moveTo(nodeB, 10);
The first argument of the moveTo
method is destination node, and the second argument is speed in distance units per time units.
We can also send an agent to a position inside the graph:
graphAgent.moveTo(new GeometricGraphPosition<>(arcAB, 50), 10);
The isMoving
method can be used to check if the graph agent is currently moving.
The getVelocity
method returns the speed at which the agent is moving, or zero if the agent is not moving:
graphAgent.moveTo(new GeometricGraphPosition<>(arcAB, 50), 10);
// This will print true as we have just started the movement of the agent
System.out.println(graphAgent.isMoving());
// This will print 10 - the velocity we specified in the moveTo call above
System.out.println(graphAgent.getVelocity());
The agent is sent to its destination via the shortest path that is determined automatically by the graph environment.
By default, the shortest path is just the one having geometrically shortest distance along the graph between source and destination positions.
However, the result of path finding algorithm can be influenced by providing custom weights to GraphEnvironment
class.
4.5. Getting notified about agent’s arrival
In the logic of simulation models it is often important to get notified about agent’s arrival to its destination.
For example, we may need to start loading a truck when it arrives to a warehouse.
To get notifications every time an agent arrives to its destination, we need to override its onDestinationReached
method:
var graphAgent = new GraphAgent<AgentGraphNode, AgentGraphArc>(engine) {
@Override
public void onDestinationReached(GeometricGraphPosition<AgentGraphNode,AgentGraphArc> destPosition) {
System.out.println("Model time is " + time());
System.out.println("The agent has reached its destination and is now at " + getCurrentPosition());
}
};
Below is the full example of getting arrival notification. In the example, we only print the current simulation time and agent’s current position at the moment of arrival:
Engine engine = new Engine();
// We create an agent overriding the onDestinationReached method
var graphAgent = new GraphAgent<AgentGraphNode, AgentGraphArc>(engine) {
@Override
public void onDestinationReached(GeometricGraphPosition<AgentGraphNode,AgentGraphArc> destPosition) {
System.out.println("Model time is " + time());
System.out.println("The agent has reached its destination and is now at " + getCurrentPosition());
}
};
// We place the agent into the environment and ask it to move from nodeA to nodeB
graphAgent.setGraphEnvironment(environment);
graphAgent.jumpTo(nodeA);
graphAgent.moveTo(nodeB, 10);
// We set the engine to the fast mode and ask it to run synchronously
engine.setFastMode(true);
engine.scheduleStop(1_000, "End of simulation");
engine.run(true);
The Engine's documentation contains more comprehensive explanation about how to run simulation models synchronously, like we did here.
4.6. Changing agent’s velocity
Agent’s speed can be changed during its movement.
The speed is measured in distance units per engine’s time units.
For simplicity, the model’s distance units are called pixels, so the speed is expressed in pixels per time units, or px/tu.
The velocity can be set using setVelocity
method:
graphAgent.setVelocity(2);
It is important too keep in mind the following aspects of this method:
-
Negative or infinite velocities cannot be set, trying to set such values will result in an exception
-
Setting zero velocity will result in stopping the agent
-
Setting a positive velocity will result in agent continuing its movement if it had been stopped before
-
Setting velocity to an agent that is not currently moving (can be checked with
isMoving
method) will have no effect
4.7. Getting agent’s path
Sometimes it is necessary to know the graph path used by an agent moving from its source to destination.
For example, we might want to know which nodes and arcs the agent will be moving through.
This can be done using GraphAgent’s getGraphPath
method:
GraphPath<AgentGraphNode, AgentGraphArc> path = graphAgent.getGraphPath();
The path is represented by GraphPath
instance which is a sequence of nodes and arcs such that each pair of subsequent nodes is adjacent.
Path returned by getGraphPath
is a result of the path-finding algorithm performed by GraphEnvironment
inside GraphAgent’s `moveTo
method.
If the agent is not currently moving, null
is returned.
Note that getGraphPath
method returns agent’s entire path from its current source to its current destination.
so, at the moment of call, the agent might have already passed some part of the path.
4.8. Getting agent’s trajectory
Besides agent’s path, it is sometimes useful to know agent’s trajectory.
A trajectory is represented with a Polyline
and always starts at the agent’s current location.
Unlike path, trajectory does not contain information about graph arcs and nodes to be passed by the agent.
Agent’s trajectory can be retrieved by getTrajectory
method:
Polyline trajectory = graphAgent.getTrajectory();
Note that if an agent does not currently have a path, then null
will be returned.
5. Handling agents events
In the previous section, we have already seen how to handle the event of agent’s arrival to its destination. In this section, we will see what other events of both agents and graph elements (i.e., nodes and arcs) can be handled.
5.1. Handling collision of agents
Whenever an agent moves and meets some other agent, its onOtherAgentReached
callback method is called.
This method can be overridden to handle such events:
var graphAgent = new GraphAgent<AgentGraphNode, AgentGraphArc>(engine) {
@Override
public void onOtherAgentReached(GraphAgent<AgentGraphNode, AgentGraphArc> otherAgent, boolean isMovingInSameDirection) {
System.out.println("The agent has reached another agent " + otherAgent);
}
};
onOtherAgentReached
method is not called is the agent is not moving and was just "passively" reached by another agent.
The isMovingInSameDirection
parameter indicates if the other agent is moving in the same direction with the current one.
If isMovingInSameDirection
parameter is false, this means that the other agent is in the reverse position relative to the current agent.
5.2. Handling agents entering and leaving graph nodes
When an agent jumps into a node or enters a node while moving, its afterEnteredNode
is called.
At the moment of calling this method the agent is already at the node.
When an agent leaves a node in the process of movement or as result of jumping, its beforeExitedNode
is called.
At the moment of calling this method the agent is still in the node but will leave it immediately after the callback method is executed.
The code below shows how GraphAgent’s afterEnteredNode
and beforeExitedNode
methods can be overridden to handle the events of entering and leaving nodes:
var graphAgent = new GraphAgent<AgentGraphNode, AgentGraphArc>(engine) {
@Override
public void afterEnteredNode(Graph<AgentGraphNode, AgentGraphArc>.Node node) {
System.out.println("Agent " + this + " has just entered the node " + node);
}
@Override
public void beforeExitedNode(Graph<AgentGraphNode, AgentGraphArc>.Node node) {
System.out.println("Agent " + this + " is about to exit the node " + node);
}
};
5.3. Handling agents entering and leaving graph arcs
Similarly to entering and exiting nodes, the events of agents' entering and exiting arcs can be handled by overriding GraphAgent’s afterEnteredArc
and beforeExitedArc
methods:
var graphAgent = new GraphAgent<AgentGraphNode, AgentGraphArc>(engine) {
@Override
public void afterEnteredArc(Graph<AgentGraphNode, AgentGraphArc>.Arc arc) {
System.out.println("Agent " + this + " has just entered the arc " + arc);
}
@Override
public void beforeExitedArc(Graph<AgentGraphNode, AgentGraphArc>.Arc arc) {
System.out.println("Agent " + this + " is about to leave the arc " + arc);
}
};
Agents can enter and leave arcs in the following cases:
-
agent jumps onto the arc,
-
agent is currently on an arc and jump elsewhere,
-
agent enters or leaves an arc during its movement,
-
agent is on a bi-directional arc and it turns around into the reverse position in the beginning or at the end of movement.
5.4. Handling path not found event
It can happen that an agent is sent to an unreachable position by one of the overloads of moveTo
or moveToClosest
methods.
Such attempt will result in calling GraphAgent’s onPathNotFound
method.
The default implementation of this method throws a runtime exception.
However, this method can be overridden to customize the agent’s behavior in case of not finding a path to any valid destinations it is being sent to:
var graphAgent = new GraphAgent<AgentGraphNode, AgentGraphArc>(engine) {
@Override
public void onPathNotFound(List<GeometricGraphPosition<AgentGraphNode, AgentGraphArc>> possibleDestinationPositions) {
System.out.println("Path to heither of possible destinations is found for the agent " + this);
}
};
6. Paths and trajectories
6.1. What is a graph path
Path is a sequence of nodes and arcs such that each pair of subsequent nodes is adjacent.
Path is a result of all path-finding algorithms, such as Algorithm.getShortestPath
.
Path is not a subgraph: the same graph node or arc can be included into path several times (e.g. if path contains cycles).
Graph path always starts and ends at a node. In a non-empty path, the number of arcs always equals the number of nodes minus one. Path can be empty (e.g. to represent a non-existent path) or consist of only one node (e.g. path from some node to itself).
Paths are represented with GraphPath
class.
The code below illustrates the use of its constructors and basic methods:
// We create an empty path, i.e. a path with no nodes and arcs
GraphPath<AgentGraphNode, AgentGraphArc> emptyPath = new GraphPath<>();
// The code below will print true, as the path is empty
System.out.println(emptyPath.isEmpty());
// We create a path consisting of a single node
var pathOfOneNode = new GraphPath<>(nodeA);
// The code below will print a list containing a single node
System.out.println(pathOfOneNode);
// We create a path with a single arc, and, correspondingly, the two nodes
var path = new GraphPath<>(arcAB);
// The code below will print a list containing two nodes
System.out.println(path.getNodes());
// The code below will print a list containing one arc
System.out.println(path.getArcs());
6.2. What is an agent graph path
AgentGraphPath
is a class representing a path of a GraphAgent
from some GeometricGraphPosition
to some other, probably equal or adjacent, GeometricGraphPosition
.
AgentGraphPath
is a subclass of GraphPath
class, so it can either be empty, or consist of a single node, or, most commonly, be a sequence of nodes and arcs such that each pair of subsequent nodes is adjacent.
As movement of an agent does not necessarily start at a node, AgentGraphPath
contains a reference to the actual source position which can point either to a node or to an arc.
getActualSourcePosition
method of AgentGraphPath
class can be used to get such position.
If getActualSourcePosition
points to a node, this is always the first node of the path, and agent’s movement actually starts from this node.
If getActualSourcePosition
points to an arc, this is always the first arc of the path, and agent’s movement actually starts from the corresponding position on this arc.
Symmetrically, getActualDestPosition
method of AgentGraphPath
class contains a reference to a destination position that is the terminal point of agent’s movement.
getWeight
method of AgentGraphPath
class returns the weight of the path calculated at the moment of path finding.
Current path of a GraphAgent
can be retrieved by its getGraphPath
method.
This method will return agent’s current path, or null of the agent currently does not have one.
6.3. Turning around at the beginning and end of movement
If agent’s movement starts or finishes on a bi-directional arc, an agent might need to turn around in the beginning and/or at the end of its movement.
This fact is also reflected in AgentGraphPath
class.
Its requiresTurnAtSource
method checks if the movement requires the agent to turn around at the very beginning of the movement.
Symmetrically, requiresTurnAtDest
method checks whether turning around is required at the destination position of the movement.
getSourcePosition
method returns the position after a possible turning around in the beginning of the movement, from which an agent actually starts moving.
The position returned by getSourcePosition
method can be either equal to or a reverse of the position returned by getActualSourcePosition
method.
If these positions are equal, turning around is not required in the beginning of the movement, otherwise it is required.
getDestPosition
method returns the position before a possible turning around at the end of the movement, where an agent actually completes its movement.
The position returned by getDestPosition
method can be either equal to or a reverse of the position returned by getActualDestPosition
method.
If these positions are equal, turning around is not required at the end of the movement, otherwise it is required.
6.4. Trajectory
Trajectory is a Polyline
representing the movement track of an agent in the 2D-space.
Trajectory does not have any references to nodes and arcs that agent will be moving through.
It can consist of a single point, however most commonly it consists of several points and segments.
If an agent at some point in time has a graph path, i.e. its getGraphPath
method does not return null, it also has a trajectory.
Agent’s current trajectory can be retrieved by calling its getTrajectory
method.
This method will return the remaining trajectory of this agent, i.e. the Polyline
ahead of this agent that it will travel according to its current path.
Returned polyline includes the segment the agent is currently moving on, and all segments ahead of this agent that it will move through before it reaches its destination.
AgentGraphPath
also has a trajectory which can be retrieved by its getTrajectory
method.
This trajectory is a Polyline
that is a representation of movement track of an agent along this path.
It is important to make difference between agent’s current trajectory and the trajectory of agent’s current path:
-
agent’s current trajectory does not include the segments that the agent has already moved through, whereas
-
trajectory of agent’s current path is a full trajectory of the agent’s current movement.
7. Path finding
7.1. Defining custom weights for arcs
The example below shows the use of overriding getArcWeight
method of GraphEnvironment
class to prevent the agents from moving through the arcs that contain the word "Disabled" in their names:
class MyArc extends AgentGraphArcImpl {
private String name;
public MyArc(String name, Polyline polyline) {
super(polyline);
this.name = name;
}
public String getName() {
return name;
}
}
var environment = new GraphEnvironment<AgentGraphNode, MyArc, GraphAgent<?, ?>>() {
@Override
public double getArcWeight(MyArc arcValue, GraphAgent<?, ?> weightKey) {
// If the length of the arc is less than 10, its weight will be 10 anyway
if(arcValue.getName().contains("Disabled")) {
return Double.POSITIVE_INFINITY;
} else {
return super.getArcWeight(arcValue, weightKey);
}
}
};
It is important to node that the arcs with infinite weights in the code above may still be included into agents' paths if there is no other paths between the specified source and destination.
To prevent the agents from moving over the disabled arcs,
we can check the value returned by the AgentGraphPath.getWeight
method and add the special handling logic for the case when the weight is infinite.
Sometimes it is required to close (or de-prioritize) not the whole arcs, but just some fragments of arcs.
To do so, we can override getPartialArcWeight
of GraphEnvironment
class.
The example below demonstrates one of possible approaches of implementing this.
In this example, we define MyArc type that contains the beginning and end of the disabled arc segment.
In the overridden getArcWeight
method we return infinity if
In the overridden getPartialArcWeight
method we return the infinity only if
class MyArc extends AgentGraphArcImpl {
private double disabledFromAbsOffset;
private double disabledToAbsOffset;
public MyArc( Polyline polyline,
double disabledFromAbsOffset,
double disabledToAbsOffset) {
super(polyline);
this.disabledFromAbsOffset = disabledFromAbsOffset;
this.disabledToAbsOffset = disabledToAbsOffset;
}
public boolean isFragmentDisabled(double startAbsOffset, double endAbsOffset) {
return startAbsOffset < disabledToAbsOffset
&& endAbsOffset > disabledFromAbsOffset;
}
public boolean isTotallyDisabled() {
return disabledToAbsOffset > disabledFromAbsOffset;
}
}
var environment = new GraphEnvironment<AgentGraphNode, MyArc, GraphAgent<?, ?>>() {
@Override
public double getArcWeight(MyArc arcValue, GraphAgent<?,?> weightKey) {
// We return infinity if the arc is totally disabled
return arcValue.isTotallyDisabled() ? Double.POSITIVE_INFINITY
: super.getArcWeight(arcValue, weightKey);
}
public double getPartialArcWeight( MyArc arcValue,
double startAbsOffset,
double endAbsOffset,
double startRelOffset,
double endRelOffset,
GraphAgent<?,?> weightKey) {
// We return infinity if the specified fragment of arc is disabled
return arcValue.isFragmentDisabled(startAbsOffset, endAbsOffset)
? Double.POSITIVE_INFINITY
: super.getPartialArcWeight( arcValue,
startAbsOffset,
endAbsOffset,
startRelOffset,
endRelOffset,
weightKey);
};
};
7.2. Defining custom weights for nodes
var environment = new GraphEnvironment<AgentGraphNode, AgentGraphArc, GraphAgent<?, ?>>() {
public double getNodeWeight(AgentGraphNode nodeValue, com.amalgamasimulation.graphagent.GraphAgent<?,?> weightKey) {
// Every node will add 1 weight unit to every path
return 1;
};
};