Skip to content

Commit

Permalink
forEachIncludingUnassigned
Browse files Browse the repository at this point in the history
  • Loading branch information
triceo committed Jan 22, 2024
1 parent 1d4850c commit bb20e56
Show file tree
Hide file tree
Showing 18 changed files with 73 additions and 105 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ private <A> Predicate<A> getNullityFilter(Class<A> fromClass) {
}

@Override
public <A> UniConstraintStream<A> forEachIncludingNullVars(Class<A> sourceClass) {
public <A> UniConstraintStream<A> forEachIncludingUnassigned(Class<A> sourceClass) {
assertValidFromType(sourceClass);
return share(new BavetForEachUniConstraintStream<>(this, sourceClass, null, RetrievalSemantics.STANDARD));
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@ public final <C> BiConstraintStream<A, B> ifExists(Class<C> otherClass, TriJoine
@Override
public final <C> BiConstraintStream<A, B> ifExistsIncludingNullVars(Class<C> otherClass, TriJoiner<A, B, C>... joiners) {
if (getRetrievalSemantics() == RetrievalSemantics.STANDARD) {
return ifExists(constraintFactory.forEachIncludingNullVars(otherClass), joiners);
return ifExists(constraintFactory.forEachIncludingUnassigned(otherClass), joiners);
} else {
return ifExists(constraintFactory.fromUnfiltered(otherClass), joiners);
}
Expand All @@ -141,7 +141,7 @@ public final <C> BiConstraintStream<A, B> ifNotExists(Class<C> otherClass, TriJo
@Override
public final <C> BiConstraintStream<A, B> ifNotExistsIncludingNullVars(Class<C> otherClass, TriJoiner<A, B, C>... joiners) {
if (getRetrievalSemantics() == RetrievalSemantics.STANDARD) {
return ifNotExists(constraintFactory.forEachIncludingNullVars(otherClass), joiners);
return ifNotExists(constraintFactory.forEachIncludingUnassigned(otherClass), joiners);
} else {
return ifNotExists(constraintFactory.fromUnfiltered(otherClass), joiners);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ public final <E> QuadConstraintStream<A, B, C, D> ifExists(Class<E> otherClass,
public final <E> QuadConstraintStream<A, B, C, D> ifExistsIncludingNullVars(Class<E> otherClass,
PentaJoiner<A, B, C, D, E>... joiners) {
if (getRetrievalSemantics() == RetrievalSemantics.STANDARD) {
return ifExists(constraintFactory.forEachIncludingNullVars(otherClass), joiners);
return ifExists(constraintFactory.forEachIncludingUnassigned(otherClass), joiners);
} else {
return ifExists(constraintFactory.fromUnfiltered(otherClass), joiners);
}
Expand Down Expand Up @@ -121,7 +121,7 @@ public final <E> QuadConstraintStream<A, B, C, D> ifNotExists(Class<E> otherClas
public final <E> QuadConstraintStream<A, B, C, D> ifNotExistsIncludingNullVars(Class<E> otherClass,
PentaJoiner<A, B, C, D, E>... joiners) {
if (getRetrievalSemantics() == RetrievalSemantics.STANDARD) {
return ifNotExists(constraintFactory.forEachIncludingNullVars(otherClass), joiners);
return ifNotExists(constraintFactory.forEachIncludingUnassigned(otherClass), joiners);
} else {
return ifNotExists(constraintFactory.fromUnfiltered(otherClass), joiners);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@ public final <D> TriConstraintStream<A, B, C> ifExists(Class<D> otherClass, Quad
public final <D> TriConstraintStream<A, B, C> ifExistsIncludingNullVars(Class<D> otherClass,
QuadJoiner<A, B, C, D>... joiners) {
if (getRetrievalSemantics() == RetrievalSemantics.STANDARD) {
return ifExists(constraintFactory.forEachIncludingNullVars(otherClass), joiners);
return ifExists(constraintFactory.forEachIncludingUnassigned(otherClass), joiners);
} else {
return ifExists(constraintFactory.fromUnfiltered(otherClass), joiners);
}
Expand Down Expand Up @@ -143,7 +143,7 @@ public final <D> TriConstraintStream<A, B, C> ifNotExists(Class<D> otherClass, Q
public final <D> TriConstraintStream<A, B, C> ifNotExistsIncludingNullVars(Class<D> otherClass,
QuadJoiner<A, B, C, D>... joiners) {
if (getRetrievalSemantics() == RetrievalSemantics.STANDARD) {
return ifNotExists(constraintFactory.forEachIncludingNullVars(otherClass), joiners);
return ifNotExists(constraintFactory.forEachIncludingUnassigned(otherClass), joiners);
} else {
return ifNotExists(constraintFactory.fromUnfiltered(otherClass), joiners);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,14 @@
/**
* Filtering nodes are expensive.
* Considering that most streams start with a nullity check on genuine planning variables,
* it makes sense to create a specialized version of the node for this case ({@link ForEachExcludingNullVarsUniNode}),
* as opposed to forcing an extra filter node on the generic case ({@link ForEachIncludingNullVarsUniNode}).
* it makes sense to create a specialized version of the node for this case ({@link ForEachExcludingUnassignedUniNode}),
* as opposed to forcing an extra filter node on the generic case ({@link ForEachIncludingUnassignedUniNode}).
*
* @param <A>
*/
public abstract sealed class AbstractForEachUniNode<A>
extends AbstractNode
permits ForEachIncludingNullVarsUniNode, ForEachExcludingNullVarsUniNode {
permits ForEachIncludingUnassignedUniNode, ForEachExcludingUnassignedUniNode {

private final Class<A> forEachClass;
private final int outputStoreSize;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ public final <B> UniConstraintStream<A> ifExists(Class<B> otherClass, BiJoiner<A
@Override
public final <B> UniConstraintStream<A> ifExistsIncludingNullVars(Class<B> otherClass, BiJoiner<A, B>... joiners) {
if (getRetrievalSemantics() == RetrievalSemantics.STANDARD) {
return ifExists(constraintFactory.forEachIncludingNullVars(otherClass), joiners);
return ifExists(constraintFactory.forEachIncludingUnassigned(otherClass), joiners);
} else {
return ifExists(constraintFactory.fromUnfiltered(otherClass), joiners);
}
Expand All @@ -146,7 +146,7 @@ public final <B> UniConstraintStream<A> ifNotExists(Class<B> otherClass, BiJoine
@Override
public final <B> UniConstraintStream<A> ifNotExistsIncludingNullVars(Class<B> otherClass, BiJoiner<A, B>... joiners) {
if (getRetrievalSemantics() == RetrievalSemantics.STANDARD) {
return ifNotExists(constraintFactory.forEachIncludingNullVars(otherClass), joiners);
return ifNotExists(constraintFactory.forEachIncludingUnassigned(otherClass), joiners);
} else {
return ifNotExists(constraintFactory.fromUnfiltered(otherClass), joiners);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,8 +48,8 @@ public void collectActiveConstraintStreams(Set<BavetAbstractConstraintStream<Sol
public <Score_ extends Score<Score_>> void buildNode(NodeBuildHelper<Score_> buildHelper) {
TupleLifecycle<UniTuple<A>> tupleLifecycle = buildHelper.getAggregatedTupleLifecycle(childStreamList);
int outputStoreSize = buildHelper.extractTupleStoreSize(this);
var node = filter == null ? new ForEachIncludingNullVarsUniNode<>(forEachClass, tupleLifecycle, outputStoreSize)
: new ForEachExcludingNullVarsUniNode<>(forEachClass, filter, tupleLifecycle, outputStoreSize);
var node = filter == null ? new ForEachIncludingUnassignedUniNode<>(forEachClass, tupleLifecycle, outputStoreSize)
: new ForEachExcludingUnassignedUniNode<>(forEachClass, filter, tupleLifecycle, outputStoreSize);
buildHelper.addNode(node, this, null);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,11 @@
import ai.timefold.solver.constraint.streams.bavet.common.tuple.TupleLifecycle;
import ai.timefold.solver.constraint.streams.bavet.common.tuple.UniTuple;

public final class ForEachExcludingNullVarsUniNode<A> extends AbstractForEachUniNode<A> {
public final class ForEachExcludingUnassignedUniNode<A> extends AbstractForEachUniNode<A> {

private final Predicate<A> filter;

public ForEachExcludingNullVarsUniNode(Class<A> forEachClass, Predicate<A> filter,
public ForEachExcludingUnassignedUniNode(Class<A> forEachClass, Predicate<A> filter,
TupleLifecycle<UniTuple<A>> nextNodesTupleLifecycle, int outputStoreSize) {
super(forEachClass, nextNodesTupleLifecycle, outputStoreSize);
this.filter = Objects.requireNonNull(filter);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@
import ai.timefold.solver.constraint.streams.bavet.common.tuple.TupleLifecycle;
import ai.timefold.solver.constraint.streams.bavet.common.tuple.UniTuple;

public final class ForEachIncludingNullVarsUniNode<A> extends AbstractForEachUniNode<A> {
public final class ForEachIncludingUnassignedUniNode<A> extends AbstractForEachUniNode<A> {

public ForEachIncludingNullVarsUniNode(Class<A> forEachClass, TupleLifecycle<UniTuple<A>> nextNodesTupleLifecycle,
public ForEachIncludingUnassignedUniNode(Class<A> forEachClass, TupleLifecycle<UniTuple<A>> nextNodesTupleLifecycle,
int outputStoreSize) {
super(forEachClass, nextNodesTupleLifecycle, outputStoreSize);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,9 @@ public enum RetrievalSemantics {

/**
* Joins do not include entities with null planning variables,
* unless specifically requested by join(forEachIncludingNullVars(...)).
* unless specifically requested by join(forEachIncludingUnassigned(...)).
* Conditional propagation does not include null planning variables,
* unless specifically requested using a *IncludingNullVars() method overload.
* unless specifically requested using a *IncludingUnassigned() method overload.
*
* <p>
* Applies when the stream comes off of a {@link ConstraintFactory#forEach(Class)} family of methods.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -189,7 +189,7 @@ public void filteringJoinNullConflictDifferentNodes() {
InnerScoreDirector<TestdataSolution, SimpleScore> scoreDirector =
buildScoreDirector(TestdataSolution.buildSolutionDescriptor(),
factory -> new Constraint[] {
factory.forEachIncludingNullVars(TestdataEntity.class)
factory.forEachIncludingUnassigned(TestdataEntity.class)
.filter(a -> a.getValue() != null)
.join(TestdataEntity.class,
filtering((a, b) -> {
Expand Down Expand Up @@ -246,7 +246,7 @@ public void filteringIfExistsNullConflictDifferentNodes() {
InnerScoreDirector<TestdataSolution, SimpleScore> scoreDirector =
buildScoreDirector(TestdataSolution.buildSolutionDescriptor(),
factory -> new Constraint[] {
factory.forEachIncludingNullVars(TestdataEntity.class)
factory.forEachIncludingUnassigned(TestdataEntity.class)
.filter(a -> a.getValue() != null)
.ifExists(TestdataEntity.class,
filtering((a, b) -> {
Expand Down Expand Up @@ -300,7 +300,7 @@ public void filteringIfNotExistsNullConflictDifferentNodes() {
InnerScoreDirector<TestdataSolution, SimpleScore> scoreDirector =
buildScoreDirector(TestdataSolution.buildSolutionDescriptor(),
factory -> new Constraint[] {
factory.forEachIncludingNullVars(TestdataEntity.class)
factory.forEachIncludingUnassigned(TestdataEntity.class)
.filter(a -> a.getValue() != null)
.ifNotExists(TestdataEntity.class,
filtering((a, b) -> {
Expand Down Expand Up @@ -359,7 +359,7 @@ public void mapPlanningEntityChanges() {
InnerScoreDirector<TestdataSolution, SimpleScore> scoreDirector =
buildScoreDirector(TestdataSolution.buildSolutionDescriptor(),
factory -> new Constraint[] {
factory.forEachIncludingNullVars(TestdataEntity.class)
factory.forEachIncludingUnassigned(TestdataEntity.class)
.map(Function.identity())
.filter(e -> e.getValue() != null)
.penalize(SimpleScore.ONE)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ public <A> UniConstraintStream<A> forEach(Class<A> sourceClass) {
}

@Override
public <A> UniConstraintStream<A> forEachIncludingNullVars(Class<A> sourceClass) {
public <A> UniConstraintStream<A> forEachIncludingUnassigned(Class<A> sourceClass) {
throw new UnsupportedOperationException();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@
import ai.timefold.solver.core.api.domain.lookup.PlanningId;
import ai.timefold.solver.core.api.domain.solution.PlanningSolution;
import ai.timefold.solver.core.api.domain.solution.ProblemFactCollectionProperty;
import ai.timefold.solver.core.api.domain.variable.InverseRelationShadowVariable;
import ai.timefold.solver.core.api.domain.variable.PlanningListVariable;
import ai.timefold.solver.core.api.domain.variable.PlanningVariable;
import ai.timefold.solver.core.api.score.stream.bi.BiConstraintStream;
Expand Down Expand Up @@ -42,72 +41,41 @@ public interface ConstraintFactory {
* {@link UniConstraintStream#filter(Predicate) filtered} to only contain entities
* for which each genuine {@link PlanningVariable} (of the sourceClass or a superclass thereof) has a non-null value.
* <p>
* This does not apply to {@link PlanningListVariable#allowsUnassignedValues()}.
* Elements can be unassigned (not present in any list) and this method will still include them.
* This situation needs to be handled by adding a {@link InverseRelationShadowVariable} on the element,
* exposing it via a new method such as {@code getUnassigned()},
* and then calling {@link UniConstraintStream#filter(Predicate)} with that method.
* <p>
* Example:
*
* <pre>
* {@code
* &#64;PlanningEntity
* public class Vehicle {
*
* &#64;PlanningListVariable(allowsUnassignedValues = true)
* List<Customer> customerList = ...;
*
* ...
*
* }
*
* &#64;PlanningEntity
* public class Customer {
*
* &#64;InverseRelationShadowVariable(sourceVariableName = "customerList")
* private Vehicle entity;
*
* public boolean isAssigned() {
* return entity != null;
* }
*
* ...
*
* }
*
* public class VehicleRoutingConstraintProvider implements ConstraintProvider {
* If the sourceClass is a shadow entity (an entity without any genuine planning variables),
* and if there exists a genuine {@link PlanningEntity} with a {@link PlanningListVariable}
* which accepts instances of this shadow entity as values in that list,
* and if that list variable {@link PlanningListVariable#allowsUnassignedValues() allows unassigned values},
* then this stream will filter out all sourceClass instances
* which are not present in any instances of that list variable.
*
* ...
*
* public Constraint everyAssignedCustomer(ConstraintFactory constraintFactory) {
* return constraintFactory.forEach(Customer.class)
* .filter(Customer::isAssigned)
* ...
* }
*
* ...
*
* }
* }
* </pre>
*
*
* @param sourceClass never null
* @param <A> the type of the matched problem fact or {@link PlanningEntity planning entity}
* @return never null
*/
<A> UniConstraintStream<A> forEach(Class<A> sourceClass);

/**
* As defined by {@link #forEachIncludingUnassigned(Class)}.
*
* @deprecated Use {@link #forEachIncludingUnassigned(Class)} instead.
*/
@Deprecated(forRemoval = true, since = "1.7.0")
default <A> UniConstraintStream<A> forEachIncludingNullVars(Class<A> sourceClass) {
return forEachIncludingUnassigned(sourceClass);
}

/**
* As defined by {@link #forEach(Class)},
* but without any filtering of null {@link PlanningEntity planning entity} variables.
* but without any filtering of unassigned {@link PlanningEntity planning entities}
* (for {@link PlanningVariable#allowsUnassigned()})
* or shadow entities not assigned to any applicable list variable
* (for {@link PlanningListVariable#allowsUnassignedValues()}).
*
* @param sourceClass never null
* @param <A> the type of the matched problem fact or {@link PlanningEntity planning entity}
* @return never null
*/
<A> UniConstraintStream<A> forEachIncludingNullVars(Class<A> sourceClass);
<A> UniConstraintStream<A> forEachIncludingUnassigned(Class<A> sourceClass);

/**
* Create a new {@link BiConstraintStream} for every unique combination of A and another A with a higher {@link PlanningId}.
Expand Down Expand Up @@ -233,7 +201,7 @@ default <A> BiConstraintStream<A, A> forEachUniquePair(Class<A> sourceClass, BiJ
* Calls to the {@link #forEach(Class)} family of methods will now filter out planning entities with null variables,
* so most constraints no longer need to do null checks,
* but the constraint that penalizes unassigned entities (typically a medium constraint)
* must now use {@link #forEachIncludingNullVars(Class)} instead.
* must now use {@link #forEachIncludingUnassigned(Class)} instead.
* Subsequent joins and conditional propagation calls will now also consistently filter out planning entities with null
* variables.</li>
* </ul>
Expand All @@ -260,7 +228,7 @@ default <A> BiConstraintStream<A, A> forEachUniquePair(Class<A> sourceClass, BiJ

/**
* This method is deprecated.
* Migrate uses of this method to {@link #forEachIncludingNullVars(Class)},
* Migrate uses of this method to {@link #forEachIncludingUnassigned(Class)},
* but first understand that subsequent joins and conditional propagation calls
* ({@link UniConstraintStream#ifExists} etc.)
* will now also consistently filter out planning entities with null variables.
Expand All @@ -270,7 +238,7 @@ default <A> BiConstraintStream<A, A> forEachUniquePair(Class<A> sourceClass, BiJ
* As defined by {@link #from(Class)},
* but without any filtering of uninitialized {@link PlanningEntity planning entities}.
*
* @deprecated Prefer {@link #forEachIncludingNullVars(Class)}.
* @deprecated Prefer {@link #forEachIncludingUnassigned(Class)}.
* @param fromClass never null
* @param <A> the type of the matched problem fact or {@link PlanningEntity planning entity}
* @return never null
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -221,12 +221,12 @@ or a xref:using-timefold-solver/modeling-planning-problems.adoc#planningEntities
and has no `null` genuine planning variables.

To include instances with a `null` genuine planning variable,
replace the `forEach()` building block by `forEachIncludingNullVars()`:
replace the `forEach()` building block by `forEachIncludingUnassigned()`:

[source,java,options="nowrap"]
----
private Constraint penalizeAllShifts(ConstraintFactory factory) {
return factory.forEachIncludingNullVars(Shift.class)
return factory.forEachIncludingUnassigned(Shift.class)
.penalize(HardSoftScore.ONE_SOFT)
.asConstraint("A shift");
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -334,7 +334,7 @@ To allow an initialized planning variable to be ``null``, set `allows_unassigned
[NOTE]
====
Constraint Streams filter out planning entities with a `null` planning variable by default.
Use xref:constraints-and-score/score-calculation.adoc#constraintStreamsForEach[forEachIncludingNullVars()] to avoid such unwanted behaviour.
Use xref:constraints-and-score/score-calculation.adoc#constraintStreamsForEach[forEachIncludingUnassigned()] to avoid such unwanted behaviour.
====
Timefold Solver will automatically add the value `null` to the value range.
Expand Down

0 comments on commit bb20e56

Please sign in to comment.