Skip to content
Merged
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
import java.util.Comparator;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.HashSet;
import java.util.IdentityHashMap;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
Expand Down Expand Up @@ -1287,7 +1288,7 @@ public List<ShadowVariableDescriptor<Solution_>> getAllShadowVariableDescriptors
}

public List<DeclarativeShadowVariableDescriptor<Solution_>> getDeclarativeShadowVariableDescriptors() {
var out = new ArrayList<DeclarativeShadowVariableDescriptor<Solution_>>();
var out = new HashSet<DeclarativeShadowVariableDescriptor<Solution_>>();
for (var entityDescriptor : entityDescriptorMap.values()) {
entityDescriptor.getShadowVariableDescriptors();
for (var shadowVariableDescriptor : entityDescriptor.getShadowVariableDescriptors()) {
Expand All @@ -1296,7 +1297,7 @@ public List<DeclarativeShadowVariableDescriptor<Solution_>> getDeclarativeShadow
}
}
}
return out;
return new ArrayList<>(out);
}

public ProblemSizeStatistics getProblemSizeStatistics(Solution_ solution) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,7 @@ public void updateShadowVariables(Class<Solution_> solutionClass,
}

private record InternalShadowVariableSession<Solution_>(SolutionDescriptor<Solution_> solutionDescriptor,
VariableReferenceGraph<Solution_> graph) {
VariableReferenceGraph graph) {

public static <Solution_> InternalShadowVariableSession<Solution_> build(
SolutionDescriptor<Solution_> solutionDescriptor, VariableReferenceGraphBuilder<Solution_> graph,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
package ai.timefold.solver.core.impl.domain.variable.declarative;

import java.util.ArrayList;
import java.util.Collections;
import java.util.IdentityHashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.function.BiConsumer;
import java.util.function.IntFunction;
import java.util.stream.Collectors;

import ai.timefold.solver.core.impl.util.DynamicIntArray;
import ai.timefold.solver.core.preview.api.domain.metamodel.VariableMetaModel;

import org.jspecify.annotations.NonNull;
import org.jspecify.annotations.Nullable;

public abstract sealed class AbstractVariableReferenceGraph<Solution_, ChangeSet_> implements VariableReferenceGraph
permits DefaultVariableReferenceGraph, FixedVariableReferenceGraph {
// These structures are immutable.
protected final List<EntityVariablePair<Solution_>> instanceList;
protected final Map<VariableMetaModel<?, ?, ?>, Map<Object, EntityVariablePair<Solution_>>> variableReferenceToInstanceMap;
protected final Map<VariableMetaModel<?, ?, ?>, List<BiConsumer<AbstractVariableReferenceGraph<Solution_, ?>, Object>>> variableReferenceToBeforeProcessor;
protected final Map<VariableMetaModel<?, ?, ?>, List<BiConsumer<AbstractVariableReferenceGraph<Solution_, ?>, Object>>> variableReferenceToAfterProcessor;

// These structures are mutable.
protected final DynamicIntArray[] edgeCount;
protected final ChangeSet_ changeSet;
protected final TopologicalOrderGraph graph;

AbstractVariableReferenceGraph(VariableReferenceGraphBuilder<Solution_> outerGraph,
IntFunction<TopologicalOrderGraph> graphCreator) {
instanceList = List.copyOf(outerGraph.instanceList);
var instanceCount = instanceList.size();
// Often the maps are a singleton; we improve performance by actually making it so.
variableReferenceToInstanceMap = mapOfMapsDeepCopyOf(outerGraph.variableReferenceToInstanceMap);
variableReferenceToBeforeProcessor = mapOfListsDeepCopyOf(outerGraph.variableReferenceToBeforeProcessor);
variableReferenceToAfterProcessor = mapOfListsDeepCopyOf(outerGraph.variableReferenceToAfterProcessor);
edgeCount = new DynamicIntArray[instanceCount];
for (int i = 0; i < instanceCount; i++) {
edgeCount[i] = new DynamicIntArray(instanceCount);
}
graph = graphCreator.apply(instanceCount);
graph.withNodeData(instanceList);

var visited = Collections.newSetFromMap(new IdentityHashMap<>());
changeSet = createChangeSet(instanceCount);
for (var instance : instanceList) {
var entity = instance.entity();
if (visited.add(entity)) {
for (var variableId : outerGraph.variableReferenceToAfterProcessor.keySet()) {
afterVariableChanged(variableId, entity);
}
}
}
for (var fixedEdgeEntry : outerGraph.fixedEdges.entrySet()) {
for (var toEdge : fixedEdgeEntry.getValue()) {
addEdge(fixedEdgeEntry.getKey(), toEdge);
}
}
}

protected abstract ChangeSet_ createChangeSet(int instanceCount);

public @Nullable EntityVariablePair<Solution_> lookupOrNull(VariableMetaModel<?, ?, ?> variableId, Object entity) {
var map = variableReferenceToInstanceMap.get(variableId);
if (map == null) {
return null;
}
return map.get(entity);
}

public void addEdge(@NonNull EntityVariablePair<Solution_> from, @NonNull EntityVariablePair<Solution_> to) {
var fromNodeId = from.graphNodeId();
var toNodeId = to.graphNodeId();
if (fromNodeId == toNodeId) {
return;
}

var count = edgeCount[fromNodeId].get(toNodeId);
if (count == 0) {
graph.addEdge(fromNodeId, toNodeId);
}
edgeCount[fromNodeId].set(toNodeId, count + 1);
markChanged(to);
}

public void removeEdge(@NonNull EntityVariablePair<Solution_> from, @NonNull EntityVariablePair<Solution_> to) {
var fromNodeId = from.graphNodeId();
var toNodeId = to.graphNodeId();
if (fromNodeId == toNodeId) {
return;
}

var count = edgeCount[fromNodeId].get(toNodeId);
if (count == 1) {
graph.removeEdge(fromNodeId, toNodeId);
}
edgeCount[fromNodeId].set(toNodeId, count - 1);
markChanged(to);
}

abstract void markChanged(EntityVariablePair<Solution_> changed);

@Override
public void beforeVariableChanged(VariableMetaModel<?, ?, ?> variableReference, Object entity) {
if (variableReference.entity().type().isInstance(entity)) {
processEntity(variableReferenceToBeforeProcessor.getOrDefault(variableReference, Collections.emptyList()), entity);
}
}

@SuppressWarnings("ForLoopReplaceableByForEach")
private void processEntity(List<BiConsumer<AbstractVariableReferenceGraph<Solution_, ?>, Object>> processorList,
Object entity) {
var processorCount = processorList.size();
// Avoid creation of iterators on the hot path.
// The short-lived instances were observed to cause considerable GC pressure.
for (int i = 0; i < processorCount; i++) {
processorList.get(i).accept(this, entity);
}
}

@Override
public void afterVariableChanged(VariableMetaModel<?, ?, ?> variableReference, Object entity) {
if (variableReference.entity().type().isInstance(entity)) {
var node = lookupOrNull(variableReference, entity);
if (node != null) {
markChanged(node);
}
processEntity(variableReferenceToAfterProcessor.getOrDefault(variableReference, Collections.emptyList()), entity);
}
}

@Override
public String toString() {
var edgeList = new LinkedHashMap<EntityVariablePair<Solution_>, List<EntityVariablePair<Solution_>>>();
graph.forEachEdge((from, to) -> edgeList.computeIfAbsent(instanceList.get(from), k -> new ArrayList<>())
.add(instanceList.get(to)));
return edgeList.entrySet()
.stream()
.map(e -> e.getKey() + "->" + e.getValue())
.collect(Collectors.joining(
"," + System.lineSeparator() + " ",
"{" + System.lineSeparator() + " ",
"}"));

}

@SuppressWarnings("unchecked")
static <K1, K2, V> Map<K1, Map<K2, V>> mapOfMapsDeepCopyOf(Map<K1, Map<K2, V>> map) {
var entryArray = map.entrySet()
.stream()
.map(e -> Map.entry(e.getKey(), Map.copyOf(e.getValue())))
.toArray(Map.Entry[]::new);
return Map.ofEntries(entryArray);
}

@SuppressWarnings("unchecked")
static <K1, V> Map<K1, List<V>> mapOfListsDeepCopyOf(Map<K1, List<V>> map) {
var entryArray = map.entrySet()
.stream()
.map(e -> Map.entry(e.getKey(), List.copyOf(e.getValue())))
.toArray(Map.Entry[]::new);
return Map.ofEntries(entryArray);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,17 @@
import ai.timefold.solver.core.impl.domain.variable.descriptor.VariableDescriptor;
import ai.timefold.solver.core.impl.score.director.InnerScoreDirector;

import org.jspecify.annotations.Nullable;

public record ChangedVariableNotifier<Solution_>(BiConsumer<VariableDescriptor<Solution_>, Object> beforeVariableChanged,
BiConsumer<VariableDescriptor<Solution_>, Object> afterVariableChanged) {
BiConsumer<VariableDescriptor<Solution_>, Object> afterVariableChanged,
@Nullable InnerScoreDirector<Solution_, ?> innerScoreDirector) {

private static final ChangedVariableNotifier<?> EMPTY = new ChangedVariableNotifier<>((a, b) -> {
},
(a, b) -> {
});
},
null);

@SuppressWarnings("unchecked")
public static <Solution_> ChangedVariableNotifier<Solution_> empty() {
Expand All @@ -20,7 +25,8 @@ public static <Solution_> ChangedVariableNotifier<Solution_> empty() {
public static <Solution_> ChangedVariableNotifier<Solution_> of(InnerScoreDirector<Solution_, ?> scoreDirector) {
return new ChangedVariableNotifier<>(
scoreDirector::beforeVariableChanged,
scoreDirector::afterVariableChanged);
scoreDirector::afterVariableChanged,
scoreDirector);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,9 @@

@NullMarked
public final class DefaultShadowVariableSession<Solution_> implements Supply {
final VariableReferenceGraph<Solution_> graph;
final VariableReferenceGraph graph;

public DefaultShadowVariableSession(VariableReferenceGraph<Solution_> graph) {
public DefaultShadowVariableSession(VariableReferenceGraph graph) {
this.graph = graph;
}

Expand Down
Loading
Loading