Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add new adaptive termination type #1313

Merged
merged 14 commits into from
Jan 23, 2025
Merged
Show file tree
Hide file tree
Changes from 8 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
48 changes: 48 additions & 0 deletions benchmark/src/main/resources/benchmark.xsd
Original file line number Diff line number Diff line change
Expand Up @@ -533,6 +533,9 @@
<xs:element minOccurs="0" name="terminationCompositionStyle" type="tns:terminationCompositionStyle"/>


<xs:element minOccurs="0" name="adaptive" type="tns:adaptiveTerminationConfig"/>


<xs:element minOccurs="0" name="spentLimit" type="xs:string"/>


Expand Down Expand Up @@ -605,6 +608,51 @@
</xs:complexType>


<xs:complexType name="adaptiveTerminationConfig">


<xs:complexContent>


<xs:extension base="tns:abstractConfig">


<xs:sequence>


<xs:element minOccurs="0" name="gracePeriod" type="xs:string"/>


<xs:element minOccurs="0" name="gracePeriodMilliseconds" type="xs:long"/>


<xs:element minOccurs="0" name="gracePeriodSeconds" type="xs:long"/>


<xs:element minOccurs="0" name="gracePeriodMinutes" type="xs:long"/>


<xs:element minOccurs="0" name="gracePeriodHours" type="xs:long"/>


<xs:element minOccurs="0" name="gracePeriodDays" type="xs:long"/>


<xs:element minOccurs="0" name="minimumImprovementRatio" type="xs:double"/>


</xs:sequence>


</xs:extension>


</xs:complexContent>


</xs:complexType>


<xs:complexType name="constructionHeuristicPhaseConfig">


Expand Down
11 changes: 11 additions & 0 deletions core/src/build/revapi-differences.json
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,17 @@
"oldValue": "{\"environmentMode\", \"daemon\", \"randomType\", \"randomSeed\", \"randomFactoryClass\", \"moveThreadCount\", \"moveThreadBufferSize\", \"threadFactoryClass\", \"monitoringConfig\", \"solutionClass\", \"entityClassList\", \"domainAccessType\", \"scoreDirectorFactoryConfig\", \"terminationConfig\", \"phaseConfigList\"}",
"newValue": "{\"enablePreviewFeatureSet\", \"environmentMode\", \"daemon\", \"randomType\", \"randomSeed\", \"randomFactoryClass\", \"moveThreadCount\", \"moveThreadBufferSize\", \"threadFactoryClass\", \"monitoringConfig\", \"solutionClass\", \"entityClassList\", \"domainAccessType\", \"scoreDirectorFactoryConfig\", \"terminationConfig\", \"nearbyDistanceMeterClass\", \"phaseConfigList\"}",
"justification": "Enable features preview config"
},
{
"ignore": true,
"code": "java.annotation.attributeValueChanged",
"old": "class ai.timefold.solver.core.config.solver.termination.TerminationConfig",
"new": "class ai.timefold.solver.core.config.solver.termination.TerminationConfig",
"annotationType": "jakarta.xml.bind.annotation.XmlType",
"attribute": "propOrder",
"oldValue": "{\"terminationClass\", \"terminationCompositionStyle\", \"spentLimit\", \"millisecondsSpentLimit\", \"secondsSpentLimit\", \"minutesSpentLimit\", \"hoursSpentLimit\", \"daysSpentLimit\", \"unimprovedSpentLimit\", \"unimprovedMillisecondsSpentLimit\", \"unimprovedSecondsSpentLimit\", \"unimprovedMinutesSpentLimit\", \"unimprovedHoursSpentLimit\", \"unimprovedDaysSpentLimit\", \"unimprovedScoreDifferenceThreshold\", \"bestScoreLimit\", \"bestScoreFeasible\", \"stepCountLimit\", \"unimprovedStepCountLimit\", \"scoreCalculationCountLimit\", \"terminationConfigList\"}",
"newValue": "{\"terminationClass\", \"terminationCompositionStyle\", \"adaptiveTerminationConfig\", \"spentLimit\", \"millisecondsSpentLimit\", \"secondsSpentLimit\", \"minutesSpentLimit\", \"hoursSpentLimit\", \"daysSpentLimit\", \"unimprovedSpentLimit\", \"unimprovedMillisecondsSpentLimit\", \"unimprovedSecondsSpentLimit\", \"unimprovedMinutesSpentLimit\", \"unimprovedHoursSpentLimit\", \"unimprovedDaysSpentLimit\", \"unimprovedScoreDifferenceThreshold\", \"bestScoreLimit\", \"bestScoreFeasible\", \"stepCountLimit\", \"unimprovedStepCountLimit\", \"scoreCalculationCountLimit\", \"moveCountLimit\", \"terminationConfigList\"}",
"justification": "Added support for the new adaptive termination type"
}
]
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,209 @@
package ai.timefold.solver.core.config.solver.termination;

import static ai.timefold.solver.core.config.solver.termination.TerminationConfig.requireNonNegative;

import java.time.Duration;
import java.util.function.Consumer;

import jakarta.xml.bind.annotation.XmlType;
import jakarta.xml.bind.annotation.adapters.XmlJavaTypeAdapter;

import ai.timefold.solver.core.config.AbstractConfig;
import ai.timefold.solver.core.config.util.ConfigUtils;
import ai.timefold.solver.core.impl.io.jaxb.adapter.JaxbDurationAdapter;

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

@XmlType(propOrder = {
"gracePeriod",
"gracePeriodMilliseconds",
"gracePeriodSeconds",
"gracePeriodMinutes",
"gracePeriodHours",
"gracePeriodDays",
"minimumImprovementRatio"
})
public class AdaptiveTerminationConfig extends AbstractConfig<AdaptiveTerminationConfig> {
@XmlJavaTypeAdapter(JaxbDurationAdapter.class)
private Duration gracePeriod = null;
private Long gracePeriodMilliseconds = null;
private Long gracePeriodSeconds = null;
private Long gracePeriodMinutes = null;
private Long gracePeriodHours = null;
private Long gracePeriodDays = null;

private Double minimumImprovementRatio = null;

public @Nullable Duration getGracePeriod() {
return gracePeriod;
}

public void setGracePeriod(@Nullable Duration gracePeriod) {
this.gracePeriod = gracePeriod;
}

public @Nullable Long getGracePeriodMilliseconds() {
return gracePeriodMilliseconds;
}

public void setGracePeriodMilliseconds(@Nullable Long gracePeriodMilliseconds) {
this.gracePeriodMilliseconds = gracePeriodMilliseconds;
}

public @Nullable Long getGracePeriodSeconds() {
return gracePeriodSeconds;
}

public void setGracePeriodSeconds(@Nullable Long gracePeriodSeconds) {
this.gracePeriodSeconds = gracePeriodSeconds;
}

public @Nullable Long getGracePeriodMinutes() {
return gracePeriodMinutes;
}

public void setGracePeriodMinutes(@Nullable Long gracePeriodMinutes) {
this.gracePeriodMinutes = gracePeriodMinutes;
}

public @Nullable Long getGracePeriodHours() {
return gracePeriodHours;
}

public void setGracePeriodHours(@Nullable Long gracePeriodHours) {
this.gracePeriodHours = gracePeriodHours;
}

public @Nullable Long getGracePeriodDays() {
return gracePeriodDays;
}

public void setGracePeriodDays(@Nullable Long gracePeriodDays) {
this.gracePeriodDays = gracePeriodDays;
}

public @Nullable Double getMinimumImprovementRatio() {
return minimumImprovementRatio;
}

public void setMinimumImprovementRatio(@Nullable Double minimumImprovementRatio) {
this.minimumImprovementRatio = minimumImprovementRatio;
}

// ************************************************************************
// With methods
// ************************************************************************
@NonNull
public AdaptiveTerminationConfig withGracePeriod(@NonNull Duration gracePeriod) {
this.gracePeriod = gracePeriod;
return this;
}

@NonNull
public AdaptiveTerminationConfig withGracePeriodMilliseconds(@NonNull Long gracePeriodMilliseconds) {
this.gracePeriodMilliseconds = gracePeriodMilliseconds;
return this;
}

@NonNull
public AdaptiveTerminationConfig withGracePeriodSeconds(@NonNull Long gracePeriodSeconds) {
this.gracePeriodSeconds = gracePeriodSeconds;
return this;
}

@NonNull
public AdaptiveTerminationConfig withGracePeriodMinutes(@NonNull Long gracePeriodMinutes) {
this.gracePeriodMinutes = gracePeriodMinutes;
return this;
}

@NonNull
public AdaptiveTerminationConfig withGracePeriodHours(@NonNull Long gracePeriodHours) {
this.gracePeriodHours = gracePeriodHours;
return this;
}

@NonNull
public AdaptiveTerminationConfig withGracePeriodDays(@NonNull Long gracePeriodDays) {
this.gracePeriodDays = gracePeriodDays;
return this;
}

@NonNull
public AdaptiveTerminationConfig withMinimumImprovementRatio(@NonNull Double minimumImprovementRatio) {
this.minimumImprovementRatio = minimumImprovementRatio;
return this;
}

// Complex methods
@Override
public @NonNull AdaptiveTerminationConfig inherit(@NonNull AdaptiveTerminationConfig inheritedConfig) {
if (!gracePeriodIsSet()) {
inheritGracePeriod(inheritedConfig);
}
minimumImprovementRatio = ConfigUtils.inheritOverwritableProperty(minimumImprovementRatio,
inheritedConfig.getMinimumImprovementRatio());
return this;
}

private void inheritGracePeriod(@NonNull AdaptiveTerminationConfig parent) {
gracePeriod = ConfigUtils.inheritOverwritableProperty(gracePeriod, parent.getGracePeriod());
gracePeriodMilliseconds = ConfigUtils.inheritOverwritableProperty(gracePeriodMilliseconds,
parent.getGracePeriodMilliseconds());
gracePeriodSeconds = ConfigUtils.inheritOverwritableProperty(gracePeriodSeconds, parent.getGracePeriodSeconds());
gracePeriodMinutes = ConfigUtils.inheritOverwritableProperty(gracePeriodMinutes, parent.getGracePeriodMinutes());
gracePeriodHours = ConfigUtils.inheritOverwritableProperty(gracePeriodHours, parent.getGracePeriodHours());
gracePeriodDays = ConfigUtils.inheritOverwritableProperty(gracePeriodDays, parent.getGracePeriodDays());
}

@Override
public @NonNull AdaptiveTerminationConfig copyConfig() {
return new AdaptiveTerminationConfig().inherit(this);
}

@Override
public void visitReferencedClasses(@NonNull Consumer<Class<?>> classVisitor) {
// intentionally empty - no classes to visit
}

/** Check whether any gracePeriod... is non-null. */
public boolean gracePeriodIsSet() {
return gracePeriod != null
|| gracePeriodMilliseconds != null
|| gracePeriodSeconds != null
|| gracePeriodMinutes != null
|| gracePeriodHours != null
|| gracePeriodDays != null;
}

public @Nullable Long calculateGracePeriodMilliseconds() {
if (gracePeriodMilliseconds == null && gracePeriodSeconds == null
&& gracePeriodMinutes == null && gracePeriodHours == null
&& gracePeriodDays == null) {
if (gracePeriod != null) {
if (gracePeriod.getNano() % 1000 != 0) {
throw new IllegalArgumentException("The termination gracePeriod (" + gracePeriod
+ ") cannot use nanoseconds.");
}
return gracePeriod.toMillis();
}
return null;
}
if (gracePeriod != null) {
throw new IllegalArgumentException("The termination gracePeriod (" + gracePeriod
+ ") cannot be combined with gracePeriodMilliseconds (" + gracePeriodMilliseconds
+ "), gracePeriodSeconds (" + gracePeriodSeconds
+ "), gracePeriodMinutes (" + gracePeriodMinutes
+ "), gracePeriodHours (" + gracePeriodHours + "),"
+ ") or gracePeriodDays (" + gracePeriodDays + ").");
}
long gracePeriodMillis = 0L
+ requireNonNegative(gracePeriodMilliseconds, "gracePeriodMilliseconds")
+ requireNonNegative(gracePeriodSeconds, "gracePeriodSeconds") * 1000L
+ requireNonNegative(gracePeriodMinutes, "gracePeriodMinutes") * 60_000L
+ requireNonNegative(gracePeriodHours, "gracePeriodHours") * 3_600_000L
+ requireNonNegative(gracePeriodDays, "gracePeriodDays") * 86_400_000L;
return gracePeriodMillis;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
@XmlType(propOrder = {
"terminationClass",
"terminationCompositionStyle",
"adaptiveTerminationConfig",
"spentLimit",
"millisecondsSpentLimit",
"secondsSpentLimit",
Expand Down Expand Up @@ -51,6 +52,9 @@ public class TerminationConfig extends AbstractConfig<TerminationConfig> {

private TerminationCompositionStyle terminationCompositionStyle = null;

@XmlElement(name = "adaptive")
private AdaptiveTerminationConfig adaptiveTerminationConfig = null;

@XmlJavaTypeAdapter(JaxbDurationAdapter.class)
private Duration spentLimit = null;
private Long millisecondsSpentLimit = null;
Expand Down Expand Up @@ -105,6 +109,15 @@ public void setTerminationCompositionStyle(@Nullable TerminationCompositionStyle
this.terminationCompositionStyle = terminationCompositionStyle;
}

public AdaptiveTerminationConfig getAdaptiveTerminationConfig() {
return adaptiveTerminationConfig;
}

public void setAdaptiveTerminationConfig(
AdaptiveTerminationConfig adaptiveTerminationConfig) {
this.adaptiveTerminationConfig = adaptiveTerminationConfig;
}

public @Nullable Duration getSpentLimit() {
return spentLimit;
}
Expand Down Expand Up @@ -284,6 +297,12 @@ public TerminationConfig withTerminationClass(Class<? extends Termination> termi
return this;
}

public @NonNull TerminationConfig
withAdaptiveTerminationConfig(@NonNull AdaptiveTerminationConfig adaptiveTerminationConfig) {
this.adaptiveTerminationConfig = adaptiveTerminationConfig;
return this;
}

public @NonNull TerminationConfig withSpentLimit(@NonNull Duration spentLimit) {
this.spentLimit = spentLimit;
return this;
Expand Down Expand Up @@ -447,7 +466,8 @@ public void overwriteUnimprovedSpentLimit(@Nullable Duration unimprovedSpentLimi

public @Nullable Long calculateUnimprovedTimeMillisSpentLimit() {
if (unimprovedMillisecondsSpentLimit == null && unimprovedSecondsSpentLimit == null
&& unimprovedMinutesSpentLimit == null && unimprovedHoursSpentLimit == null) {
&& unimprovedMinutesSpentLimit == null && unimprovedHoursSpentLimit == null
&& unimprovedDaysSpentLimit == null) {
if (unimprovedSpentLimit != null) {
if (unimprovedSpentLimit.getNano() % 1000 != 0) {
throw new IllegalArgumentException("The termination unimprovedSpentLimit (" + unimprovedSpentLimit
Expand All @@ -462,7 +482,8 @@ public void overwriteUnimprovedSpentLimit(@Nullable Duration unimprovedSpentLimi
+ ") cannot be combined with unimprovedMillisecondsSpentLimit (" + unimprovedMillisecondsSpentLimit
+ "), unimprovedSecondsSpentLimit (" + unimprovedSecondsSpentLimit
+ "), unimprovedMinutesSpentLimit (" + unimprovedMinutesSpentLimit
+ "), unimprovedHoursSpentLimit (" + unimprovedHoursSpentLimit + ").");
+ "), unimprovedHoursSpentLimit (" + unimprovedHoursSpentLimit + "),"
+ ") or unimprovedDaysSpentLimit (" + unimprovedDaysSpentLimit + ").");
}
long unimprovedTimeMillisSpentLimit = 0L
+ requireNonNegative(unimprovedMillisecondsSpentLimit, "unimprovedMillisecondsSpentLimit")
Expand All @@ -483,6 +504,7 @@ public boolean isConfigured() {
return terminationClass != null ||
timeSpentLimitIsSet() ||
unimprovedTimeSpentLimitIsSet() ||
adaptiveTerminationConfig != null ||
bestScoreLimit != null ||
bestScoreFeasible != null ||
stepCountLimit != null ||
Expand Down Expand Up @@ -511,6 +533,8 @@ private boolean isTerminationListConfigured() {
if (!unimprovedTimeSpentLimitIsSet()) {
inheritUnimprovedTimeSpentLimit(inheritedConfig);
}
adaptiveTerminationConfig = ConfigUtils.inheritConfig(adaptiveTerminationConfig,
inheritedConfig.getAdaptiveTerminationConfig());
terminationClass = ConfigUtils.inheritOverwritableProperty(terminationClass,
inheritedConfig.getTerminationClass());
terminationCompositionStyle = ConfigUtils.inheritOverwritableProperty(terminationCompositionStyle,
Expand Down Expand Up @@ -545,6 +569,9 @@ public void visitReferencedClasses(@NonNull Consumer<Class<?>> classVisitor) {
if (terminationConfigList != null) {
terminationConfigList.forEach(tc -> tc.visitReferencedClasses(classVisitor));
}
if (adaptiveTerminationConfig != null) {
adaptiveTerminationConfig.visitReferencedClasses(classVisitor);
}
}

private TerminationConfig inheritTimeSpentLimit(TerminationConfig parent) {
Expand Down Expand Up @@ -587,7 +614,7 @@ private TerminationConfig inheritUnimprovedTimeSpentLimit(TerminationConfig pare
* @param name the name of the parameter, for use in the exception message
* @throws IllegalArgumentException iff param is negative
*/
private Long requireNonNegative(Long param, String name) {
static Long requireNonNegative(Long param, String name) {
if (param == null) {
return 0L; // Makes adding a null param a NOP.
} else if (param < 0L) {
Expand Down
Loading
Loading