Skip to content
Draft
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
package com.cleanroommc.kirino.ecs.system.exegraph.scheduler;

import com.cleanroommc.kirino.ecs.component.ComponentRegistry;
import com.cleanroommc.kirino.ecs.system.CleanSystem;
import com.cleanroommc.kirino.ecs.system.exegraph.SystemExeFlowGraph;
import com.cleanroommc.kirino.engine.resource.ResourceLayout;
import com.cleanroommc.kirino.schemata.graph.Hypergraph;
import com.cleanroommc.kirino.utils.GraphUtils;
import com.google.common.base.Preconditions;
import com.google.common.graph.Graph;
import it.unimi.dsi.fastutil.PriorityQueue;
import it.unimi.dsi.fastutil.objects.Object2IntArrayMap;
import it.unimi.dsi.fastutil.objects.ObjectHeapPriorityQueue;
import org.jspecify.annotations.NonNull;

import java.lang.reflect.Field;
import java.util.Map;
import java.util.Optional;

import static com.cleanroommc.kirino.ecs.system.exegraph.SystemExeFlowGraph.Builder;


public final class SystemScheduler {

private final Hypergraph<Vertex, Edge> graph;
private final ComponentRegistry componentRegistry;
private final int resourceCount;

public SystemScheduler(ComponentRegistry componentRegistry,
ResourceLayout layout) {
Preconditions.checkNotNull(componentRegistry);
Preconditions.checkNotNull(layout);

this.componentRegistry = componentRegistry;

//<editor-fold desc="Get Resource Count">
try {
final Class<ResourceLayout> resourceLayoutClass = ResourceLayout.class;
final Field nextIDField = resourceLayoutClass.getDeclaredField("nextId");
resourceCount = nextIDField.getInt(layout);
} catch (NoSuchFieldException | IllegalAccessException e) {
throw new RuntimeException(e);
}
//</editor-fold>

graph = new Hypergraph<>();
}

/**
* Add a system-component relation. If the system or component are not present,
* they will be added ot the hypergraph.
* @param system the system.
* @param priority the priority of the system, determines which system will run first.
* @param componentDependency the component the system depends on.
* @see Hypergraph#add(Object, Object)
*/
public void add(@NonNull CleanSystem system, int priority, @NonNull String componentDependency) {
Preconditions.checkNotNull(system);
Preconditions.checkState(priority >= 0);
Preconditions.checkNotNull(componentDependency);
Preconditions.checkArgument(componentRegistry.componentExists(componentDependency));

graph.add(new Edge(componentDependency), new Vertex(system, priority));
}

/**
* Add a system-resource relation. If the system or resource are not present,
* they will be added ot the hypergraph.
* @param system the system
* @param priority the priority of the system, determines which system will run first.
* @param resourceDependency the resource the system depends on.
* @see Hypergraph#add(Object, Object)
*/
public void add(@NonNull CleanSystem system, int priority, int resourceDependency) {
Preconditions.checkNotNull(system);
Preconditions.checkState(priority >= 0);
Preconditions.checkPositionIndex(resourceDependency, resourceCount);

graph.add(new Edge(resourceDependency), new Vertex(system, priority));
}

public <TFlowGraph extends SystemExeFlowGraph> Builder<TFlowGraph> scheduleSystemExecution(
Builder<TFlowGraph> builder, String... stageNames) {
final int colors = Runtime.getRuntime().availableProcessors();
Graph<Vertex> disjointed = this.graph.buildDisjointedGraph();
Optional<Map<Vertex, Integer>> colored = GraphUtils.colorGraph(disjointed, colors);
if (colored.isPresent()) {
PriorityQueue<Vertex>[] colorQueues = new PriorityQueue[colors];
for (int i = 0; i < colors; i++) {
colorQueues[i] = new ObjectHeapPriorityQueue<Vertex>();
}
for (Map.Entry<Vertex, Integer> entry : colored.get().entrySet()) {
colorQueues[entry.getValue()].enqueue(entry.getKey());
}
for (int i = -1; i < stageNames.length; i++) {
for (PriorityQueue<Vertex> colorQueue : colorQueues) {
if (!colorQueue.isEmpty()) {
Vertex vertex = colorQueue.dequeue();
String from = i != -1 ? stageNames[i] : SystemExeFlowGraph.START_NODE;
String to = i != stageNames.length - 1 ? stageNames[i + 1] : SystemExeFlowGraph.END_NODE;
builder.addTransition(vertex.system, from, to);
}
}
}
}

return builder;
}

//<editor-fold desc="vertices">
private record Vertex(CleanSystem system, int priority) implements Comparable<Vertex> {

@Override
public int compareTo(@NonNull Vertex o) {
Preconditions.checkNotNull(system);

return priority - o.priority;
}
}
//</editor-fold>
//<editor-fold desc="edges">
private enum EdgeType {
COMPONENT,
RESOURCE
}

private record Edge(EdgeType type, Object id) {

public Edge(int id) {
this(EdgeType.RESOURCE, id);
}

public Edge(String name) {
this(EdgeType.COMPONENT, name);
}
}
//</editor-fold>
}
116 changes: 116 additions & 0 deletions src/main/java/com/cleanroommc/kirino/schemata/graph/Hypergraph.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
package com.cleanroommc.kirino.schemata.graph;

import com.google.common.base.Preconditions;
import com.google.common.collect.HashMultimap;
import com.google.common.collect.Multimap;
import com.google.common.graph.*;
import it.unimi.dsi.fastutil.objects.ReferenceArraySet;
import org.jspecify.annotations.NonNull;

import java.util.Set;

/**
* Represents a <a href="https://en.wikipedia.org/wiki/Hypergraph">hypergraph</a>, a mathematical structure representing an
* association of many vertices to many edges.
* @param <V> Vertex Type
* @param <E> Edge Type
* @implNote Currently uses {@link Multimap Multimaps} for implementation.
* Can probably be sped up with a better data structure, this is a draft.
* This class does not implement all functions related to graph like graph degree
* or graph coloring because YAGNI.
*/
public class Hypergraph<V,E> {
private final Multimap<E,V> verticesPerEdge;
private final Multimap<V,E> edgesPerVertex;

/**
* Creates the hypergraph.
*/
public Hypergraph() {
verticesPerEdge = HashMultimap.create();
edgesPerVertex = HashMultimap.create();
}

/**
* Adds a vertex to an edge in the hypergraph, if the edge does not exist, it is created.
* @param edge the edge
* @param vertex the vertex
*/
public void add(@NonNull E edge, @NonNull V vertex) {
Preconditions.checkNotNull(edge);
Preconditions.checkNotNull(vertex);

edgesPerVertex.put(vertex, edge);
verticesPerEdge.put(edge, vertex);
}

public void addVertexDependency(@NonNull V dependency, @NonNull V dependent) {
Preconditions.checkNotNull(dependency);
Preconditions.checkNotNull(dependent);
}

/**
* Removes an association from the hypergraph, if there are no more associations between the edge/vertex they are deleted.
* @param edge the edge
* @param vertex the vertex
*/
public void remove(@NonNull E edge, @NonNull V vertex) {
Preconditions.checkNotNull(edge);
Preconditions.checkNotNull(vertex);

edgesPerVertex.remove(vertex, edge);
verticesPerEdge.remove(edge, vertex);
}

/**
* Gets all the vertices that share an edge with the vertex.
* @param vertex the vertex
* @return A set of all the vertices that share an edge with the vertex
*/
@NonNull
public Set<V> getNeighbours(@NonNull V vertex) {
Preconditions.checkNotNull(vertex);

ReferenceArraySet<V> neighbours = new ReferenceArraySet<>();

for (E edge : this.edgesPerVertex.get(vertex)) {
for (V neighbour : this.verticesPerEdge.get(edge)) {
if (!neighbour.equals(vertex)) {
neighbours.add(neighbour);
}
}
}

return neighbours;
}

/**
* Squashed the hypergraph into a graph, strips edge metadata, then inverts it.
* The resulting graph contains all the vertices of the hypergraph,
* connected to vertices, that share no edges.
* @return Inverted squashed graph.
* @apiNote Uses classes and methods marked with {@link com.google.common.annotations.Beta @Beta}
*/
@NonNull
public Graph<V> buildDisjointedGraph() {
ImmutableGraph.Builder<V> builder = GraphBuilder.undirected()
.allowsSelfLoops(false)
.expectedNodeCount(verticesPerEdge.keySet().size())
.immutable();

Set<V> vertices = edgesPerVertex.keySet();

for (V vertex : vertices) {
builder.addNode(vertex);
Set<V> neighbours = getNeighbours(vertex);
for (V tmp : vertices) {
if (!tmp.equals(vertex) && !neighbours.contains(tmp)) {
builder.addNode(tmp);
builder.putEdge(vertex, tmp);
}
}
}

return builder.build();
}
}
98 changes: 98 additions & 0 deletions src/main/java/com/cleanroommc/kirino/utils/GraphUtils.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
package com.cleanroommc.kirino.utils;

import com.google.common.base.Preconditions;
import com.google.common.collect.HashMultimap;
import com.google.common.collect.Multimap;
import com.google.common.graph.Graph;
import it.unimi.dsi.fastutil.objects.Reference2IntArrayMap;
import org.jspecify.annotations.NonNull;

import java.util.*;

/**
* Utilities regarding graphs unavailable in {@link Graph guava graph classes}.
*/
public final class GraphUtils {

/**
* DSatur algorithm for graph coloring.
* @param graph The {@link Graph graph}
* @param availableColors Available Colors
* @return An {@link Optional} of a {@link Map} of vertices to colors,
* if the graph is empty, then the value is empty as well.
* @param <V> Type of graph vertices.
*/
public static <V> Optional<Map<V, Integer>> colorGraph(@NonNull Graph<V> graph, int availableColors) { // TODO: throw an error or return Optional.empty() if availableColors is lower than the chromatic number of the graph
Preconditions.checkNotNull(graph);

if (graph.nodes().isEmpty() || graph.edges().isEmpty()) {
return Optional.empty();
}

record VertexInfo<V>(int saturation, int degree, V vertex) implements Comparable<VertexInfo<V>> {
@Override
public int compareTo(@NonNull VertexInfo<V> o) {
Preconditions.checkNotNull(o);
if (this.saturation != o.saturation) {
return saturation - o.saturation;
} else if (degree != o.degree) {
return degree - o.degree;
} else {
return vertex.hashCode() - o.vertex.hashCode();
}
}
}

BitSet usedColors = new BitSet(availableColors);
V currVertex;
int currColor;
Map<V, Integer> colors = new Reference2IntArrayMap<>();
Map<V, Integer> degrees = new Reference2IntArrayMap<>();
Multimap<V, Integer> adjColors = HashMultimap.create();
PriorityQueue<VertexInfo<V>> verticesToColor = new PriorityQueue<>(); // TODO: Replace with Fibonacci Heap

for (V v : graph.nodes()) {
colors.put(v, -1);
degrees.put(v, graph.degree(v));
verticesToColor.add(new VertexInfo<>(0, degrees.get(v), v));
}

while (!verticesToColor.isEmpty()) {
VertexInfo<V> info = verticesToColor.poll();
currVertex = info.vertex;
Set<V> adj = graph.adjacentNodes(currVertex);
// Set all unavailable colors.
for (V v : adj) {
if (colors.get(v) != -1) {
usedColors.set(colors.get(v));
}
}
// Find first availableColor
for (currColor = 0; currColor < availableColors; currColor++) {
if (!usedColors.get(currColor)) {
break;
}
}
// Reset color filter
for (V v : adj) {
if (colors.get(v) != -1) {
usedColors.set(colors.get(v), false);
}
}
colors.put(currVertex, currColor); // Set color
// Push adjacent vertices to coloring queue
for (V v : adj) {
if (colors.get(v) == -1) {
verticesToColor.remove(new VertexInfo<>(adjColors.get(v).size(),
degrees.get(v), v));
adjColors.put(v, currColor);
degrees.compute(v, (ignored, val) -> val != null ? --val : graph.degree(v)-1);
verticesToColor.add(new VertexInfo<>(adjColors.get(v).size(),
degrees.get(v), v));
}
}
}

return Optional.of(colors);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package com.cleanroommc.test.kirino.graph;

import com.cleanroommc.kirino.utils.GraphUtils;
import com.google.common.graph.GraphBuilder;
import com.google.common.graph.MutableGraph;
import org.junit.jupiter.api.Test;

import java.util.Map;
import java.util.Optional;

import static org.junit.jupiter.api.Assertions.assertNotEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;

public class GraphColoringTest {
@Test
public void testGraphColoring() {
MutableGraph<Integer> graph = GraphBuilder.undirected().allowsSelfLoops(false).build();
graph.putEdge(0, 1);
graph.putEdge(1, 2);
graph.putEdge(2, 3);
graph.putEdge(3, 4);
graph.putEdge(4, 5);
graph.putEdge(5, 6);
graph.putEdge(6, 0);
graph.putEdge(0, 2);
graph.putEdge(1, 3);
graph.putEdge(2, 4);
graph.putEdge(3, 5);
graph.putEdge(4, 6);
graph.putEdge(5, 0);
graph.putEdge(6, 1);
graph.putEdge(0, 3);
graph.putEdge(1, 4);
graph.putEdge(2, 5);
Optional<Map<Integer, Integer>> colored = GraphUtils.colorGraph(graph, 3);
assertTrue(colored.isPresent());
Map<Integer, Integer> color = colored.get();
for (var colorEntry : color.entrySet()) {
for (Integer vertex : graph.adjacentNodes(colorEntry.getKey())) {
assertNotEquals(colorEntry.getValue(), color.get(vertex));
}
}
}
}
Loading