Skip to content

Commit f09a244

Browse files
committed
fix: prevent excessive logging of constraint weights
1 parent 3a82714 commit f09a244

File tree

14 files changed

+106
-62
lines changed

14 files changed

+106
-62
lines changed

core/src/main/java/ai/timefold/solver/core/impl/score/director/AbstractScoreDirector.java

Lines changed: 12 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -247,17 +247,12 @@ protected void setWorkingEntityListDirty() {
247247
workingEntityListRevision++;
248248
}
249249

250-
@Override
251-
public Solution_ cloneWorkingSolution() {
252-
return cloneSolution(workingSolution);
253-
}
254-
255250
@Override
256251
public Solution_ cloneSolution(Solution_ originalSolution) {
257252
SolutionDescriptor<Solution_> solutionDescriptor = getSolutionDescriptor();
258-
Score_ originalScore = (Score_) solutionDescriptor.getScore(originalSolution);
253+
Score_ originalScore = solutionDescriptor.getScore(originalSolution);
259254
Solution_ cloneSolution = solutionDescriptor.getSolutionCloner().cloneSolution(originalSolution);
260-
Score_ cloneScore = (Score_) solutionDescriptor.getScore(cloneSolution);
255+
Score_ cloneScore = solutionDescriptor.getScore(cloneSolution);
261256
if (scoreDirectorFactory.isAssertClonedSolution()) {
262257
if (!Objects.equals(originalScore, cloneScore)) {
263258
throw new IllegalStateException("Cloning corruption: "
@@ -297,33 +292,29 @@ protected void setCalculatedScore(Score_ score) {
297292
calculationCount++;
298293
}
299294

295+
/**
296+
* @deprecated Unused, but kept for backward compatibility.
297+
*/
298+
@Deprecated(forRemoval = true, since = "1.14.0")
300299
@Override
301300
public AbstractScoreDirector<Solution_, Score_, Factory_> clone() {
302-
// Breaks incremental score calculation.
303-
// Subclasses should overwrite this method to avoid breaking it if possible.
304-
AbstractScoreDirector<Solution_, Score_, Factory_> clone =
305-
(AbstractScoreDirector<Solution_, Score_, Factory_>) scoreDirectorFactory
306-
.buildScoreDirector(lookUpEnabled, constraintMatchEnabledPreference);
307-
clone.setWorkingSolution(cloneWorkingSolution());
308-
return clone;
301+
throw new UnsupportedOperationException("Cloning score directors is not supported.");
309302
}
310303

311304
@Override
312305
public InnerScoreDirector<Solution_, Score_> createChildThreadScoreDirector(ChildThreadType childThreadType) {
313306
if (childThreadType == ChildThreadType.PART_THREAD) {
314-
AbstractScoreDirector<Solution_, Score_, Factory_> childThreadScoreDirector =
315-
(AbstractScoreDirector<Solution_, Score_, Factory_>) scoreDirectorFactory
316-
.buildScoreDirector(lookUpEnabled, constraintMatchEnabledPreference);
307+
var childThreadScoreDirector = (AbstractScoreDirector<Solution_, Score_, Factory_>) scoreDirectorFactory
308+
.buildDerivedScoreDirector(lookUpEnabled, constraintMatchEnabledPreference);
317309
// ScoreCalculationCountTermination takes into account previous phases
318310
// but the calculationCount of partitions is maxed, not summed.
319311
childThreadScoreDirector.calculationCount = calculationCount;
320312
return childThreadScoreDirector;
321313
} else if (childThreadType == ChildThreadType.MOVE_THREAD) {
322314
// TODO The move thread must use constraintMatchEnabledPreference in FULL_ASSERT,
323315
// but it doesn't have to for Indictment Local Search, in which case it is a performance loss
324-
AbstractScoreDirector<Solution_, Score_, Factory_> childThreadScoreDirector =
325-
(AbstractScoreDirector<Solution_, Score_, Factory_>) scoreDirectorFactory
326-
.buildScoreDirector(true, constraintMatchEnabledPreference);
316+
var childThreadScoreDirector = (AbstractScoreDirector<Solution_, Score_, Factory_>) scoreDirectorFactory
317+
.buildDerivedScoreDirector(true, constraintMatchEnabledPreference);
327318
childThreadScoreDirector.setWorkingSolution(cloneWorkingSolution());
328319
return childThreadScoreDirector;
329320
} else {
@@ -593,7 +584,7 @@ private void assertScoreFromScratch(Score_ score, Object completedAction, boolea
593584
if (assertionScoreDirectorFactory == null) {
594585
assertionScoreDirectorFactory = scoreDirectorFactory;
595586
}
596-
try (var uncorruptedScoreDirector = assertionScoreDirectorFactory.buildScoreDirector(false, true)) {
587+
try (var uncorruptedScoreDirector = assertionScoreDirectorFactory.buildDerivedScoreDirector(false, true)) {
597588
uncorruptedScoreDirector.setWorkingSolution(workingSolution);
598589
Score_ uncorruptedScore = uncorruptedScoreDirector.calculateScore();
599590
if (!score.equals(uncorruptedScore)) {

core/src/main/java/ai/timefold/solver/core/impl/score/director/AbstractScoreDirectorFactory.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -100,8 +100,8 @@ public InnerScoreDirector<Solution_, Score_> buildScoreDirector() {
100100
@Override
101101
public void assertScoreFromScratch(Solution_ solution) {
102102
// Get the score before uncorruptedScoreDirector.calculateScore() modifies it
103-
Score_ score = (Score_) getSolutionDescriptor().getScore(solution);
104-
try (InnerScoreDirector<Solution_, Score_> uncorruptedScoreDirector = buildScoreDirector(false, true)) {
103+
Score_ score = getSolutionDescriptor().getScore(solution);
104+
try (var uncorruptedScoreDirector = buildDerivedScoreDirector(false, true)) {
105105
uncorruptedScoreDirector.setWorkingSolution(solution);
106106
Score_ uncorruptedScore = uncorruptedScoreDirector.calculateScore();
107107
if (!score.equals(uncorruptedScore)) {

core/src/main/java/ai/timefold/solver/core/impl/score/director/InnerScoreDirector.java

Lines changed: 3 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -203,7 +203,9 @@ default Score_ doAndProcessMove(Move<Solution_> move, boolean assertMoveScoreFro
203203
*
204204
* @return never null, planning clone
205205
*/
206-
Solution_ cloneWorkingSolution();
206+
default Solution_ cloneWorkingSolution() {
207+
return cloneSolution(getWorkingSolution());
208+
}
207209

208210
/**
209211
* Returns a planning clone of the solution,
@@ -228,17 +230,6 @@ default Score_ doAndProcessMove(Move<Solution_> move, boolean assertMoveScoreFro
228230
*/
229231
SupplyManager getSupplyManager();
230232

231-
/**
232-
* Clones this {@link ScoreDirector} and its {@link PlanningSolution working solution}.
233-
* Use {@link #getWorkingSolution()} to retrieve the {@link PlanningSolution working solution} of that clone.
234-
* <p>
235-
* This is heavy method, because it usually breaks incremental score calculation. Use it sparingly.
236-
* Therefore it's best to clone lazily by delaying the clone call as long as possible.
237-
*
238-
* @return never null
239-
*/
240-
InnerScoreDirector<Solution_, Score_> clone();
241-
242233
InnerScoreDirector<Solution_, Score_> createChildThreadScoreDirector(ChildThreadType childThreadType);
243234

244235
/**

core/src/main/java/ai/timefold/solver/core/impl/score/director/InnerScoreDirectorFactory.java

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,25 @@ default InnerScoreDirector<Solution_, Score_> buildScoreDirector(boolean lookUpE
6262
InnerScoreDirector<Solution_, Score_> buildScoreDirector(boolean lookUpEnabled, boolean constraintMatchEnabledPreference,
6363
boolean expectShadowVariablesInCorrectState);
6464

65+
/**
66+
* Like {@link #buildScoreDirector(boolean, boolean)}, but makes the score director a derived one.
67+
* Derived score directors may make choices which the main score director can not make, such as reducing logging.
68+
* Derived score directors are typically used for multithreaded solving, testing and assert modes.
69+
*
70+
* @param lookUpEnabled true if a {@link ScoreDirector} implementation should track all working objects
71+
* for {@link ScoreDirector#lookUpWorkingObject(Object)}
72+
* @param constraintMatchEnabledPreference false if a {@link ScoreDirector} implementation
73+
* should not do {@link ConstraintMatch} tracking even if it supports it.
74+
* @return never null
75+
* @see InnerScoreDirector#isConstraintMatchEnabled()
76+
* @see InnerScoreDirector#getConstraintMatchTotalMap()
77+
*/
78+
default InnerScoreDirector<Solution_, Score_> buildDerivedScoreDirector(boolean lookUpEnabled,
79+
boolean constraintMatchEnabledPreference) {
80+
// Most score directors don't need derived status; CS will override this.
81+
return buildScoreDirector(lookUpEnabled, constraintMatchEnabledPreference, true);
82+
}
83+
6584
/**
6685
* @return never null
6786
*/

core/src/main/java/ai/timefold/solver/core/impl/score/director/stream/BavetConstraintStreamScoreDirector.java

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,11 +25,19 @@
2525
public final class BavetConstraintStreamScoreDirector<Solution_, Score_ extends Score<Score_>>
2626
extends AbstractScoreDirector<Solution_, Score_, BavetConstraintStreamScoreDirectorFactory<Solution_, Score_>> {
2727

28+
private final boolean derived;
2829
private BavetConstraintSession<Score_> session;
2930

3031
public BavetConstraintStreamScoreDirector(BavetConstraintStreamScoreDirectorFactory<Solution_, Score_> scoreDirectorFactory,
3132
boolean lookUpEnabled, boolean constraintMatchEnabledPreference, boolean expectShadowVariablesInCorrectState) {
33+
this(scoreDirectorFactory, lookUpEnabled, constraintMatchEnabledPreference, expectShadowVariablesInCorrectState, false);
34+
}
35+
36+
public BavetConstraintStreamScoreDirector(BavetConstraintStreamScoreDirectorFactory<Solution_, Score_> scoreDirectorFactory,
37+
boolean lookUpEnabled, boolean constraintMatchEnabledPreference, boolean expectShadowVariablesInCorrectState,
38+
boolean derived) {
3239
super(scoreDirectorFactory, lookUpEnabled, constraintMatchEnabledPreference, expectShadowVariablesInCorrectState);
40+
this.derived = derived;
3341
}
3442

3543
// ************************************************************************
@@ -38,7 +46,7 @@ public BavetConstraintStreamScoreDirector(BavetConstraintStreamScoreDirectorFact
3846

3947
@Override
4048
public void setWorkingSolution(Solution_ workingSolution) {
41-
session = scoreDirectorFactory.newSession(workingSolution, constraintMatchEnabledPreference);
49+
session = scoreDirectorFactory.newSession(workingSolution, constraintMatchEnabledPreference, derived);
4250
getSolutionDescriptor().visitAll(workingSolution, session::insert);
4351
super.setWorkingSolution(workingSolution);
4452
}

core/src/main/java/ai/timefold/solver/core/impl/score/director/stream/BavetConstraintStreamScoreDirectorFactory.java

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
import ai.timefold.solver.core.config.util.ConfigUtils;
1414
import ai.timefold.solver.core.enterprise.TimefoldSolverEnterpriseService;
1515
import ai.timefold.solver.core.impl.domain.solution.descriptor.SolutionDescriptor;
16+
import ai.timefold.solver.core.impl.score.director.InnerScoreDirector;
1617
import ai.timefold.solver.core.impl.score.stream.bavet.BavetConstraintFactory;
1718
import ai.timefold.solver.core.impl.score.stream.bavet.BavetConstraintSession;
1819
import ai.timefold.solver.core.impl.score.stream.bavet.BavetConstraintSessionFactory;
@@ -72,13 +73,21 @@ public BavetConstraintStreamScoreDirector<Solution_, Score_> buildScoreDirector(
7273
expectShadowVariablesInCorrectState);
7374
}
7475

75-
public BavetConstraintSession<Score_> newSession(Solution_ workingSolution, boolean constraintMatchEnabled) {
76-
return constraintSessionFactory.buildSession(workingSolution, constraintMatchEnabled);
76+
@Override
77+
public InnerScoreDirector<Solution_, Score_> buildDerivedScoreDirector(boolean lookUpEnabled,
78+
boolean constraintMatchEnabledPreference) {
79+
return new BavetConstraintStreamScoreDirector<>(this, lookUpEnabled, constraintMatchEnabledPreference,
80+
true, true);
81+
}
82+
83+
public BavetConstraintSession<Score_> newSession(Solution_ workingSolution, boolean constraintMatchEnabled,
84+
boolean scoreDirectorDerived) {
85+
return constraintSessionFactory.buildSession(workingSolution, constraintMatchEnabled, scoreDirectorDerived);
7786
}
7887

7988
@Override
8089
public AbstractScoreInliner<Score_> fireAndForget(Object... facts) {
81-
var session = newSession(null, true);
90+
var session = newSession(null, true, true);
8291
Arrays.stream(facts).forEach(session::insert);
8392
session.calculateScore(0);
8493
return session.getScoreInliner();

core/src/main/java/ai/timefold/solver/core/impl/score/stream/bavet/BavetConstraintFactory.java

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -65,11 +65,10 @@ public <Stream_ extends BavetAbstractConstraintStream<Solution_>> Stream_ share(
6565

6666
/**
6767
* Enables node sharing.
68-
* If a constraint already exists in this factory, it replaces it by the old copy.
68+
* If a constraint already exists in this factory, it replaces it with the old copy.
6969
* {@link BavetAbstractConstraintStream} implement equals/hashcode ignoring child streams.
7070
* <p>
71-
* {@link BavetConstraintSessionFactory#buildSession(Object, boolean)} relies on this
72-
* occurring for all streams.
71+
* {@link BavetConstraintSessionFactory#buildSession(Object, boolean, boolean)} needs this to happen for all streams.
7372
* <p>
7473
* This must be called before the stream receives child streams.
7574
*

core/src/main/java/ai/timefold/solver/core/impl/score/stream/bavet/BavetConstraintSession.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616

1717
/**
1818
* The type is public to make it easier for Bavet-specific minimal bug reproducers to be created.
19-
* Instances should be created through {@link BavetConstraintStreamScoreDirectorFactory#newSession(Object, boolean)}.
19+
* Instances should be created through {@link BavetConstraintStreamScoreDirectorFactory#newSession(Object, boolean, boolean)}.
2020
*
2121
* @see PropagationQueue Description of the tuple propagation mechanism.
2222
* @param <Score_>

core/src/main/java/ai/timefold/solver/core/impl/score/stream/bavet/BavetConstraintSessionFactory.java

Lines changed: 36 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -31,10 +31,13 @@
3131

3232
import org.slf4j.Logger;
3333
import org.slf4j.LoggerFactory;
34+
import org.slf4j.event.Level;
3435

3536
public final class BavetConstraintSessionFactory<Solution_, Score_ extends Score<Score_>> {
3637

3738
private static final Logger LOGGER = LoggerFactory.getLogger(BavetConstraintSessionFactory.class);
39+
private static final Level CONSTRAINT_WEIGHT_LOGGING_LEVEL = Level.DEBUG;
40+
3841
private final SolutionDescriptor<Solution_> solutionDescriptor;
3942
private final ConstraintLibrary<Score_> constraintLibrary;
4043

@@ -50,7 +53,8 @@ public BavetConstraintSessionFactory(SolutionDescriptor<Solution_> solutionDescr
5053
// ************************************************************************
5154

5255
@SuppressWarnings("unchecked")
53-
public BavetConstraintSession<Score_> buildSession(Solution_ workingSolution, boolean constraintMatchEnabled) {
56+
public BavetConstraintSession<Score_> buildSession(Solution_ workingSolution, boolean constraintMatchEnabled,
57+
boolean scoreDirectorDerived) {
5458
var constraintWeightSupplier = solutionDescriptor.getConstraintWeightSupplier();
5559
if (constraintWeightSupplier != null) { // Fail fast on unknown constraints.
5660
var knownConstraints = constraintLibrary.getConstraints()
@@ -63,16 +67,28 @@ public BavetConstraintSession<Score_> buildSession(Solution_ workingSolution, bo
6367
var zeroScore = scoreDefinition.getZeroScore();
6468
var constraintStreamSet = new LinkedHashSet<BavetAbstractConstraintStream<Solution_>>();
6569
var constraintWeightMap = new HashMap<Constraint, Score_>(constraintLibrary.getConstraints().size());
66-
LOGGER.debug("Constraint weights for solution ({}):", workingSolution);
70+
71+
// Only log constraint weights if logging is enabled; otherwise we don't need to build the string.
72+
var constraintWeightLoggingEnabled = !scoreDirectorDerived && LOGGER.isEnabledForLevel(CONSTRAINT_WEIGHT_LOGGING_LEVEL);
73+
var constraintWeightString = constraintWeightLoggingEnabled
74+
? new StringBuilder("Constraint weights for solution (%s):%n"
75+
.formatted(workingSolution))
76+
: null;
77+
6778
for (var constraint : constraintLibrary.getConstraints()) {
6879
var constraintRef = constraint.getConstraintRef();
6980
var castConstraint = (BavetConstraint<Solution_>) constraint;
7081
var defaultConstraintWeight = castConstraint.getDefaultConstraintWeight();
7182
var constraintWeight = (Score_) castConstraint.extractConstraintWeight(workingSolution);
7283
if (!constraintWeight.equals(zeroScore)) {
73-
if (defaultConstraintWeight != null && !defaultConstraintWeight.equals(constraintWeight)) {
74-
LOGGER.debug(" Constraint ({}) weight overridden to ({}) from ({}).", constraintRef, constraintWeight,
75-
defaultConstraintWeight);
84+
if (constraintWeightLoggingEnabled) {
85+
if (defaultConstraintWeight != null && !defaultConstraintWeight.equals(constraintWeight)) {
86+
constraintWeightString.append(" Constraint (%s) weight overridden to (%s) from (%s).%n"
87+
.formatted(constraintRef, constraintWeight, defaultConstraintWeight));
88+
} else {
89+
constraintWeightString.append(" Constraint (%s) weight set to (%s).%n"
90+
.formatted(constraintRef, constraintWeight));
91+
}
7692
}
7793
/*
7894
* Relies on BavetConstraintFactory#share(Stream_) occurring for all constraint stream instances
@@ -81,18 +97,27 @@ public BavetConstraintSession<Score_> buildSession(Solution_ workingSolution, bo
8197
castConstraint.collectActiveConstraintStreams(constraintStreamSet);
8298
constraintWeightMap.put(constraint, constraintWeight);
8399
} else {
84-
/*
85-
* Filter out nodes that only lead to constraints with zero weight.
86-
* Note: Node sharing happens earlier, in BavetConstraintFactory#share(Stream_).
87-
*/
88-
LOGGER.debug(" Constraint ({}) disabled.", constraintRef);
100+
if (constraintWeightLoggingEnabled) {
101+
/*
102+
* Filter out nodes that only lead to constraints with zero weight.
103+
* Note: Node sharing happens earlier, in BavetConstraintFactory#share(Stream_).
104+
*/
105+
constraintWeightString.append(" Constraint (%s) disabled.%n"
106+
.formatted(constraintRef));
107+
}
89108
}
90109
}
91110

92111
var scoreInliner = AbstractScoreInliner.buildScoreInliner(scoreDefinition, constraintWeightMap, constraintMatchEnabled);
93-
if (constraintStreamSet.isEmpty()) { // All constraints were disabled.
112+
if (constraintStreamSet.isEmpty()) {
113+
LOGGER.warn("No constraints enabled for solution ({}).", workingSolution);
94114
return new BavetConstraintSession<>(scoreInliner);
95115
}
116+
117+
if (constraintWeightLoggingEnabled) {
118+
LOGGER.atLevel(CONSTRAINT_WEIGHT_LOGGING_LEVEL)
119+
.log(constraintWeightString.toString().trim());
120+
}
96121
/*
97122
* Build constraintStreamSet in reverse order to create downstream nodes first
98123
* so every node only has final variables (some of which have downstream node method references).

core/src/main/java/ai/timefold/solver/core/impl/score/stream/common/AbstractConstraintStreamScoreDirectorFactory.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import ai.timefold.solver.core.api.score.Score;
55
import ai.timefold.solver.core.impl.domain.solution.descriptor.SolutionDescriptor;
66
import ai.timefold.solver.core.impl.score.director.AbstractScoreDirectorFactory;
7+
import ai.timefold.solver.core.impl.score.director.InnerScoreDirector;
78
import ai.timefold.solver.core.impl.score.director.ScoreDirectorFactory;
89
import ai.timefold.solver.core.impl.score.stream.common.inliner.AbstractScoreInliner;
910

@@ -35,4 +36,9 @@ protected AbstractConstraintStreamScoreDirectorFactory(SolutionDescriptor<Soluti
3536
public boolean supportsConstraintMatching() {
3637
return true;
3738
}
39+
40+
@Override
41+
public abstract InnerScoreDirector<Solution_, Score_> buildDerivedScoreDirector(boolean lookUpEnabled,
42+
boolean constraintMatchEnabledPreference);
43+
3844
}

0 commit comments

Comments
 (0)