Skip to content
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
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

import ai.timefold.solver.core.api.score.Score;
import ai.timefold.solver.core.api.score.stream.ConstraintProvider;
import ai.timefold.solver.core.api.score.stream.ConstraintRef;
import ai.timefold.solver.core.api.score.stream.uni.UniConstraintStream;
import ai.timefold.solver.core.api.solver.change.ProblemChange;
import ai.timefold.solver.core.impl.domain.solution.DefaultConstraintWeightOverrides;
Expand Down Expand Up @@ -44,16 +45,24 @@ static <Score_ extends Score<Score_>> ConstraintWeightOverrides<Score_> of(Map<S
/**
* Return a constraint weight for a particular constraint.
*
* @return null if the constraint name is not known
* @return null if the constraint id is not known
*/
@Nullable
Score_ getConstraintWeight(String constraintName);
Score_ getConstraintWeight(String constraintId);

/**
* As defined by {@link #getConstraintWeight(String)},
* but accepts {@link ConstraintRef} instead of the ID directly.
*/
default @Nullable Score_ getConstraintWeight(ConstraintRef constraintRef) {
return getConstraintWeight(constraintRef.id());
}

/**
* Returns all known constraints.
*
* @return All constraint names for which {@link #getConstraintWeight(String)} returns a non-null value.
* @return All constraint IDs for which {@link #getConstraintWeight(String)} returns a non-null value.
*/
Set<String> getKnownConstraintNames();
Set<String> getKnownConstraintIds();

}
Original file line number Diff line number Diff line change
Expand Up @@ -52,11 +52,13 @@ public interface ConstraintAnalysis<Score_ extends Score<Score_>> {
int matchCount();

/**
* Return name of the constraint that this analysis is for.
* Return id of the constraint that this analysis is for.
*
* @return equal to {@code constraintRef.constraintName()}
* @return equal to {@code constraintRef.id()}
*/
String constraintName();
default String constraintId() {
return constraintRef().id();
}

/**
* Returns a diagnostic text that explains part of the score quality through the {@link ConstraintAnalysis} API.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ public interface ScoreAnalysis<Score_ extends Score<Score_>> {
* @return null if no constraint matches of such constraint are present
*/
@Nullable
ConstraintAnalysis<Score_> getConstraintAnalysis(String constraintName);
ConstraintAnalysis<Score_> getConstraintAnalysis(String constraintId);

/**
* Compare this {@link ScoreAnalysis} to another {@link ScoreAnalysis}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import ai.timefold.solver.core.api.score.Score;

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

/**
Expand All @@ -11,34 +11,27 @@
* It is defined in {@link ConstraintProvider#defineConstraints(ConstraintFactory)}
* by calling {@link ConstraintFactory#forEach(Class)}.
*/
@NullMarked
public interface Constraint {

String DEFAULT_CONSTRAINT_GROUP = "default";

ConstraintRef getConstraintRef();

/**
* Returns a human-friendly description of the constraint.
* The format of the description is left unspecified and will not be parsed in any way.
* Returns the metadata for this constraint, as provided to
* {@link ConstraintBuilder#asConstraint(ConstraintMetadata)}.
* The constraint's identity ({@link ConstraintMetadata#id()}) is fixed at build time;
* any later mutation of the returned object does not affect the constraint's identity.
*
* @return may be left empty
* @return never null
*/
default @NonNull String getDescription() {
return "";
}

default @NonNull String getConstraintGroup() {
return DEFAULT_CONSTRAINT_GROUP;
}
ConstraintMetadata getConstraintMetadata();

/**
* Returns the weight of the constraint as defined in the {@link ConstraintProvider},
* without any overrides.
*
* @return null if the constraint does not have a weight defined
*/
default <Score_ extends Score<Score_>> @Nullable Score_ getConstraintWeight() {
return null;
}
<Score_ extends Score<Score_>> @Nullable Score_ getConstraintWeight();

}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package ai.timefold.solver.core.api.score.stream;

import ai.timefold.solver.core.api.score.analysis.ScoreAnalysis;
import ai.timefold.solver.core.impl.score.stream.common.DefaultConstraintMetadata;

import org.jspecify.annotations.NullMarked;

Expand All @@ -9,40 +10,23 @@ public interface ConstraintBuilder {

/**
* Builds a {@link Constraint} from the constraint stream.
* The constraint will be placed in the {@link Constraint#DEFAULT_CONSTRAINT_GROUP default constraint group}.
* Shorthand for {@link #asConstraint(ConstraintMetadata)}.
*
* @param constraintName shows up in {@link ScoreAnalysis}
* @param id shows up in {@link ScoreAnalysis}
*/
default Constraint asConstraint(String constraintName) {
return asConstraintDescribed(constraintName, "");
}

/**
* As defined by {@link #asConstraintDescribed(String, String, String)},
* placing the constraint in the {@link Constraint#DEFAULT_CONSTRAINT_GROUP default constraint group}.
*
* @param constraintName shows up in {@link ScoreAnalysis}
* @param constraintDescription can contain any character, but it is recommended to keep it short and concise.
*/
default Constraint asConstraintDescribed(String constraintName, String constraintDescription) {
return asConstraintDescribed(constraintName, constraintDescription, Constraint.DEFAULT_CONSTRAINT_GROUP);
default Constraint asConstraint(String id) {
Comment thread
triceo marked this conversation as resolved.
return asConstraint(new DefaultConstraintMetadata(id));
}

/**
* Builds a {@link Constraint} from the constraint stream.
* Both the constraint name and the constraint group are only allowed
* to contain alphanumeric characters, " ", "-" or "_".
* The constraint description can contain any character, but it is recommended to keep it short and concise.
* <p>
* Unlike the constraint name and group,
* the constraint description is unlikely to be used externally as an identifier,
* and therefore doesn't need to be URL-friendly, or protected against injection attacks.
* {@link ConstraintMetadata#id()} is called exactly once at this point;
* the returned value is validated and snapshotted as the constraint's permanent identity.
* Subsequent changes to the description's {@link ConstraintMetadata#id()} return value are ignored.
*
* @param constraintName shows up in {@link ScoreAnalysis}
* @param constraintDescription can contain any character, but it is recommended to keep it short and concise.
* @param constraintGroup not used by the solver directly, but may be used by external tools to group constraints together,
* such as by their source or by their purpose
* @param metadata identifies and describes the constraint;
* {@link ConstraintMetadata#id()} shows up in {@link ScoreAnalysis}
*/
Constraint asConstraintDescribed(String constraintName, String constraintDescription, String constraintGroup);
Constraint asConstraint(ConstraintMetadata metadata);

}
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
package ai.timefold.solver.core.api.score.stream;

import java.util.Collection;
import java.util.Set;

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

/**
* Provides information about the known constraints.
* Works in combination with {@link ConstraintProvider}.
*/
@NullMarked
public interface ConstraintMetaModel {

/**
Expand All @@ -18,30 +18,23 @@ public interface ConstraintMetaModel {
* @return null if such constraint does not exist
*/
@Nullable
Constraint getConstraint(@NonNull ConstraintRef constraintRef);
Constraint getConstraint(ConstraintRef constraintRef);

/**
* Returns all constraints defined in the {@link ConstraintProvider}.
* Returns the constraint with the given id.
* Convenience shorthand for {@link #getConstraint(ConstraintRef)}.
*
* @return iteration order is undefined
*/
@NonNull
Collection<Constraint> getConstraints();

/**
* Returns all constraints from {@link #getConstraints()} that belong to the given group.
*
* @return iteration order is undefined
* @return null if such constraint does not exist
*/
@NonNull
Collection<Constraint> getConstraintsPerGroup(@NonNull String constraintGroup);
default @Nullable Constraint getConstraint(String id) {
return getConstraint(ConstraintRef.of(id));
}

/**
* Returns constraint groups with at least one constraint in it.
* Returns all constraints defined in the {@link ConstraintProvider}.
*
* @return iteration order is undefined
*/
@NonNull
Set<String> getConstraintGroups();
Collection<Constraint> getConstraints();

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package ai.timefold.solver.core.api.score.stream;

import org.jspecify.annotations.NullMarked;

/**
* Identifies a {@link Constraint} and optionally carries metadata about it.
* Users implement this interface, adding any fields they require.
* <p>
* <strong>Immutability contract:</strong> {@link #id()} must return the same value
* for the lifetime of the object.
* The first value returned is snapshotted
* when the constraint is built via {@link ConstraintBuilder#asConstraint(ConstraintMetadata)};
* any later change to the return value of {@link #id()} will be silently ignored
* and will NOT affect the constraint's identity.
*/
@NullMarked
public interface ConstraintMetadata {

/**
* Returns the unique identifier of the constraint.
* Must be non-null, non-empty, and stable (see class Javadoc).
*
* @return the constraint's unique identifier
*/
String id();

}
Original file line number Diff line number Diff line change
Expand Up @@ -9,24 +9,24 @@
* <p>
* If you need an instance created, use {@link ConstraintRef#of(String)} and not the record's constructors.
*
* @param constraintName The constraint name. It must be unique.
* @param id The constraint id. It must be unique.
*/
@NullMarked
public record ConstraintRef(String constraintName)
public record ConstraintRef(String id)
implements
Comparable<ConstraintRef> {

public static ConstraintRef of(String constraintName) {
return new ConstraintRef(constraintName);
public static ConstraintRef of(String id) {
return new ConstraintRef(id);
}

public ConstraintRef {
constraintName = AbstractConstraintBuilder.sanitize("constraintName", constraintName);
id = AbstractConstraintBuilder.sanitize("id", id);
}

@Override
public int compareTo(ConstraintRef other) {
return constraintName.compareTo(other.constraintName);
return id.compareTo(other.id);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
import ai.timefold.solver.core.api.score.Score;
import ai.timefold.solver.core.api.score.stream.Constraint;
import ai.timefold.solver.core.api.score.stream.ConstraintFactory;
import ai.timefold.solver.core.api.score.stream.ConstraintRef;
import ai.timefold.solver.core.api.score.stream.ConstraintMetadata;
import ai.timefold.solver.core.api.score.stream.ConstraintStream;
import ai.timefold.solver.core.api.score.stream.bi.BiConstraintStream;
import ai.timefold.solver.core.api.score.stream.quad.QuadConstraintStream;
Expand Down Expand Up @@ -104,14 +104,13 @@
}
}

protected <Score_ extends Score<Score_>> Constraint buildConstraint(String constraintName, String description,
String constraintGroup, Score_ constraintWeight, ScoreImpactType impactType, Object justificationFunction,
protected <Score_ extends Score<Score_>> Constraint buildConstraint(ConstraintMetadata description,
Score_ constraintWeight, ScoreImpactType impactType, Object justificationFunction,
BavetScoringConstraintStream<Solution_> stream) {
var resolvedJustificationMapping =
Objects.requireNonNullElseGet(justificationFunction, this::getDefaultJustificationMapping);
var isConstraintWeightConfigurable = constraintWeight == null;
var constraintRef = ConstraintRef.of(constraintName);
var constraint = new BavetConstraint<>(constraintFactory, constraintRef, description, constraintGroup,
var constraint = new BavetConstraint<>(constraintFactory, description,

Check warning on line 113 in core/src/main/java/ai/timefold/solver/core/impl/bavet/common/BavetAbstractConstraintStream.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Annotate the parameter with @javax.annotation.Nullable in constructor declaration, or make sure that null can not be passed as argument.

See more on https://sonarcloud.io/project/issues?id=ai.timefold%3Atimefold-solver&issues=AZ2R1GX00vWJv1eB1_Z6&open=AZ2R1GX00vWJv1eB1_Z6&pullRequest=2234
isConstraintWeightConfigurable ? null : constraintWeight, impactType, resolvedJustificationMapping,
stream);
stream.setConstraint(constraint);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,7 @@ private static <Solution_> String getMetadata(GraphSink<Solution_> sink, Solutio
var constraint = sink.constraint();
var metadata = getBaseDOTProperties("#3423a6", true);
metadata.put("label", "<B>%s</B><BR />(Weight: %s)"
.formatted(constraint.getConstraintRef().constraintName(), constraint.extractConstraintWeight(solution)));
.formatted(constraint.getConstraintRef().id(), constraint.extractConstraintWeight(solution)));
return mergeMetadata(metadata);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,12 @@ public DefaultConstraintWeightOverrides(Map<String, Score_> constraintWeightMap)
}

@Override
public @Nullable Score_ getConstraintWeight(String constraintName) {
return constraintWeightMap.get(constraintName);
public @Nullable Score_ getConstraintWeight(String constraintId) {
return constraintWeightMap.get(constraintId);
}

@Override
public Set<String> getKnownConstraintNames() {
public Set<String> getKnownConstraintIds() {
return Collections.unmodifiableSet(constraintWeightMap.keySet());
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,19 +62,19 @@ private ConstraintWeightSupplier(SolutionDescriptor<Solution_> solutionDescripto
* @param userDefinedConstraints never null
*/
public void validate(@Nullable Solution_ workingSolution, Set<ConstraintRef> userDefinedConstraints) {
var userDefinedConstraintNames =
userDefinedConstraints.stream().map(ConstraintRef::constraintName).collect(Collectors.toSet());
var userDefinedConstraintIds =
userDefinedConstraints.stream().map(ConstraintRef::id).collect(Collectors.toSet());
// Constraint verifier is known to cause null here.
var overrides = workingSolution == null ? ConstraintWeightOverrides.none()
: Objects.requireNonNull(getConstraintWeights(workingSolution));
var supportedConstraints = overrides.getKnownConstraintNames();
var supportedConstraints = overrides.getKnownConstraintIds();
var excessiveConstraints = supportedConstraints.stream()
.filter(constraintName -> !userDefinedConstraintNames.contains(constraintName)).collect(Collectors.toSet());
.filter(constraintId -> !userDefinedConstraintIds.contains(constraintId)).collect(Collectors.toSet());
if (!excessiveConstraints.isEmpty()) {
throw new IllegalStateException("""
The constraint weight overrides contain the following constraints (%s) \
that are not in the user-defined constraints (%s).
Maybe check your %s for missing constraints.""".formatted(excessiveConstraints, userDefinedConstraintNames,
Maybe check your %s for missing constraints.""".formatted(excessiveConstraints, userDefinedConstraintIds,
ConstraintProvider.class.getSimpleName()));
}
// Constraints are allowed to be missing; the default value provided by the ConstraintProvider will be used.
Expand Down Expand Up @@ -107,7 +107,7 @@ public Class<? extends ConstraintWeightOverrides<Score_>> getProblemFactClass()
if (workingSolution == null) { // ConstraintVerifier is known to cause null here.
return null;
}
var weight = getConstraintWeights(workingSolution).getConstraintWeight(constraintRef.constraintName());
var weight = getConstraintWeights(workingSolution).getConstraintWeight(constraintRef.id());
if (weight == null) { // This is fine; use default value from ConstraintProvider.
return null;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -188,8 +188,8 @@ private void collectMetrics(LocalSearchStepScope<Solution_> stepScope) {
if (scoreDirector.getConstraintMatchPolicy().isEnabled()) {
for (ConstraintMatchTotal<?> constraintMatchTotal : scoreDirector.getConstraintMatchTotalMap()
.values()) {
var tags = solverScope.getMonitoringTags().and("constraint.name",
constraintMatchTotal.getConstraintRef().constraintName());
var tags = solverScope.getMonitoringTags().and("constraint.id",
constraintMatchTotal.getConstraintRef().id());
collectConstraintMatchTotalMetrics(SolverMetric.CONSTRAINT_MATCH_TOTAL_BEST_SCORE, tags,
constraintMatchTotalTagsToBestCount,
constraintMatchTotalBestScoreMap, constraintMatchTotal, scoreDefinition, solverScope);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ public int compareTo(ConstraintMatch<Score_> other) {

@Override
public String toString() {
return "%s/%s=%s".formatted(getConstraintRef().constraintName(), justification, score);
return "%s/%s=%s".formatted(getConstraintRef().id(), justification, score);
}

}
Loading
Loading