-
Notifications
You must be signed in to change notification settings - Fork 237
feat: Create profile set custom seed #2665
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
Merged
diogodanielsoaresferreira
merged 32 commits into
TimefoldAI:main
from
diogodanielsoaresferreira:create_profile_set_custom_seed
Oct 1, 2026
+380
−19
Merged
Changes from all commits
Commits
Show all changes
32 commits
Select commit
Hold shift + click to select a range
35a3605
feat: Allow custom options in the RunConfiguration
cristianonicolai 2e651b5
feat: store execution profile in metadata
diogodanielsoaresferreira 2b6b5b6
Merge branch 'main' into execution_profiles
diogodanielsoaresferreira 8e22ec4
feat: remove execution profiles from metadata
diogodanielsoaresferreira f6e0240
Merge branch 'execution_profiles' of github.com:diogodanielsoaresferr…
diogodanielsoaresferreira b16cfe5
feat: rename execution profile artifacts to execution artifacts
diogodanielsoaresferreira dcb620a
Merge branch 'main' into execution_profiles
diogodanielsoaresferreira 9c1479c
feat: add execution profiles to solver
diogodanielsoaresferreira ce245cd
feat: Remove default execution profile
diogodanielsoaresferreira a91a294
feat: move ExecutionProfileRegistry to internal package
diogodanielsoaresferreira 29902da
Merge branch 'main' into execution_profiles
diogodanielsoaresferreira a1a595d
feat: remove applicationScoped from execution profile registry and us…
diogodanielsoaresferreira 9b1de8d
Merge branch 'main' into execution_profiles
diogodanielsoaresferreira 09c3c32
feat: Add seed execution profile
diogodanielsoaresferreira 93fa617
feat: add name to execution profiles
diogodanielsoaresferreira b78efb4
feat: add name to execution profiles
diogodanielsoaresferreira cc08d65
Merge branch 'execution_profiles' into create_profile_set_custom_seed
diogodanielsoaresferreira 609e890
Merge #2590 (custom options in RunConfiguration) into create_profile_…
diogodanielsoaresferreira da9707f
feat: execution profile parameters given by the caller
diogodanielsoaresferreira e6d05ae
Merge branch 'main' into create_profile_set_custom_seed
diogodanielsoaresferreira 57fe853
fix: sonarlog issues
diogodanielsoaresferreira 90bb6b2
remove toProperties from execution profiles
diogodanielsoaresferreira 52390fa
Fail if seed parameter is not supplied
diogodanielsoaresferreira 5c806b4
Merge branch 'main' into create_profile_set_custom_seed
diogodanielsoaresferreira 17eea68
Merge branch 'main' into create_profile_set_custom_seed
diogodanielsoaresferreira 1be9439
feat: Update docs
diogodanielsoaresferreira 6d0c846
documentation: add options in documentation
diogodanielsoaresferreira 9fcfeaf
Merge branch 'main' into create_profile_set_custom_seed
diogodanielsoaresferreira d384840
feat: make options in RunConfigurations visible
diogodanielsoaresferreira 831adb5
Merge branch 'main' into create_profile_set_custom_seed
diogodanielsoaresferreira a1e352b
feat: make execution profile options input invisible to client
diogodanielsoaresferreira cbf1d22
Merge branch 'main' into create_profile_set_custom_seed
diogodanielsoaresferreira File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Some comments aren't visible on the classic Files Changed page.
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
59 changes: 59 additions & 0 deletions
59
...ava/ai/timefold/solver/service/definition/impl/executionprofile/SeedExecutionProfile.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,59 @@ | ||
| package ai.timefold.solver.service.definition.impl.executionprofile; | ||
|
|
||
| import java.util.Map; | ||
|
|
||
| import ai.timefold.solver.service.definition.internal.executionprofile.ExecutionProfile; | ||
|
|
||
| /** | ||
| * Runs the solver with a fixed random seed, making a run reproducible. | ||
| * <p> | ||
| * The seed is taken from the required {@code seed} run option; selecting this profile without supplying it is rejected. The | ||
| * profile never invents a seed. The supplied seed is applied by mapping it to the Timefold Quarkus property | ||
| * {@code quarkus.timefold.solver.random-seed} (via its environment-variable form), which the solver pod applies to its | ||
| * {@code SolverConfig} at startup. | ||
| */ | ||
| public final class SeedExecutionProfile implements ExecutionProfile { | ||
|
|
||
| static final String PARAMETER_SEED = "seed"; | ||
|
|
||
| /** | ||
| * Environment-variable form of {@code quarkus.timefold.solver.default.random-seed}, consumed by the Timefold Quarkus | ||
| * extension. The {@code default} segment is the solver name ({@code TimefoldRuntimeConfig.DEFAULT_SOLVER_NAME}); it is | ||
| * correct for the usual single, unnamed solver a model defines. Note this property lives under a solver-name-keyed map, | ||
| * so injecting it purely via an environment variable may not be honored by SmallRye - see the profile's notes. | ||
| */ | ||
| static final String ENV_QUARKUS_RANDOM_SEED = "QUARKUS_TIMEFOLD_SOLVER_DEFAULT_RANDOM_SEED"; | ||
|
|
||
| @Override | ||
| public String id() { | ||
| return "seed"; | ||
| } | ||
|
|
||
| @Override | ||
| public String name() { | ||
| return "Fixed random seed"; | ||
| } | ||
|
|
||
| @Override | ||
| public String description() { | ||
| return "Runs the solver with a fixed random seed for reproducible results. " | ||
| + "The seed must be supplied as the 'seed' run option."; | ||
| } | ||
|
|
||
| @Override | ||
| public Map<String, String> toEnvironment(Map<String, String> options) { | ||
| String seed = options == null ? null : options.get(PARAMETER_SEED); | ||
| if (seed == null) { | ||
| throw new IllegalArgumentException( | ||
| "Execution profile '" + id() + "' requires the '" + PARAMETER_SEED + "' option to be supplied."); | ||
| } | ||
| try { | ||
| Long.parseLong(seed); | ||
| } catch (NumberFormatException e) { | ||
| throw new IllegalArgumentException( | ||
| "Execution profile '" + id() + "' requires option '" + PARAMETER_SEED + "' to be a long, but was: " | ||
| + seed); | ||
| } | ||
| return Map.of(ENV_QUARKUS_RANDOM_SEED, seed); | ||
|
diogodanielsoaresferreira marked this conversation as resolved.
|
||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
1 change: 1 addition & 0 deletions
1
...services/ai.timefold.solver.service.definition.internal.executionprofile.ExecutionProfile
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| ai.timefold.solver.service.definition.impl.executionprofile.SeedExecutionProfile |
150 changes: 150 additions & 0 deletions
150
.../src/test/java/ai/timefold/solver/service/definition/api/domain/RunConfigurationTest.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,150 @@ | ||
| package ai.timefold.solver.service.definition.api.domain; | ||
|
|
||
| import static org.assertj.core.api.Assertions.assertThat; | ||
|
|
||
| import java.time.Duration; | ||
| import java.util.Map; | ||
| import java.util.Set; | ||
|
|
||
| import ai.timefold.solver.service.definition.api.termination.SolverTerminationConfig; | ||
|
|
||
| import org.junit.jupiter.api.Test; | ||
|
|
||
| import com.fasterxml.jackson.core.JsonProcessingException; | ||
| import com.fasterxml.jackson.databind.ObjectMapper; | ||
|
|
||
| class RunConfigurationTest { | ||
|
|
||
| private final ObjectMapper mapper = new ObjectMapper(); | ||
|
|
||
| @Test | ||
| void convenienceConstructorsLeaveOptionsNull() { | ||
| assertThat(new RunConfiguration("dataset", null, 4, Set.of("a")).options()).isNull(); | ||
| assertThat(new RunConfiguration("dataset", null).options()).isNull(); | ||
| assertThat(new RunConfiguration(4, null).options()).isNull(); | ||
| assertThat(new RunConfiguration("dataset").options()).isNull(); | ||
| } | ||
|
|
||
| @Test | ||
| void overrideFillsMissingOptionsFromFallback() { | ||
| RunConfiguration primary = new RunConfiguration("dataset", null, null, Set.of(), null); | ||
| RunConfiguration fallback = new RunConfiguration(null, null, null, Set.of(), Map.of("solver", "fast")); | ||
|
|
||
| RunConfiguration merged = primary.override(fallback); | ||
|
|
||
| assertThat(merged.options()).containsExactlyEntriesOf(Map.of("solver", "fast")); | ||
| } | ||
|
|
||
| @Test | ||
| void overrideKeepsPrimaryOptionsWhenPresent() { | ||
| RunConfiguration primary = new RunConfiguration("dataset", null, null, Set.of(), Map.of("solver", "accurate")); | ||
| RunConfiguration fallback = new RunConfiguration(null, null, null, Set.of(), Map.of("solver", "fast")); | ||
|
|
||
| RunConfiguration merged = primary.override(fallback); | ||
|
|
||
| // Options are replaced wholesale, never merged key-by-key. | ||
| assertThat(merged.options()).containsExactlyEntriesOf(Map.of("solver", "accurate")); | ||
| } | ||
|
|
||
| @Test | ||
| void overrideKeepsPrimaryOptionsWhenPresentWithDisjointFallbackKeys() { | ||
| RunConfiguration primary = new RunConfiguration("dataset", null, null, Set.of(), Map.of("solver", "accurate")); | ||
| RunConfiguration fallback = new RunConfiguration(null, null, null, Set.of(), Map.of("logLevel", "debug")); | ||
|
|
||
| RunConfiguration merged = primary.override(fallback); | ||
|
|
||
| assertThat(merged.options()).containsExactlyEntriesOf(Map.of("solver", "accurate")); | ||
| } | ||
|
|
||
| @Test | ||
| void overrideKeepsPrimaryEmptyOptionsInsteadOfInheriting() { | ||
| RunConfiguration primary = new RunConfiguration("dataset", null, null, Set.of(), Map.of()); | ||
| RunConfiguration fallback = new RunConfiguration(null, null, null, Set.of(), Map.of("solver", "fast")); | ||
|
|
||
| RunConfiguration merged = primary.override(fallback); | ||
|
|
||
| // Only a null options map inherits from the fallback; an empty one is a deliberate "no options". | ||
| assertThat(merged.options()).isEmpty(); | ||
| } | ||
|
|
||
| @Test | ||
| void overrideWithNullConfigurationKeepsOptions() { | ||
| RunConfiguration primary = new RunConfiguration("dataset", null, null, Set.of(), Map.of("solver", "fast")); | ||
|
|
||
| assertThat(primary.override(null).options()).containsExactlyEntriesOf(Map.of("solver", "fast")); | ||
| } | ||
|
|
||
| @Test | ||
| void withTerminationPreservesOptions() { | ||
| RunConfiguration configuration = | ||
| new RunConfiguration("dataset", null, 4, Set.of("nightly"), Map.of("solver", "fast")); | ||
|
|
||
| RunConfiguration copy = configuration.withTermination(new SolverTerminationConfig(Duration.ofMinutes(1), null)); | ||
|
|
||
| assertThat(copy.options()).containsExactlyEntriesOf(Map.of("solver", "fast")); | ||
| assertThat(copy.termination().spentLimit()).isEqualTo(Duration.ofMinutes(1)); | ||
| assertThat(copy.name()).isEqualTo("dataset"); | ||
| assertThat(copy.maxThreadCount()).isEqualTo(4); | ||
| assertThat(copy.tags()).containsExactly("nightly"); | ||
| } | ||
|
|
||
| @Test | ||
| void deserializesOptionsFromJson() throws JsonProcessingException { | ||
| String json = """ | ||
| { | ||
| "name": "dataset", | ||
| "options": { | ||
| "solver": "fast", | ||
| "logLevel": "debug" | ||
| } | ||
| } | ||
| """; | ||
|
|
||
| RunConfiguration configuration = mapper.readValue(json, RunConfiguration.class); | ||
|
|
||
| assertThat(configuration.name()).isEqualTo("dataset"); | ||
| assertThat(configuration.options()) | ||
| .containsExactlyInAnyOrderEntriesOf(Map.of("solver", "fast", "logLevel", "debug")); | ||
| } | ||
|
|
||
| @Test | ||
| void omitsNullOptionsFromJson() throws JsonProcessingException { | ||
| String json = mapper.writeValueAsString(new RunConfiguration("dataset")); | ||
|
|
||
| assertThat(json).doesNotContain("options"); | ||
| } | ||
|
|
||
| @Test | ||
| void serializesOptionsWhenPresent() throws JsonProcessingException { | ||
| RunConfiguration configuration = | ||
| new RunConfiguration(null, null, null, null, Map.of("solver", "fast")); | ||
|
|
||
| String json = mapper.writeValueAsString(configuration); | ||
|
|
||
| assertThat(json).contains("\"options\":{\"solver\":\"fast\"}"); | ||
| } | ||
|
|
||
| @Test | ||
| void overrideFillsNullTagsFromFallback() { | ||
| RunConfiguration primary = new RunConfiguration("dataset", null, null, null, null); | ||
| RunConfiguration fallback = new RunConfiguration(null, null, null, Set.of("nightly"), null); | ||
|
|
||
| assertThat(primary.override(fallback).tags()).containsExactly("nightly"); | ||
| } | ||
|
|
||
| @Test | ||
| void overrideFillsEmptyTagsFromFallback() { | ||
| RunConfiguration primary = new RunConfiguration("dataset", null, null, Set.of(), null); | ||
| RunConfiguration fallback = new RunConfiguration(null, null, null, Set.of("nightly"), null); | ||
|
|
||
| assertThat(primary.override(fallback).tags()).containsExactly("nightly"); | ||
| } | ||
|
|
||
| @Test | ||
| void overrideKeepsPrimaryTagsWhenPresent() { | ||
| RunConfiguration primary = new RunConfiguration("dataset", null, null, Set.of("adhoc"), null); | ||
| RunConfiguration fallback = new RunConfiguration(null, null, null, Set.of("nightly"), null); | ||
|
|
||
| assertThat(primary.override(fallback).tags()).containsExactly("adhoc"); | ||
| } | ||
| } |
40 changes: 40 additions & 0 deletions
40
...ai/timefold/solver/service/definition/impl/executionprofile/SeedExecutionProfileTest.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,40 @@ | ||
| package ai.timefold.solver.service.definition.impl.executionprofile; | ||
|
|
||
| import static org.assertj.core.api.Assertions.assertThat; | ||
| import static org.assertj.core.api.Assertions.assertThatThrownBy; | ||
|
|
||
| import java.util.Map; | ||
|
|
||
| import org.junit.jupiter.api.Test; | ||
|
|
||
| class SeedExecutionProfileTest { | ||
|
|
||
| private final SeedExecutionProfile profile = new SeedExecutionProfile(); | ||
|
|
||
| @Test | ||
| void mapsSuppliedSeedToEnvironment() { | ||
| assertThat(profile.toEnvironment(Map.of(SeedExecutionProfile.PARAMETER_SEED, "42"))) | ||
| .containsExactly(Map.entry(SeedExecutionProfile.ENV_QUARKUS_RANDOM_SEED, "42")); | ||
| } | ||
|
|
||
| @Test | ||
| void ignoresUnrelatedOptions() { | ||
| // The seed profile only reads its own option key; other run options are left alone. | ||
| assertThat(profile.toEnvironment(Map.of("solver", "fast", SeedExecutionProfile.PARAMETER_SEED, "7"))) | ||
| .containsExactly(Map.entry(SeedExecutionProfile.ENV_QUARKUS_RANDOM_SEED, "7")); | ||
| } | ||
|
|
||
| @Test | ||
| void failsWhenSeedMissing() { | ||
| // Selecting the seed profile without supplying a seed is an error - the profile never invents one. | ||
| assertThatThrownBy(() -> profile.toEnvironment(Map.of())) | ||
| .isInstanceOf(IllegalArgumentException.class); | ||
| } | ||
|
|
||
| @Test | ||
| void rejectsNonLongSeed() { | ||
| Map<String, String> options = Map.of(SeedExecutionProfile.PARAMETER_SEED, "not-a-number"); | ||
| assertThatThrownBy(() -> profile.toEnvironment(options)) | ||
| .isInstanceOf(IllegalArgumentException.class); | ||
| } | ||
| } |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.