Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Walk on predefined edge sequences #274

Merged
merged 9 commits into from
Sep 28, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,3 +45,36 @@ Run it like:
```bash
java -jar graphwalker-studio/target/graphwalker-studio-<VERSION>.jar
```

===================

Predefined Path
===================
This fork of the GraphWalker project enables the user to define an edge sequence in the input graph, along which the machine should execute.

## Graph input format

Currently a predefined path can only be specified in JSON GW3 input graphs.

To define an edge sequence, the model has to contain an array element called *predefinedPathEdgeIds* containing the edge IDs in the sequence.

The generator and stop condition has to be specified in the *generator* element.

Example:

```JSON
{
"models": [
{
"generator": "predefined_path(predefined_path)",
...
"edges": [
{ "id": "e0", ... },
{ "id": "e1", ... },
{ "id": "e2", ... }
],
"predefinedPathEdgeIds": [ "e0", "e1", "e2", "e0" ]
}
]
}
```
3 changes: 3 additions & 0 deletions graphwalker-core/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,7 @@ Are used by path generators. The [algorithm] provides the generators the logic f

### Stop conditions
Used by path generators to determine when to stop generating a path. Stop conditions can be logically AND'ed and OR'ed. When a stop condition (or a combination of several) evaluates to true, the generator stops. Examples are:

* **Edge coverage** - The condition is a percentage number. When, during execution, the percentage of traversed edges are reached, the test is stopped. If an edge is traversed more than one time, it still counts as 1, when calculating the percentage coverage.
* **Length** - The condition is a number, representing the total numbers of pairs of vertices and edges generated by a generator. For example, if the number is 110, the test sequence would be 220 lines long. (including 110 pairs of edges and vertices).
* **Never** - This special condition will never halt the generator.
Expand All @@ -112,6 +113,7 @@ Used by path generators to determine when to stop generating a path. Stop condit
* **Requirement coverage** - The condition is a percentage number. When, during execution, the percentage of traversed requirements is reached, the test is stopped. If an requirement is traversed more than one time, it still counts as 1, when calculating the percentage coverage.
* **Time duration** - The condition is a time, representing the number of seconds that the test generator is allowed to execute.
* **Vertex coverage** - The condition is a percentage number. When, during execution, the percentage of traversed vertices are reached, the test is stopped. If a vertex is traversed more than one time, it still counts as 1, when calculating the percentage coverage.
* **PredefinedPath** - The condition is the end of the predefined edge sequence. When, during execution, the last edge and its target vertex is reached, the test is stopped. The stop condition only works when a predefined path is present in the model.


### Events
Expand All @@ -131,6 +133,7 @@ A generator used an algorithm to decide how to traverse a model. Different gener

The algorithm works well an very large models, and generates reasonably short sequences. The downside is when used in conjunction with EFSM, the algorithm can choose a path which is blocked by a guard.
* **RandomPath** - Navigate through the model in a completely random manor. Also called "Drunkard’s walk", or "Random walk". This algorithm selects an out-edge from a vertex by random, and repeats the process in the next vertex.
* **PredefinedPath** - Navigate through the model deterministically along a predefined edge sequence. This generator only works with its corresponding PredefinedPath stop condition.


### Machines
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package org.graphwalker.core.condition;

import org.graphwalker.core.model.Edge;
import org.graphwalker.core.model.Vertex;

public class PredefinedPathStopCondition extends StopConditionBase {

public PredefinedPathStopCondition() {
super("PredefinedPath");
}

@Override
public boolean isFulfilled() {
return getCurrentStepCount() == getTotalStepCount();
}

@Override
public double getFulfilment() {
return (double) getCurrentStepCount() / getTotalStepCount();
}

private int getCurrentStepCount() {
// *2 because each index increment corresponds to a vertex-edge step pair
int currentStepCount = getContext().getPredefinedPathCurrentEdgeIndex() * 2;
if (getContext().getCurrentElement() instanceof Vertex.RuntimeVertex) {
return currentStepCount + 1;
}
return currentStepCount;
}

private int getTotalStepCount() {
return (getContext().getModel().getPredefinedPath().size() * 2) + 1;
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
package org.graphwalker.core.generator;

import org.graphwalker.core.condition.PredefinedPathStopCondition;
import org.graphwalker.core.condition.StopCondition;
import org.graphwalker.core.condition.StopConditionException;
import org.graphwalker.core.machine.Context;
import org.graphwalker.core.model.Edge;
import org.graphwalker.core.model.Element;
import org.graphwalker.core.model.Vertex;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.List;

public class PredefinedPath extends PathGeneratorBase<StopCondition> {

private static final Logger LOG = LoggerFactory.getLogger(PredefinedPath.class);

public PredefinedPath(StopCondition stopCondition) {
if (!(stopCondition instanceof PredefinedPathStopCondition)) {
throw new StopConditionException("PredefinedPath generator can only work with a PredefinedPathStopCondition instance");
}
setStopCondition(stopCondition);
}

@Override
public Context getNextStep() {
Context context = super.getNextStep();
Element currentElement = context.getCurrentElement();
List<Element> elements = context.filter(context.getModel().getElements(currentElement));
if (elements.isEmpty()) {
LOG.error("currentElement: " + currentElement);
LOG.error("context.getModel().getElements(): " + context.getModel().getElements());
throw new NoPathFoundException(context.getCurrentElement());
}
Element nextElement;
if (currentElement instanceof Edge.RuntimeEdge) {
nextElement = getNextElementFromEdge(context, elements, (Edge.RuntimeEdge) currentElement);
} else if (currentElement instanceof Vertex.RuntimeVertex) {
nextElement = getNextElementFromVertex(context, elements, (Vertex.RuntimeVertex) currentElement);
context.setPredefinedPathCurrentElementIndex(context.getPredefinedPathCurrentEdgeIndex() + 1);
} else {
LOG.error("Current element is neither an edge or a vertex");
throw new NoPathFoundException(context.getCurrentElement());
}
context.setCurrentElement(nextElement);
return context;
}

private Element getNextElementFromEdge(Context context, List<Element> reachableElements, Edge.RuntimeEdge currentElement) {
if (reachableElements.size() != 1) {
LOG.error("Next vertex of predefined path is ambiguous (after step " + context.getPredefinedPathCurrentEdgeIndex() + ", from edge with id \"" + currentElement.getId() + "\")");
throw new NoPathFoundException(currentElement);
}
return reachableElements.get(0);
}

private Element getNextElementFromVertex(Context context, List<Element> reachableElements, Vertex.RuntimeVertex currentElement) {
Element nextElement = context.getModel().getPredefinedPath().get(context.getPredefinedPathCurrentEdgeIndex());
if (!reachableElements.contains(nextElement)) {
LOG.error("Next edge with id \"" + nextElement.getId() + "\" from predefined path is unreachable (either the guarding condition was not met or the edge has a different source vertex.");
throw new NoPathFoundException(currentElement);
}
return nextElement;
}

@Override
public boolean hasNextStep() {
return !getStopCondition().isFulfilled();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,10 @@ public interface Context {

Context setNextElement(Element nextElement);

Integer getPredefinedPathCurrentEdgeIndex();

Context setPredefinedPathCurrentElementIndex(Integer predefinedPathCurrentElementIndex);

List<Requirement> getRequirements();

List<Requirement> getRequirements(RequirementStatus status);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ public abstract class ExecutionContext implements Context {
private Element currentElement;
private Element nextElement;
private Element lastElement;
private Integer predefinedPathCurrentEdgeIndex;

private String REGEXP_GLOBAL = "global\\.";

Expand All @@ -80,6 +81,7 @@ public abstract class ExecutionContext implements Context {
public ExecutionContext() {
executionEnvironment = org.graalvm.polyglot.Context.newBuilder().allowAllAccess(true).build();
executionEnvironment.getBindings("js").putMember(getClass().getSimpleName(), this);
predefinedPathCurrentEdgeIndex = 0;
}

public ExecutionContext(Model model, PathGenerator pathGenerator) {
Expand Down Expand Up @@ -179,6 +181,15 @@ public Context setNextElement(Element nextElement) {
return this;
}

public Integer getPredefinedPathCurrentEdgeIndex() {
return predefinedPathCurrentEdgeIndex;
}

public Context setPredefinedPathCurrentElementIndex(Integer predefinedPathCurrentElementIndex) {
this.predefinedPathCurrentEdgeIndex = predefinedPathCurrentElementIndex;
return this;
}

public Context setRequirementStatus(Requirement requirement, RequirementStatus requirementStatus) {
requirements.put(requirement, requirementStatus);
return this;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,10 @@
*/

import org.graphwalker.core.common.Objects;
import org.graphwalker.core.machine.MachineException;

import java.util.*;
import java.util.stream.Collectors;

import static org.graphwalker.core.common.Objects.*;
import static org.graphwalker.core.model.Edge.RuntimeEdge;
Expand All @@ -54,6 +56,7 @@ public class Model extends BuilderBase<Model, Model.RuntimeModel> {
private List<Vertex> vertices = new ArrayList<>();
private List<Edge> edges = new ArrayList<>();
private List<Action> actions = new ArrayList<>();
private List<Edge> predefinedPath = new ArrayList<>();

/**
* Create a new Model
Expand Down Expand Up @@ -97,6 +100,19 @@ public Model(RuntimeModel model) {
edge.setProperties(runtimeEdge.getProperties());
this.edges.add(edge);
}
for (RuntimeEdge runtimeEdge : model.getPredefinedPath()) {
Edge edge = new Edge();
edge.setId(runtimeEdge.getId());
edge.setName(runtimeEdge.getName());
edge.setSourceVertex(cache.get(runtimeEdge.getSourceVertex()));
edge.setTargetVertex(cache.get(runtimeEdge.getTargetVertex()));
edge.setGuard(runtimeEdge.getGuard());
edge.setActions(runtimeEdge.getActions());
edge.setRequirements(runtimeEdge.getRequirements());
edge.setWeight(runtimeEdge.getWeight());
edge.setProperties(runtimeEdge.getProperties());
this.predefinedPath.add(edge);
}
}

/**
Expand Down Expand Up @@ -141,6 +157,9 @@ public Model addEdge(Edge edge) {
* @return The model.
*/
public Model deleteEdge(Edge edge) {
if (isNotNull(predefinedPath) && predefinedPath.contains(edge)) {
throw new RuntimeException("Cannot remove edge contained in predefined path");
}
edges.remove(edge);
return this;
}
Expand All @@ -156,6 +175,10 @@ public Model deleteEdge(Edge edge) {
* @return The model.
*/
public Model deleteVertex(Vertex vertex) {
if (isNotNull(predefinedPath)
&& predefinedPath.stream().anyMatch(edge -> vertex.equals(edge.getSourceVertex()) || vertex.equals(edge.getTargetVertex()))) {
throw new RuntimeException("Cannot remove vertex with connecting edge contained in predefined path");
}
edges.removeIf(edge -> vertex.equals(edge.getSourceVertex()) || vertex.equals(edge.getTargetVertex()));
vertices.remove(vertex);
return this;
Expand Down Expand Up @@ -224,6 +247,23 @@ public List<Edge> getEdges() {
return edges;
}

public Model setPredefinedPath(List<Edge> predefinedPath) {
this.predefinedPath = predefinedPath.stream()
.map(predefinedPathEdge -> getEdges().stream()
.filter(localEdge -> localEdge.getId().equals(predefinedPathEdge.getId()))
.findFirst().orElseThrow(() -> new RuntimeException("Not all edges from predefined path exist in the model")))
.collect(Collectors.toList());
return this;
}

public List<Edge> getPredefinedPath() {
return predefinedPath;
}

public boolean hasPredefinedPath() {
return isNotNullOrEmpty(predefinedPath);
}

/**
* Creates an immutable model from this model.
*
Expand Down Expand Up @@ -253,6 +293,7 @@ public static class RuntimeModel extends RuntimeBase {
private final Map<RuntimeVertex, List<RuntimeEdge>> inEdgesByVertexCache;
private final Map<RuntimeVertex, List<RuntimeEdge>> outEdgesByVertexCache;
private final Map<String, List<RuntimeVertex>> sharedStateCache;
private final List<RuntimeEdge> predefinedPath;

private RuntimeModel(Model model) {
super(model.getId(), model.getName(), model.getActions(), model.getRequirements(), model.getProperties());
Expand All @@ -266,6 +307,11 @@ private RuntimeModel(Model model) {
this.elementsByNameCache = createElementsByNameCache();
this.elementsByElementCache = createElementsByElementCache(elementsCache, outEdgesByVertexCache);
this.sharedStateCache = createSharedStateCache();
this.predefinedPath = model.getPredefinedPath().stream()
.map(predefinedPathEdge -> this.edges.stream()
.filter(localEdge -> localEdge.getId().equals(predefinedPathEdge.getId()))
.findFirst().orElseThrow(() -> new RuntimeException("Not all edges from predefined path exist in the model")))
.collect(Collectors.toList());
}

/**
Expand Down Expand Up @@ -379,6 +425,14 @@ public List<RuntimeEdge> findEdges(String name) {
return edgesByNameCache.get(name);
}

public List<RuntimeEdge> getPredefinedPath() {
return predefinedPath;
}

public boolean hasPredefinedPath() {
return isNotNullOrEmpty(predefinedPath);
}

/**
* Searches the model for any element matching the search string.
* </p>
Expand Down
Loading