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
Expand Up @@ -14,7 +14,7 @@
*/
public final class BiJoinerComber<A, B> {

public static <A, B> BiJoinerComber<A, B> comb(BiJoiner<A, B>[] joiners) {
public static <A, B> BiJoinerComber<A, B> comb(BiJoiner<A, B>... joiners) {
List<DefaultBiJoiner<A, B>> defaultJoinerList = new ArrayList<>(joiners.length);
List<BiPredicate<A, B>> filteringList = new ArrayList<>(joiners.length);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@
import ai.timefold.solver.core.impl.bavet.common.joiner.JoinerType;
import ai.timefold.solver.core.impl.util.ElementAwareListEntry;

import org.jspecify.annotations.NullMarked;

@NullMarked
final class ComparisonIndexer<T, Key_ extends Comparable<Key_>>
implements Indexer<T> {

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

import ai.timefold.solver.core.impl.util.ElementAwareListEntry;

import org.jspecify.annotations.NullMarked;

@NullMarked
final class EqualsIndexer<T, Key_> implements Indexer<T> {

private final KeyRetriever<Key_> keyRetriever;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
import ai.timefold.solver.core.impl.bavet.common.tuple.TupleState;
import ai.timefold.solver.core.impl.util.ElementAwareListEntry;

import org.jspecify.annotations.NullMarked;

/**
* An indexer for entity or fact {@code X},
* maps a property or a combination of properties of {@code X}, denoted by {@code indexKeys},
Expand All @@ -20,6 +22,7 @@
* For example for {@code from(A).join(B)}, the tuple is {@code UniTuple<A>} xor {@code UniTuple<B>}.
* For example for {@code Bi<A, B>.join(C)}, the tuple is {@code BiTuple<A, B>} xor {@code UniTuple<C>}.
*/
@NullMarked
public sealed interface Indexer<T> permits ComparisonIndexer, EqualsIndexer, NoneIndexer {

ElementAwareListEntry<T> put(Object indexKeys, T tuple);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@
import ai.timefold.solver.core.impl.util.ElementAwareList;
import ai.timefold.solver.core.impl.util.ElementAwareListEntry;

import org.jspecify.annotations.NullMarked;

@NullMarked
public final class NoneIndexer<T> implements Indexer<T> {

private final ElementAwareList<T> tupleList = new ElementAwareList<>();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,18 +21,14 @@ public enum JoinerType {
}

public JoinerType flip() {
switch (this) {
case LESS_THAN:
return GREATER_THAN;
case LESS_THAN_OR_EQUAL:
return GREATER_THAN_OR_EQUAL;
case GREATER_THAN:
return LESS_THAN;
case GREATER_THAN_OR_EQUAL:
return LESS_THAN_OR_EQUAL;
default:
throw new IllegalStateException("The joinerType (" + this + ") cannot be flipped.");
}
return switch (this) {
case LESS_THAN -> GREATER_THAN;
case LESS_THAN_OR_EQUAL -> GREATER_THAN_OR_EQUAL;
case GREATER_THAN -> LESS_THAN;
case GREATER_THAN_OR_EQUAL -> LESS_THAN_OR_EQUAL;
default -> throw new IllegalStateException("The joinerType (%s) cannot be flipped."
.formatted(this));
};
}

public boolean matches(Object left, Object right) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
import ai.timefold.solver.core.impl.domain.variable.ListVariableStateDemand;
import ai.timefold.solver.core.impl.domain.variable.inverserelation.InverseRelationShadowVariableDescriptor;
import ai.timefold.solver.core.impl.move.director.MoveDirector;
import ai.timefold.solver.core.impl.neighborhood.maybeapi.stream.enumerating.function.BiEnumeratingFilter;
import ai.timefold.solver.core.impl.neighborhood.maybeapi.stream.enumerating.function.BiEnumeratingPredicate;

public final class ListVariableDescriptor<Solution_> extends GenuineVariableDescriptor<Solution_> {

Expand All @@ -24,7 +24,7 @@ public final class ListVariableDescriptor<Solution_> extends GenuineVariableDesc
var list = getValue(entity);
return list.contains(element);
};
private final BiEnumeratingFilter<Solution_, Object, Object> entityContainsPinnedValuePredicate =
private final BiEnumeratingPredicate<Solution_, Object, Object> entityContainsPinnedValuePredicate =
(solutionView, value, entity) -> {
var moveDirector = (MoveDirector<Solution_, ?>) solutionView;
return moveDirector.isPinned(this, value);
Expand All @@ -47,8 +47,8 @@ public <A> BiPredicate<A, Object> getInListPredicate() {
}

@SuppressWarnings("unchecked")
public <A, B> BiEnumeratingFilter<Solution_, A, B> getEntityContainsPinnedValuePredicate() {
return (BiEnumeratingFilter<Solution_, A, B>) entityContainsPinnedValuePredicate;
public <A, B> BiEnumeratingPredicate<Solution_, A, B> getEntityContainsPinnedValuePredicate() {
return (BiEnumeratingPredicate<Solution_, A, B>) entityContainsPinnedValuePredicate;
}

public boolean allowsUnassignedValues() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,11 @@
import ai.timefold.solver.core.api.domain.entity.PlanningPinToIndex;
import ai.timefold.solver.core.api.domain.solution.ProblemFactCollectionProperty;
import ai.timefold.solver.core.api.domain.variable.PlanningListVariable;
import ai.timefold.solver.core.impl.neighborhood.maybeapi.stream.enumerating.BiEnumeratingStream;
import ai.timefold.solver.core.impl.neighborhood.maybeapi.stream.enumerating.EnumeratingStream;
import ai.timefold.solver.core.impl.neighborhood.maybeapi.stream.enumerating.UniEnumeratingStream;
import ai.timefold.solver.core.impl.neighborhood.maybeapi.stream.enumerating.function.UniEnumeratingFilter;
import ai.timefold.solver.core.impl.neighborhood.maybeapi.stream.sampling.BiSamplingStream;
import ai.timefold.solver.core.impl.neighborhood.maybeapi.stream.sampling.UniSamplingStream;
import ai.timefold.solver.core.preview.api.domain.metamodel.ElementPosition;
import ai.timefold.solver.core.preview.api.domain.metamodel.GenuineVariableMetaModel;
import ai.timefold.solver.core.preview.api.domain.metamodel.PlanningListVariableMetaModel;
import ai.timefold.solver.core.preview.api.domain.metamodel.PlanningSolutionMetaModel;
import ai.timefold.solver.core.preview.api.domain.metamodel.UnassignedElement;
Expand Down Expand Up @@ -61,18 +58,6 @@ public interface MoveStreamFactory<Solution_> {
*/
<A> UniEnumeratingStream<Solution_, A> forEachUnfiltered(Class<A> sourceClass, boolean includeNull);

/**
* Enumerate possible values for any given entity,
* where entities are obtained using {@link #forEach(Class, boolean)},
* with the class matching the entity type of the variable.
* If the variable allows unassigned values, the resulting stream will include a null value.
*
* @param variableMetaModel the meta model of the variable to enumerate
* @return enumerating stream with all possible values of a given variable
*/
<Entity_, Value_> BiEnumeratingStream<Solution_, Entity_, Value_>
forEachEntityValuePair(GenuineVariableMetaModel<Solution_, Entity_, Value_> variableMetaModel);

/**
* Enumerate all possible positions of a list variable to which a value can be assigned.
* This will eliminate all positions on {@link PlanningPin pinned entities},
Expand All @@ -89,6 +74,4 @@ public interface MoveStreamFactory<Solution_> {

<A> UniSamplingStream<Solution_, A> pick(UniEnumeratingStream<Solution_, A> enumeratingStream);

<A, B> BiSamplingStream<Solution_, A, B> pick(BiEnumeratingStream<Solution_, A, B> enumeratingStream);

}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
import ai.timefold.solver.core.impl.neighborhood.maybeapi.MoveDefinition;
import ai.timefold.solver.core.impl.neighborhood.maybeapi.MoveStream;
import ai.timefold.solver.core.impl.neighborhood.maybeapi.MoveStreamFactory;
import ai.timefold.solver.core.impl.neighborhood.maybeapi.stream.enumerating.EnumeratingJoiners;
import ai.timefold.solver.core.impl.neighborhood.stream.DefaultMoveStreamFactory;
import ai.timefold.solver.core.preview.api.domain.metamodel.PlanningVariableMetaModel;

import org.jspecify.annotations.NullMarked;
Expand All @@ -21,13 +23,12 @@ public ChangeMoveDefinition(PlanningVariableMetaModel<Solution_, Entity_, Value_

@Override
public MoveStream<Solution_> build(MoveStreamFactory<Solution_> moveStreamFactory) {
var enumeratingStream =
moveStreamFactory.forEachEntityValuePair(variableMetaModel)
.filter((solutionView, entity, value) -> {
Value_ currentValue = solutionView.getValue(variableMetaModel, Objects.requireNonNull(entity));
return !Objects.equals(currentValue, value);
});
return moveStreamFactory.pick(enumeratingStream)
var nodeSharingSupportFunctions =
((DefaultMoveStreamFactory<Solution_>) moveStreamFactory).getNodeSharingSupportFunctions(variableMetaModel);
return moveStreamFactory.pick(moveStreamFactory.forEach(variableMetaModel.entity().type(), false))
.pick(moveStreamFactory.forEach(variableMetaModel.type(), variableMetaModel.allowsUnassigned()),
EnumeratingJoiners.filtering(nodeSharingSupportFunctions.differentValueFilter()),
EnumeratingJoiners.filtering(nodeSharingSupportFunctions.valueInRangeFilter()))
.asMove((solution, entity, value) -> Moves.change(Objects.requireNonNull(entity), value, variableMetaModel));
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,14 +46,10 @@ public ListChangeMoveDefinition(PlanningListVariableMetaModel<Solution_, Entity_
@Override
public MoveStream<Solution_> build(MoveStreamFactory<Solution_> moveStreamFactory) {
var entityValuePairs = moveStreamFactory.forEachAssignablePosition(variableMetaModel);
// The stream of these positions is joined with the stream of all existing values,
// filtering out those which would not result in a valid move.
var enumeratingStream = moveStreamFactory.forEach(variableMetaModel.type(), false)
.join(entityValuePairs, EnumeratingJoiners.filtering(this::isValidChange));
// When picking from this stream, we decide what kind of move we need to create,
// based on whether the value is assigned or unassigned.
return moveStreamFactory.pick(enumeratingStream)
.asMove((solutionView, value, targetPosition) -> {
var availableValues = moveStreamFactory.forEach(variableMetaModel.type(), false);
return moveStreamFactory.pick(entityValuePairs)
.pick(availableValues, EnumeratingJoiners.filtering(this::isValidChange))
.asMove((solutionView, targetPosition, value) -> {
var currentPosition = solutionView.getPositionOf(variableMetaModel, Objects.requireNonNull(value));
if (targetPosition instanceof UnassignedElement) {
var currentElementPosition = currentPosition.ensureAssigned();
Expand All @@ -68,7 +64,7 @@ public MoveStream<Solution_> build(MoveStreamFactory<Solution_> moveStreamFactor
});
}

private boolean isValidChange(SolutionView<Solution_> solutionView, Value_ value, ElementPosition targetPosition) {
private boolean isValidChange(SolutionView<Solution_> solutionView, ElementPosition targetPosition, Value_ value) {
var currentPosition = solutionView.getPositionOf(variableMetaModel, value);
if (currentPosition.equals(targetPosition)) { // No change needed.
return false;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
package ai.timefold.solver.core.impl.neighborhood.maybeapi.move;

import java.util.Objects;
import java.util.function.Function;

import ai.timefold.solver.core.api.domain.lookup.PlanningId;
import ai.timefold.solver.core.impl.domain.common.accessor.MemberAccessor;
import ai.timefold.solver.core.impl.domain.solution.descriptor.DefaultPlanningSolutionMetaModel;
import ai.timefold.solver.core.impl.domain.solution.descriptor.SolutionDescriptor;
import ai.timefold.solver.core.impl.neighborhood.maybeapi.MoveDefinition;
import ai.timefold.solver.core.impl.neighborhood.maybeapi.MoveStream;
import ai.timefold.solver.core.impl.neighborhood.maybeapi.MoveStreamFactory;
Expand All @@ -17,46 +22,60 @@ public class ListSwapMoveDefinition<Solution_, Entity_, Value_>
implements MoveDefinition<Solution_> {

private final PlanningListVariableMetaModel<Solution_, Entity_, Value_> variableMetaModel;
private final Function<Entity_, Comparable> planningIdGetter;

public ListSwapMoveDefinition(PlanningListVariableMetaModel<Solution_, Entity_, Value_> variableMetaModel) {
this.variableMetaModel = Objects.requireNonNull(variableMetaModel);
this.planningIdGetter = getPlanningIdGetter(variableMetaModel.entity().type());
}

private <A> Function<A, Comparable> getPlanningIdGetter(Class<A> sourceClass) {
SolutionDescriptor<Solution_> solutionDescriptor =
((DefaultPlanningSolutionMetaModel<Solution_>) variableMetaModel.entity().solution()).solutionDescriptor();
MemberAccessor planningIdMemberAccessor = solutionDescriptor.getPlanningIdAccessor(sourceClass);
if (planningIdMemberAccessor == null) {
throw new IllegalArgumentException(
"The fromClass (%s) has no member with a @%s annotation, so the pairs cannot be made unique ([A,B] vs [B,A])."
.formatted(sourceClass, PlanningId.class.getSimpleName()));
}
return planningIdMemberAccessor.getGetterFunction();
}

@Override
public MoveStream<Solution_> build(MoveStreamFactory<Solution_> moveStreamFactory) {
var assignedValueStream = moveStreamFactory.forEach(variableMetaModel.type(), false)
.filter((solutionView,
value) -> solutionView.getPositionOf(variableMetaModel, value) instanceof PositionInList);
var validAssignedValuePairStream = assignedValueStream.join(assignedValueStream,
EnumeratingJoiners.filtering((SolutionView<Solution_> solutionView, Value_ leftValue,
Value_ rightValue) -> !Objects.equals(leftValue, rightValue)));
// Ensure unique pairs; without demanding PlanningId, this becomes tricky.
// Convert values to their locations in list.
var validAssignedValueUniquePairStream =
validAssignedValuePairStream
.map((solutionView, leftValue, rightValue) -> new UniquePair<>(leftValue, rightValue))
.distinct()
.map((solutionView, pair) -> FullElementPosition.of(variableMetaModel, solutionView, pair.first()),
(solutionView, pair) -> FullElementPosition.of(variableMetaModel, solutionView, pair.second()));
// Eliminate pairs that cannot be swapped due to value range restrictions.
var result = validAssignedValueUniquePairStream
.filter((solutionView, leftPosition, rightPosition) -> solutionView.isValueInRange(variableMetaModel,
rightPosition.entity(), leftPosition.value())
&& solutionView.isValueInRange(variableMetaModel, leftPosition.entity(), rightPosition.value()));
// Finally pick the moves.
return moveStreamFactory.pick(result)
.filter((solutionView, value) -> solutionView.getPositionOf(variableMetaModel, value) instanceof PositionInList)
.map((solutionView, value) -> new FullElementPosition<>(value,
solutionView.getPositionOf(variableMetaModel, value).ensureAssigned(), planningIdGetter));
// TODO this requires everything that is ever swapped to implement @PlanningID; likely not acceptable
return moveStreamFactory.pick(assignedValueStream)
.pick(assignedValueStream,
EnumeratingJoiners.lessThan(a -> a),
EnumeratingJoiners.filtering(this::isValidSwap))
.asMove((solutionView, leftPosition, rightPosition) -> Moves.swap(leftPosition.elementPosition,
rightPosition.elementPosition, variableMetaModel));
}

private boolean isValidSwap(SolutionView<Solution_> solutionView,
FullElementPosition<Entity_, Value_> leftPosition,
FullElementPosition<Entity_, Value_> rightPosition) {
if (Objects.equals(leftPosition, rightPosition)) {
return false;
}
return solutionView.isValueInRange(variableMetaModel, rightPosition.entity(), leftPosition.value())
&& solutionView.isValueInRange(variableMetaModel, leftPosition.entity(), rightPosition.value());
}

@NullMarked
private record FullElementPosition<Entity_, Value_>(Value_ value, PositionInList elementPosition) {
private record FullElementPosition<Entity_, Value_>(Value_ value, PositionInList elementPosition,
Function<Entity_, Comparable> planningIdGetter) implements Comparable<FullElementPosition<Entity_, Value_>> {

public static <Solution_, Entity_, Value_> FullElementPosition<Entity_, Value_> of(
PlanningListVariableMetaModel<Solution_, Entity_, Value_> variableMetaModel,
SolutionView<Solution_> solutionView, Value_ value) {
SolutionView<Solution_> solutionView, Value_ value,
Function<Entity_, Comparable> planningIdGetter) {
var assignedElement = solutionView.getPositionOf(variableMetaModel, value).ensureAssigned();
return new FullElementPosition<>(value, assignedElement);
return new FullElementPosition<>(value, assignedElement, planningIdGetter);
}

public Entity_ entity() {
Expand All @@ -67,6 +86,15 @@ public int index() {
return elementPosition.index();
}

@Override
public int compareTo(FullElementPosition<Entity_, Value_> o) {
var entityComparison = planningIdGetter.apply(this.entity()).compareTo(planningIdGetter.apply(o.entity()));
if (entityComparison != 0) {
return entityComparison;
}
return Integer.compare(this.index(), o.index());
}

}

}
Loading
Loading