From c73bd8007532e4c1eb952b874bb41f78e85533da Mon Sep 17 00:00:00 2001 From: Tom Cools Date: Thu, 25 Jun 2026 17:00:48 +0200 Subject: [PATCH 01/17] chore: first draft - service as primary approach --- docs/src/modules/ROOT/nav.adoc | 29 +- .../commercial-editions.adoc | 2 +- .../modules/ROOT/pages/service/overview.adoc | 43 +- .../upgrade-from-v1.adoc | 2 +- .../library-integration.adoc | 419 +++++++++++++++ .../pages/using-timefold-solver/overview.adoc | 43 +- .../running-the-solver.adoc | 491 +++--------------- 7 files changed, 586 insertions(+), 443 deletions(-) create mode 100644 docs/src/modules/ROOT/pages/using-timefold-solver/library-integration.adoc diff --git a/docs/src/modules/ROOT/nav.adoc b/docs/src/modules/ROOT/nav.adoc index 32f89d7e6d6..667dbd4988c 100644 --- a/docs/src/modules/ROOT/nav.adoc +++ b/docs/src/modules/ROOT/nav.adoc @@ -2,11 +2,11 @@ * xref:planning-ai-concepts.adoc[leveloffset=+1] * Getting started ** xref:quickstart/overview.adoc[leveloffset=+1] +** xref:quickstart/service/getting-started.adoc[Run as a service (Preview)] ** Embed as a library *** xref:quickstart/hello-world/hello-world-quickstart.adoc[Hello World Guide] *** xref:quickstart/quarkus/quarkus-quickstart.adoc[Quarkus Guide] *** xref:quickstart/spring-boot/spring-boot-quickstart.adoc[Spring Boot Guide] -** xref:quickstart/service/getting-started.adoc[Run as a service (Preview)] * Example use cases ** xref:quickstart/quarkus-vehicle-routing/quarkus-vehicle-routing-quickstart.adoc[Vehicle Routing (Guide)] ** https://github.com/TimefoldAI/timefold-quickstarts#-employee-scheduling[Employee Scheduling^] @@ -25,16 +25,16 @@ ** https://github.com/TimefoldAI/timefold-quickstarts#-tournament-scheduling[Tournament Scheduling^] * Using Timefold Solver ** xref:using-timefold-solver/overview.adoc[leveloffset=+1] -** xref:using-timefold-solver/configuration.adoc[leveloffset=+1] ** xref:using-timefold-solver/modeling-planning-problems.adoc[leveloffset=+1] -** xref:using-timefold-solver/running-the-solver.adoc[leveloffset=+1] -** xref:using-timefold-solver/benchmarking-and-tweaking.adoc[leveloffset=+1] -** xref:service/overview.adoc[Building a service (Preview)] +** xref:service/overview.adoc[Run as a Service (Preview)] *** xref:service/rest-api.adoc[leveloffset=+1] *** xref:service/modeling-changes.adoc[leveloffset=+1] *** xref:service/constraint-weights.adoc[leveloffset=+1] *** xref:service/demo-data.adoc[leveloffset=+1] *** xref:service/exposing-metrics.adoc[leveloffset=+1] +** xref:using-timefold-solver/library-integration.adoc[Use as a Library (Advanced)] +*** xref:using-timefold-solver/configuration.adoc[leveloffset=+1] +** xref:using-timefold-solver/running-the-solver.adoc[leveloffset=+1] * Constraints and score ** xref:constraints-and-score/overview.adoc[leveloffset=+1] ** xref:constraints-and-score/score-calculation.adoc[leveloffset=+1] @@ -42,13 +42,16 @@ ** xref:constraints-and-score/constraint-configuration.adoc[leveloffset=+1] ** xref:constraints-and-score/load-balancing-and-fairness.adoc[leveloffset=+1] ** xref:constraints-and-score/performance.adoc[leveloffset=+1] -* Optimization algorithms -** xref:optimization-algorithms/overview.adoc[leveloffset=+1] -** xref:optimization-algorithms/construction-heuristics.adoc[leveloffset=+1] -** xref:optimization-algorithms/local-search.adoc[leveloffset=+1] -** xref:optimization-algorithms/exhaustive-search.adoc[leveloffset=+1] -** xref:optimization-algorithms/neighborhoods.adoc[leveloffset=+1] -** xref:optimization-algorithms/move-selector-reference.adoc[leveloffset=+1] + +* Tuning the Solver +** xref:using-timefold-solver/benchmarking-and-tweaking.adoc[leveloffset=+1] +** xref:optimization-algorithms/overview.adoc[Optimization algorithms] +*** xref:optimization-algorithms/construction-heuristics.adoc[leveloffset=+1] +*** xref:optimization-algorithms/local-search.adoc[leveloffset=+1] +*** xref:optimization-algorithms/exhaustive-search.adoc[leveloffset=+1] +** Custom moves +*** xref:optimization-algorithms/neighborhoods.adoc[Neighborhoods API] +*** xref:optimization-algorithms/move-selector-reference.adoc[leveloffset=+1] * xref:responding-to-change/responding-to-change.adoc[leveloffset=+1] * xref:integration/integration.adoc[leveloffset=+1] * xref:design-patterns/design-patterns.adoc[leveloffset=+1] @@ -72,4 +75,4 @@ ** xref:using-timefold-solver/running-the-solver.adoc#partitionedSearch[Partitioned search] ** xref:constraints-and-score/performance.adoc#constraintProfiling[Constraint profiling] ** xref:commercial-editions/multistage-moves.adoc[leveloffset=+1] -** xref:using-timefold-solver/running-the-solver.adoc#throttlingBestSolutionEvents[Throttling best solution events] +** xref:using-timefold-solver/library-integration.adoc#throttlingBestSolutionEvents[Throttling best solution events] diff --git a/docs/src/modules/ROOT/pages/commercial-editions/commercial-editions.adoc b/docs/src/modules/ROOT/pages/commercial-editions/commercial-editions.adoc index 0f36ae61ab4..2519f4ea973 100644 --- a/docs/src/modules/ROOT/pages/commercial-editions/commercial-editions.adoc +++ b/docs/src/modules/ROOT/pages/commercial-editions/commercial-editions.adoc @@ -49,7 +49,7 @@ See our https://licenses.timefold.ai/[license portal] to get your license, then | xref:commercial-editions/multistage-moves.adoc[Multistage moves] | | ✓ -| xref:using-timefold-solver/running-the-solver.adoc#throttlingBestSolutionEvents[Throttling best solution events] +| xref:using-timefold-solver/library-integration.adoc#throttlingBestSolutionEvents[Throttling best solution events] | | ✓ |=== \ No newline at end of file diff --git a/docs/src/modules/ROOT/pages/service/overview.adoc b/docs/src/modules/ROOT/pages/service/overview.adoc index 2ad7e964139..7471f459e2c 100644 --- a/docs/src/modules/ROOT/pages/service/overview.adoc +++ b/docs/src/modules/ROOT/pages/service/overview.adoc @@ -12,19 +12,46 @@ This opinionated approach builds on https://quarkus.io[Quarkus] and eliminates t If you haven't done so yet, start with the xref:../quickstart/service/getting-started.adoc[Getting started: building a service] guide. +[#howSolvingWorks] +== How solving works + +Once your service is running, solving is driven entirely through the REST API — no solver management code required. + +. *Submit a problem* — `POST /` with your planning problem as a JSON body. +The service registers the dataset, starts optimizing asynchronously, and returns an ID immediately. +. *Poll for results* — `GET //\{id\}` returns the best solution found so far, along with the current score. +Call this repeatedly to show progress to the end-user, or once after a known time limit. +. *Terminate early* — `DELETE //\{id\}` stops solving and returns the best solution reached. +. *Inspect score analysis* — `GET //\{id\}/score-analysis` breaks down which constraints are violated and by how much. + +See xref:rest-api.adoc[REST API] for the full endpoint reference and request/response formats. + [#foundationsStillApply] -== Foundations still apply +== Built on Timefold Solver (the library) -Building a service does not replace the core Timefold Solver concepts, it builds on top of them. -Everything covered in the following sections is still relevant and applies directly to your service model: +The service module is not a separate product, it is Timefold Solver with an opinionated runtime on top. +Under the hood, the same `Solver`, `SolverManager`, constraint streams, and optimization algorithms are running. +Everything you know about modeling planning problems and writing constraints transfers directly. -xref:../using-timefold-solver/overview.adoc#usingTimefoldSolverOverview[*Using Timefold Solver*]:: -How to model your planning problem, configure the solver, and understand the solving lifecycle. -This knowledge is required regardless of whether you embed the solver as a library or run it as a service. +If you are coming from the library, think of the service as the infrastructure layer you no longer have to write yourself: +instead of wiring up a `SolverManager`, a REST endpoint, or thinking about good model ergonomics, +the service module provides all of that out of the box. + +Your modeling and constraint knowledge is the same, only the plumbing changes. + +The following areas of the documentation apply equally to both service and library mode: + +xref:../using-timefold-solver/modeling-planning-problems.adoc[*Modeling planning problems*]:: +How to annotate your domain with `@PlanningSolution`, `@PlanningEntity`, `@PlanningVariable`, and related annotations. +This is identical whether you run as a service or embed as a library. xref:../constraints-and-score/overview.adoc#constraintsAndScoreOverview[*Constraints and Score*]:: How to define hard and soft constraints, choose a score type, and analyze solution quality. -Constraint streams and score calculation work identically in a service model. +Constraint streams and score calculation work identically in both modes. + +xref:../using-timefold-solver/running-the-solver.adoc[*Tuning and diagnostics*]:: +Environment modes, logging, monitoring, multi-threaded solving, and random seed configuration. +These concepts and most configuration options apply regardless of how you run the solver. -The pages in this section cover only what is *specific* to running the solver as a service: +The pages in this section cover only what is *specific* to the service module: the REST API contract, model enrichment, constraint weight overrides, demo data, and metrics. diff --git a/docs/src/modules/ROOT/pages/upgrading-timefold-solver/upgrade-from-v1.adoc b/docs/src/modules/ROOT/pages/upgrading-timefold-solver/upgrade-from-v1.adoc index 9fefaa3de05..517982f8591 100644 --- a/docs/src/modules/ROOT/pages/upgrading-timefold-solver/upgrade-from-v1.adoc +++ b/docs/src/modules/ROOT/pages/upgrading-timefold-solver/upgrade-from-v1.adoc @@ -830,7 +830,7 @@ void onFinalBestSolution(FinalBestSolutionEvent event) { } ---- -Users of xref:using-timefold-solver/running-the-solver.adoc#throttlingBestSolutionEvents[solution throttling] +Users of xref:using-timefold-solver/library-integration.adoc#throttlingBestSolutionEvents[solution throttling] should also update their code to use `ThrottlingBestSolutionEventConsumer` instead of the now removed `ThrottlingBestSolutionConsumer`. ==== diff --git a/docs/src/modules/ROOT/pages/using-timefold-solver/library-integration.adoc b/docs/src/modules/ROOT/pages/using-timefold-solver/library-integration.adoc new file mode 100644 index 00000000000..9bfab7089fa --- /dev/null +++ b/docs/src/modules/ROOT/pages/using-timefold-solver/library-integration.adoc @@ -0,0 +1,419 @@ +[#libraryIntegration] += Using Timefold Solver as a Library +:doctype: book +:sectnums: +:icons: font + +When xref:service/overview.adoc[running as a service], the framework manages the `Solver` and `SolverManager` on your behalf, +you interact only through the REST API. +When embedding Timefold Solver as a library, your code drives the entire solving lifecycle. +This page covers what that means in practice. + +[#howLibrarySolvingWorks] +== How library solving works + +At its core, a `Solver` takes a planning problem and returns the best solution it finds within the configured time limit: + +[tabs] +==== +Java:: ++ +[source,java,options="nowrap"] +---- +Timetable problem = ...; +Timetable bestSolution = solver.solve(problem); +---- +==== + +The `Solver` wades through xref:optimization-algorithms/overview.adoc#searchSpaceSize[the search space] of possible solutions +and remembers the best it encounters. +Depending on the problem size, time budget, and configuration, +xref:optimization-algorithms/overview.adoc#doesTimefoldFindTheOptimalSolution[that solution may or may not be optimal]. + +[NOTE] +==== +The instance passed to `solve(solution)` is modified by the Solver — do not treat it as the best solution. +The returned instance is most likely xref:using-timefold-solver/modeling-planning-problems.adoc#cloningASolution[a planning clone] of the input. +==== + +NOTE: The input may be partially or fully initialized, which is common in xref:responding-to-change/responding-to-change.adoc[repeated planning]. + +However, `Solver.solve()` *blocks the calling thread* for the entire duration of solving. +This makes it unsuitable for use in REST endpoints or anywhere you need to handle multiple problems concurrently. +`SolverManager` solves both problems. + +[#solverManagerConcept] +== `SolverManager` + +A `SolverManager` is a facade for one or more `Solver` instances +to simplify solving planning problems in REST and other enterprise services. +Unlike the `Solver.solve(...)` method: + +* *`SolverManager.solve(...)` returns immediately*: it schedules a problem for asynchronous solving without blocking the calling thread. +This avoids timeout issues of HTTP and other technologies. +* *`SolverManager.solve(...)` solves multiple planning problems* of the same domain, in parallel. + +Internally a `SolverManager` manages a thread pool of solver threads, which call `Solver.solve(...)`, +and a thread pool of consumer threads, which handle best solution changed events. + +In xref:integration/integration.adoc#integrationWithQuarkus[Quarkus] and xref:integration/integration.adoc#integrationWithSpringBoot[Spring Boot], +the `SolverManager` instance is automatically injected in your code. +Otherwise, build a `SolverManager` instance with the `create(...)` method: + +[tabs] +==== +Java:: ++ +[source,java,options="nowrap"] +---- +SolverConfig solverConfig = SolverConfig.createFromXmlResource(".../solverConfig.xml"); +SolverManager solverManager = SolverManager.create(solverConfig, new SolverManagerConfig()); +---- +==== + +Each problem submitted to the `SolverManager.solve(...)` methods needs a unique problem ID. +Later calls to `getSolverStatus(problemId)` or `terminateEarly(problemId)` use that problem ID +to distinguish between the planning problems. +The problem ID must be an immutable class, such as `Long`, `String` or `java.util.UUID`. + +The `SolverManagerConfig` class has a `parallelSolverCount` property, +that controls how many solvers are run in parallel. +For example, if set to `4`, submitting five problems +has four problems solving immediately, and the fifth one starts when another one ends. +If those problems solve for 5 minutes each, the fifth problem takes 10 minutes to finish. +By default, `parallelSolverCount` is set to `AUTO`, which resolves to half the CPU cores, +regardless of the xref:using-timefold-solver/running-the-solver.adoc#multithreadedIncrementalSolving[`moveThreadCount`] of the solvers. + +To retrieve the best solution, after solving terminates normally, use `SolverJob.getFinalBestSolution()`: + +[tabs] +==== +Java:: ++ +[source,java,options="nowrap"] +---- +VehicleRoutePlan problem1 = ...; +String problemId = UUID.randomUUID().toString(); +// Returns immediately +SolverJob solverJob = solverManager.solve(problemId, problem1); +... + +try { + // Returns only after solving terminates + VehicleRoutePlan solution1 = solverJob.getFinalBestSolution(); +} catch (InterruptedException | ExecutionException e) { + throw ...; +} +---- +==== + +However, there are better approaches, both for solving batch problems before an end-user needs the solution +as well as for live solving while an end-user is actively waiting for the solution, as explained below. + +The current `SolverManager` implementation runs on a single computer node, +but future work aims to distribute solver loads across a cloud. + + +[#solverManagerBuilder] +== The `SolverManager` Builder + +The `SolverManager` also enables the creation of a builder to customize and submit a planning problem for solving. + +[tabs] +==== +Java:: ++ +[source,java,options="nowrap"] +---- +public interface SolverManager { + + SolverJobBuilder solveBuilder(); + + ... +} +---- +==== + +=== Required settings + +The `SolverJobBuilder` contract includes many optional methods, but only two are required: `withProblemId(...)` and `withProblem(...)`. + +[tabs] +==== +Java:: ++ +[source,java,options="nowrap"] +---- +solverManager.solveBuilder() + .withProblemId(problemId) + .withProblem(problem) +... +---- +==== + +The job's unique ID is specified using `withProblemId(problemId)`. +The provided ID allows for the identification of a specific problem, +enabling actions such as checking the solving status or terminating its execution. +In addition to the unique ID, we must specify the problem to solve using `withProblem(problem)`. + +=== Optional settings + +Additional optional methods are also included in the `SolverJobBuilder` contract: + +[tabs] +==== +Java:: ++ +[source,java,options="nowrap"] +---- +solverManager.solveBuilder() + .withProblemId(problemId) + .withProblem(problem) + .withFirstInitializedSolutionEventConsumer(firstInitializedSolutionEventConsumer) + .withBestSolutionEventConsumer(bestSolutionEventConsumer) + .withFinalBestSolutionEventConsumer(finalBestSolutionEventConsumer) + .withExceptionHandler(exceptionHandler) + .withConfigOverride(configOverride) +... +---- +==== + +A consumer for the first initialized solution can be configured with `withFirstInitializedSolutionEventConsumer(...)`. +The solution is returned by the last phase that immediately precedes the first local search phase. + +Whenever a new best solution is generated by the solver, +it can be consumed by configuring it with `withBestSolutionEventConsumer(...)`. +The final best solution consumer, +which is called at the end of the solving process, +can be set using `withFinalBestSolutionEventConsumer(...)`. +Additionally, +an improved solution consumer capable of throttling events is available in the <> of the Timefold Solver. + +[WARNING] +==== +Do not modify the solutions returned by the events in `withFirstInitializedSolutionEventConsumer(...)` and `withBestSolutionEventConsumer(...)`. +These instances are still utilized during the solving process, and any modifications may lead to solver corruption. +==== + +[#throttlingBestSolutionEvents] +==== Throttling best solution events in `SolverManager` + +include::../commercial-editions/_only-enterprise.adoc[] + +This feature helps you avoid overloading your system with best solution events, +especially in the early phase of the solving process when the solver is typically improving the solution very rapidly. + +To enable event throttling, use `ThrottlingBestSolutionEventConsumer` when starting a new `SolverJob` using `SolverManager`: + +[source,java,options="nowrap"] +---- +... +import ai.timefold.solver.enterprise.core.api.ThrottlingBestSolutionEventConsumer; +import java.time.Duration; +... + +public class TimetableService { + + private SolverManager solverManager; + + public String solve(Timetable problem) { + var bestSolutionEventConsumer = ThrottlingBestSolutionEventConsumer.of( + event -> { + // Your custom event handling code goes here. + }, + Duration.ofSeconds(1)); // Throttle to 1 event per second. + + String jobId = ...; + solverManager.solveBuilder() + .withProblemId(jobId) + .withProblem(problem) + .withBestSolutionEventConsumer(bestSolutionEventConsumer) + .run(); // Start the solver job and listen to best solutions, with throttling. + return jobId; + } + +} +---- + +This will ensure that your system will never receive more than one best solution event per second. +Some other important points to note: + +- If multiple events arrive during the pre-defined 1-second interval, only the last event will be delivered. +- When the `SolverJob` terminates, the last event received will be delivered regardless of the throttle, +unless it was already delivered before. +- If your consumer throws an exception, we will still count the event as delivered. +- If the system is too occupied to start and execute new threads, +event delivery will be delayed until a thread can be started. + +[NOTE] +==== +If you are using the `ThrottlingBestSolutionEventConsumer` for intermediate best solutions +together with a final best solution consumer, +both these consumers will receive the final best solution. +==== + +To handle errors that may arise during the solving process, +set up the handling logic by defining `withExceptionHandler(...)`. + +Finally, to build an instance of the solver, +xref:using-timefold-solver/configuration.adoc[a configuration step] is necessary. +These settings are static and applied to any related solving execution. +If you want to override certain settings for a particular job, +such as the termination configuration, you can use the `withConfigOverride(...)` method. + +[NOTE] +==== +The solver also permits the configuration of multiple solver managers with distinct settings in xref:integration/integration.adoc#integrationWithQuarkusMultipleResources[Quarkus] or xref:integration/integration.adoc#integrationWithSpringBootMultipleResources[Spring Boot]. +==== + +[#solverManagerSolveBatch] +=== Solve batch problems + +At night, batch solving is a great approach to deliver solid plans by breakfast, because: + +* There are typically few or no problem changes in the middle of the night. +Some organizations even enforce a deadline, for example, _submit all day off requests before midnight_. +* The solvers can run for much longer, often hours, because nobody's waiting for it and CPU resources are often cheaper. + +To solve a multiple datasets in parallel (limited by `parallelSolverCount`), +call `solve(...)` for each dataset: + +[tabs] +==== +Java:: ++ +[source,java,options="nowrap"] +---- +public class TimetableService { + + private SolverManager solverManager; + + // Returns immediately, call it for every dataset + public void solveBatch(Long timetableId) { + solverManager.solve(timetableId, + // Called once, when solving starts + this::findById, + // Called once, when solving ends + this::save); + } + + public Timetable findById(Long timetableId) {...} + + public void save(Timetable timetable) {...} + +} +---- +==== + +A solid plan delivered by breakfast is great, +even if you need to react on problem changes during the day. + + +[#solverManagerSolveAndListen] +=== Solve and listen to show progress to the end-user + +When a solver is running while an end-user is waiting for that solution, +the user might need to wait for several minutes or hours before receiving a result. +To assure the user that everything is going well, +show progress by displaying the best solution and best score attained so far. + +To handle intermediate best solutions, use `solveAndListen(...)`: + +[tabs] +==== +Java:: ++ +[source,java,options="nowrap"] +---- +public class TimetableService { + + private SolverManager solverManager; + + // Returns immediately + public void solveLive(Long timetableId) { + solverManager.solveAndListen(timetableId, + // Called once, when solving starts + this::findById, + // Called multiple times, for every best solution change + this::save); + } + + public Timetable findById(Long timetableId) {...} + + public void save(Timetable timetable) {...} + + public void stopSolving(Long timetableId) { + solverManager.terminateEarly(timetableId); + } + +} +---- +==== + +This implementation is using the database to communicate with the UI, which polls the database. +More advanced implementations push the best solutions directly to the UI or a messaging queue. + +If the user is satisfied with the intermediate best solution +and does not want to wait any longer for a better one, call `SolverManager.terminateEarly(problemId)`. + +[NOTE] +==== +Best solution events may be triggered in a rapid succession, +especially at the start of solving. + +Users of our xref:commercial-editions/commercial-editions.adoc[Enterprise Edition] +may use the <> +to limit the number of best solution events fired over any period of time. + +Open-source users may implement their own throttling mechanism within the `Consumer` itself. +==== + +[#noSolverModelInterface] +== No `SolverModel` interface + +In library mode, your `@PlanningSolution` class is a plain Java class with Timefold annotations. +You do not implement any framework interface on the solution class itself. + +[tabs] +==== +Java:: ++ +[source,java,options="nowrap"] +---- +@PlanningSolution +public class Timetable { + + @ProblemFactCollectionProperty + @ValueRangeProvider + private List timeslots; + + @PlanningEntityCollectionProperty + private List lessons; + + @PlanningScore + private HardSoftScore score; + + // Getters, Setters, Constructors +} +---- +==== + +[#manualEnrichment] +== Manual enrichment + +The service module's xref:service/modeling-changes.adoc#solverModelEnrichment[`SolverModelEnricher`] is not available in library mode. +Any pre-processing or enrichment of the planning problem — such as fetching external data, +computing derived fields, or pinning entities from a previous solution — must be performed +before calling `Solver.solve()`: + +[tabs] +==== +Java:: ++ +[source,java,options="nowrap"] +---- +Timetable problem = loadFromDatabase(); +enrichWithExternalData(problem); // your own enrichment logic +Timetable solution = solver.solve(problem); +---- +==== diff --git a/docs/src/modules/ROOT/pages/using-timefold-solver/overview.adoc b/docs/src/modules/ROOT/pages/using-timefold-solver/overview.adoc index e2140ad672d..e8e16832c76 100644 --- a/docs/src/modules/ROOT/pages/using-timefold-solver/overview.adoc +++ b/docs/src/modules/ROOT/pages/using-timefold-solver/overview.adoc @@ -4,14 +4,41 @@ :doctype: book :sectnums: :icons: font -:relevance: library-and-service -:notes: split into both usages -Solving a planning problem with Timefold Solver consists of the following steps: +Timefold Solver can be used in two modes. +Both build on the same modeling and constraint concepts, but differ in how you deploy and interact with the solver. -. *Model your planning problem* as a class annotated with the ``@PlanningSolution`` annotation, for example the ``Timetable`` class. -. *Configure a ``Solver``*, for example a First Fit and Tabu Search solver for any `Timetable` instance. -. *Load a problem data set* from your data layer. That is the planning problem. -. *Solve it* with `Solver.solve(problem)` which returns the best solution found. +[#runAsAService] +== Run as a Service (recommended) -image::using-timefold-solver/overview/inputOutputOverview.png[align="center"] \ No newline at end of file +Deploy Timefold Solver as a standalone optimization service. +This mode handles the REST API, solver lifecycle, and more. + +Submit a planning problem via REST and receive optimized results asynchronously. +Configuration is done via `application.properties`. + +This is the recommended approach for most teams: +it lets you focus on modeling the problem and writing constraints +rather than on plumbing. This is the approach we use ourselves at Timefold for https://app.timefold.ai[models on our platform]. + +xref:service/overview.adoc[→ Run as a Service] + +[#embedAsALibrary] +== Use as a Library (advanced) + +Embed Timefold Solver directly in your application for full control over the solving lifecycle. +Use this approach when you need deep integration with existing infrastructure or have a use case or architecture where the recommended approach above doesn't apply to you. + +xref:using-timefold-solver/configuration.adoc[→ Use as a Library] + +[#commonFoundation] +== Common foundation + +Regardless of mode, the following steps and concepts apply: + +. *Model your planning problem* as a class annotated with `@PlanningSolution`, for example `Timetable`. +. *Define constraints* using a `ConstraintProvider` that expresses hard and soft rules. +. *Submit your problem* — via REST API (service) or `Solver.solve()` / `SolverManager` (library). +. *Receive the best solution* found within the configured time limit. + +image::using-timefold-solver/overview/inputOutputOverview.png[align="center"] diff --git a/docs/src/modules/ROOT/pages/using-timefold-solver/running-the-solver.adoc b/docs/src/modules/ROOT/pages/using-timefold-solver/running-the-solver.adoc index 52dc3694ad4..145be43bf7d 100644 --- a/docs/src/modules/ROOT/pages/using-timefold-solver/running-the-solver.adoc +++ b/docs/src/modules/ROOT/pages/using-timefold-solver/running-the-solver.adoc @@ -1,86 +1,13 @@ [#useTheSolver] [#runningTimefoldSolver] -= Running Timefold Solver += Tuning and diagnostics :doctype: book :sectnums: :icons: font -:relevance: core-only -:notes: Too many things the service module does automatically, or with a single configuration. -[#theSolverInterface] -== The `Solver` interface - -A `Solver` solves your planning problem. - -[tabs] -==== -Java:: -+ -[source,java,options="nowrap"] ----- -public interface Solver { - - Solution_ solve(Solution_ problem); - - ... -} ----- -==== - -A `Solver` can only solve one planning problem instance at a time. -It is built with a ``SolverFactory``, there is no need to implement it yourself. - -A `Solver` should only be accessed from a single thread, except for the methods that are specifically documented in javadoc as being thread-safe. -The `solve()` method hogs the current thread. -This can cause HTTP timeouts for REST services and it requires extra code to solve multiple datasets in parallel. -To avoid such issues, use a <> instead. - - -[#solvingAProblem] -== Solving a problem - -Solving a problem is quite easy once you have: - -* A `Solver` built from a solver configuration -* A `@PlanningSolution` that represents the planning problem instance - -Just provide the planning problem as argument to the `solve()` method and it will return the best solution found: - -[tabs] -==== -Java:: -+ -[source,java,options="nowrap"] ----- - Timetable problem = ...; - Timetable bestSolution = solver.solve(problem); ----- -==== - -In school timetabling, -the `solve()` method will return a `Timetable` instance with every `Lesson` assigned to a `Teacher` and a `Timeslot`. - -The `solve(Solution)` method can take a long time (depending on the problem size and the solver configuration). -The `Solver` intelligently wades through xref:optimization-algorithms/overview.adoc#searchSpaceSize[the search space] of possible solutions -and remembers the best solution it encounters during solving. -Depending on a number of factors (including problem size, how much time the `Solver` has, the solver configuration, ...), -xref:optimization-algorithms/overview.adoc#doesTimefoldFindTheOptimalSolution[that best solution might or might not be an optimal solution]. - -[NOTE] -==== -The solution instance given to the method `solve(solution)` is changed by the ``Solver``, -but do not mistake it for the best solution. - -The solution instance returned by the methods `solve(solution)` or `getBestSolution()` -is most likely xref:using-timefold-solver/modeling-planning-problems.adoc#cloningASolution[a planning clone] of the instance -given to the method ``solve(solution)``, which implies it is a different instance. -==== - -NOTE: The solution instance given to the `solve(Solution)` method may be partially or fully initialized, -which is often the case in xref:responding-to-change/responding-to-change.adoc[repeated planning]. [#multithreadedSolving] -=== Multi-threaded solving +== Multi-threaded solving There are several ways of running the solver in parallel: @@ -94,12 +21,12 @@ Split 1 dataset in multiple parts and solve them independently. ** Not recommended: This is a marginal gain for a high cost of hardware resources. ** Use the xref:using-timefold-solver/benchmarking-and-tweaking.adoc#benchmarker[Benchmarker] during development to determine the algorithm that is the most appropriate on average. * *Multitenancy*: solve different datasets in parallel. -** The xref:using-timefold-solver/running-the-solver.adoc#solverManager[`SolverManager`] can help with this. +** The xref:using-timefold-solver/library-integration.adoc[`SolverManager`] can help with this. image::using-timefold-solver/running-the-solver/multiThreadingStrategies.png[align="center"] [#multithreadedIncrementalSolving] -==== Multi-threaded incremental solving +=== Multi-threaded incremental solving include::../commercial-editions/_only-enterprise.adoc[] @@ -132,7 +59,7 @@ We recommend you benchmark your use case to determine the optimal number of move threads for your problem. ==== -===== Enabling multi-threaded incremental solving +==== Enabling multi-threaded incremental solving Enable multi-threaded incremental solving by xref:using-timefold-solver/modeling-planning-problems.adoc#planningId[adding a `@PlanningId` annotation] @@ -141,7 +68,7 @@ Then configure a `moveThreadCount`: [tabs] ==== -Quarkus:: +Service / Quarkus:: + -- Add the following to your `application.properties`: @@ -225,7 +152,7 @@ A run of the same solver configuration on 2 machines with a different number of is still reproducible, unless the `moveThreadCount` is set to `AUTO` or a function of `availableProcessorCount`. ==== -===== Advanced configuration +==== Advanced configuration There are additional parameters you can supply to your `solverConfig.xml`: @@ -244,12 +171,12 @@ use `threadFactoryClass` to plug in a <> with it: The environment mode allows you to detect common bugs in your implementation. It does not affect the <>. -You can set the environment mode in the solver configuration XML file: +You can set the environment mode: + +[tabs] +==== +Service / Quarkus:: ++ +Add the following to your `application.properties`: ++ +[source,properties,options="nowrap"] +---- +quarkus.timefold.solver.environment-mode=STEP_ASSERT +---- + +Spring Boot:: ++ +Add the following to your `application.properties`: ++ +[source,properties,options="nowrap"] +---- +timefold.solver.environment-mode=STEP_ASSERT +---- + +Java:: ++ +[source,java,options="nowrap"] +---- +SolverConfig solverConfig = new SolverConfig() + ... + .withEnvironmentMode(EnvironmentMode.STEP_ASSERT); +---- +XML:: ++ [source,xml,options="nowrap"] ---- ---- +==== A solver has a single `Random` instance. Some solver configurations use the `Random` instance a lot more than others. @@ -770,10 +729,25 @@ In Logback, use a `SiftingAppender` in ``logback.xml``: [#monitoring] == Monitoring the solver -Timefold Solver exposes metrics through https://micrometer.io/[Micrometer] which you can use to monitor the solver. Timefold automatically connects to configured registries when it is used in Quarkus or Spring Boot. If you use Timefold with plain Java, you must add the metrics registry to the global registry. +Timefold Solver exposes metrics through https://micrometer.io/[Micrometer] which you can use to monitor the solver. + +[tabs] +==== +Service:: ++ +Micrometer is automatically configured in the service module. +See xref:service/exposing-metrics.adoc[Exposing Metrics] for setup instructions and the list of available metrics. + +Quarkus / Spring Boot:: ++ +Timefold automatically connects to configured Micrometer registries in Quarkus and Spring Boot. +Add the appropriate Micrometer registry dependency for your monitoring system. + +Java:: ++ +When using plain Java, you must add the metrics registry to the global registry manually. .Prerequisites -* You have a plain Java Timefold Solver project. * You have configured a Micrometer registry. For information about configuring Micrometer registries, see the https://micrometer.io[Micrometer] web site. .Procedure @@ -807,8 +781,9 @@ try { Metrics.addRegistry(prometheusRegistry); ---- +==== -. Open your monitoring system to view the metrics for your Timefold Solver project. The following metrics are exposed: +The following metrics are exposed: + [NOTE] ==== @@ -919,338 +894,30 @@ Many heuristics and metaheuristics depend on a pseudorandom number generator for To change the random seed of that `RandomGenerator` instance, specify a ``randomSeed``: -[source,xml,options="nowrap"] ----- - - 0 - ... - ----- - - -[#solverManager] -== `SolverManager` - -A `SolverManager` is a facade for one or more `Solver` instances -to simplify solving planning problems in REST and other enterprise services. -Unlike the `Solver.solve(...)` method: - -* *`SolverManager.solve(...)` returns immediately*: it schedules a problem for asynchronous solving without blocking the calling thread. -This avoids timeout issues of HTTP and other technologies. -* *`SolverManager.solve(...)` solves multiple planning problems* of the same domain, in parallel. - -Internally a `SolverManager` manages a thread pool of solver threads, which call `Solver.solve(...)`, -and a thread pool of consumer threads, which handle best solution changed events. - -In xref:integration/integration.adoc#integrationWithQuarkus[Quarkus] and xref:integration/integration.adoc#integrationWithSpringBoot[Spring Boot], -the `SolverManager` instance is automatically injected in your code. -Otherwise, build a `SolverManager` instance with the `create(...)` method: - [tabs] ==== -Java:: +Service / Quarkus:: + -[source,java,options="nowrap"] +[source,properties,options="nowrap"] ---- -SolverConfig solverConfig = SolverConfig.createFromXmlResource(".../solverConfig.xml"); -SolverManager solverManager = SolverManager.create(solverConfig, new SolverManagerConfig()); +quarkus.timefold.solver.random-seed=0 ---- -==== -Each problem submitted to the `SolverManager.solve(...)` methods needs a unique problem ID. -Later calls to `getSolverStatus(problemId)` or `terminateEarly(problemId)` use that problem ID -to distinguish between the planning problems. -The problem ID must be an immutable class, such as `Long`, `String` or `java.util.UUID`. - -The `SolverManagerConfig` class has a `parallelSolverCount` property, -that controls how many solvers are run in parallel. -For example, if set to `4`, submitting five problems -has four problems solving immediately, and the fifth one starts when another one ends. -If those problems solve for 5 minutes each, the fifth problem takes 10 minutes to finish. -By default, `parallelSolverCount` is set to `AUTO`, which resolves to half the CPU cores, -regardless of the xref:using-timefold-solver/running-the-solver.adoc#multithreadedIncrementalSolving[`moveThreadCount`] of the solvers. - -To retrieve the best solution, after solving terminates normally, use `SolverJob.getFinalBestSolution()`: - -[tabs] -==== -Java:: +Spring Boot:: + -[source,java,options="nowrap"] +[source,properties,options="nowrap"] ---- -VehicleRoutePlan problem1 = ...; -String problemId = UUID.randomUUID().toString(); -// Returns immediately -SolverJob solverJob = solverManager.solve(problemId, problem1); -... - -try { - // Returns only after solving terminates - VehicleRoutePlan solution1 = solverJob.getFinalBestSolution(); -} catch (InterruptedException | ExecutionException e) { - throw ...; -} +timefold.solver.random-seed=0 ---- -==== - -However, there are better approaches, both for solving batch problems before an end-user needs the solution -as well as for live solving while an end-user is actively waiting for the solution, as explained below. - -The current `SolverManager` implementation runs on a single computer node, -but future work aims to distribute solver loads across a cloud. - - -[#solverManagerBuilder] -== The `SolverManager` Builder - -The `SolverManager` also enables the creation of a builder to customize and submit a planning problem for solving. - -[tabs] -==== -Java:: -+ -[source,java,options="nowrap"] ----- -public interface SolverManager { - - SolverJobBuilder solveBuilder(); - ... -} ----- -==== - -=== Required settings - -The `SolverJobBuilder` contract includes many optional methods, but only two are required: `withProblemId(...)` and `withProblem(...)`. - -[tabs] -==== -Java:: -+ -[source,java,options="nowrap"] ----- -solverManager.solveBuilder() - .withProblemId(problemId) - .withProblem(problem) -... ----- -==== - -The job's unique ID is specified using `withProblemId(problemId)`. -The provided ID allows for the identification of a specific problem, -enabling actions such as checking the solving status or terminating its execution. -In addition to the unique ID, we must specify the problem to solve using `withProblem(problem)`. - -=== Optional settings - -Additional optional methods are also included in the `SolverJobBuilder` contract: - -[tabs] -==== -Java:: -+ -[source,java,options="nowrap"] ----- -solverManager.solveBuilder() - .withProblemId(problemId) - .withProblem(problem) - .withFirstInitializedSolutionEventConsumer(firstInitializedSolutionEventConsumer) - .withBestSolutionEventConsumer(bestSolutionEventConsumer) - .withFinalBestSolutionEventConsumer(finalBestSolutionEventConsumer) - .withExceptionHandler(exceptionHandler) - .withConfigOverride(configOverride) -... ----- -==== - -A consumer for the first initialized solution can be configured with `withFirstInitializedSolutionEventConsumer(...)`. -The solution is returned by the last phase that immediately precedes the first local search phase. - -Whenever a new best solution is generated by the solver, -it can be consumed by configuring it with `withBestSolutionEventConsumer(...)`. -The final best solution consumer, -which is called at the end of the solving process, -can be set using `withFinalBestSolutionEventConsumer(...)`. -Additionally, -an improved solution consumer capable of throttling events is available in the xref:using-timefold-solver/running-the-solver.adoc#throttlingBestSolutionEvents[Enterprise Edition] of the Timefold Solver. - -[WARNING] -==== -Do not modify the solutions returned by the events in `withFirstInitializedSolutionEventConsumer(...)` and `withBestSolutionEventConsumer(...)`. -These instances are still utilized during the solving process, and any modifications may lead to solver corruption. -==== - -[#throttlingBestSolutionEvents] -==== Throttling best solution events in `SolverManager` - -include::../commercial-editions/_only-enterprise.adoc[] - -This feature helps you avoid overloading your system with best solution events, -especially in the early phase of the solving process when the solver is typically improving the solution very rapidly. - -To enable event throttling, use `ThrottlingBestSolutionEventConsumer` when starting a new `SolverJob` using `SolverManager`: - -[source,java,options="nowrap"] ----- -... -import ai.timefold.solver.enterprise.core.api.ThrottlingBestSolutionEventConsumer; -import java.time.Duration; -... - -public class TimetableService { - - private SolverManager solverManager; - - public String solve(Timetable problem) { - var bestSolutionEventConsumer = ThrottlingBestSolutionEventConsumer.of( - event -> { - // Your custom event handling code goes here. - }, - Duration.ofSeconds(1)); // Throttle to 1 event per second. - - String jobId = ...; - solverManager.solveBuilder() - .withProblemId(jobId) - .withProblem(problem) - .withBestSolutionEventConsumer(bestSolutionEventConsumer) - .run(); // Start the solver job and listen to best solutions, with throttling. - return jobId; - } - -} ----- - -This will ensure that your system will never receive more than one best solution event per second. -Some other important points to note: - -- If multiple events arrive during the pre-defined 1-second interval, only the last event will be delivered. -- When the `SolverJob` terminates, the last event received will be delivered regardless of the throttle, -unless it was already delivered before. -- If your consumer throws an exception, we will still count the event as delivered. -- If the system is too occupied to start and execute new threads, -event delivery will be delayed until a thread can be started. - -[NOTE] -==== -If you are using the `ThrottlingBestSolutionEventConsumer` for intermediate best solutions -together with a final best solution consumer, -both these consumers will receive the final best solution. -==== - -To handle errors that may arise during the solving process, -set up the handling logic by defining `withExceptionHandler(...)`. - -Finally, to build an instance of the solver, -xref:using-timefold-solver/configuration.adoc[a configuration step] is necessary. -These settings are static and applied to any related solving execution. -If you want to override certain settings for a particular job, -such as the termination configuration, you can use the `withConfigOverride(...)` method. - -[NOTE] -==== -The solver also permits the configuration of multiple solver managers with distinct settings in xref:integration/integration.adoc#integrationWithQuarkusMultipleResources[Quarkus] or xref:integration/integration.adoc#integrationWithSpringBootMultipleResources[Spring Boot]. -==== - -[#solverManagerSolveBatch] -=== Solve batch problems - -At night, batch solving is a great approach to deliver solid plans by breakfast, because: - -* There are typically few or no problem changes in the middle of the night. -Some organizations even enforce a deadline, for example, _submit all day off requests before midnight_. -* The solvers can run for much longer, often hours, because nobody's waiting for it and CPU resources are often cheaper. - -To solve a multiple datasets in parallel (limited by `parallelSolverCount`), -call `solve(...)` for each dataset: - -[tabs] -==== -Java:: -+ -[source,java,options="nowrap"] ----- -public class TimetableService { - - private SolverManager solverManager; - - // Returns immediately, call it for every dataset - public void solveBatch(Long timetableId) { - solverManager.solve(timetableId, - // Called once, when solving starts - this::findById, - // Called once, when solving ends - this::save); - } - - public Timetable findById(Long timetableId) {...} - - public void save(Timetable timetable) {...} - -} ----- -==== - -A solid plan delivered by breakfast is great, -even if you need to react on problem changes during the day. - - -[#solverManagerSolveAndListen] -=== Solve and listen to show progress to the end-user - -When a solver is running while an end-user is waiting for that solution, -the user might need to wait for several minutes or hours before receiving a result. -To assure the user that everything is going well, -show progress by displaying the best solution and best score attained so far. - -To handle intermediate best solutions, use `solveAndListen(...)`: - -[tabs] -==== -Java:: +XML:: + -[source,java,options="nowrap"] +[source,xml,options="nowrap"] ---- -public class TimetableService { - - private SolverManager solverManager; - - // Returns immediately - public void solveLive(Long timetableId) { - solverManager.solveAndListen(timetableId, - // Called once, when solving starts - this::findById, - // Called multiple times, for every best solution change - this::save); - } - - public Timetable findById(Long timetableId) {...} - - public void save(Timetable timetable) {...} - - public void stopSolving(Long timetableId) { - solverManager.terminateEarly(timetableId); - } - -} + + 0 + ... + ---- ==== - -This implementation is using the database to communicate with the UI, which polls the database. -More advanced implementations push the best solutions directly to the UI or a messaging queue. - -If the user is satisfied with the intermediate best solution -and does not want to wait any longer for a better one, call `SolverManager.terminateEarly(problemId)`. - -[NOTE] -==== -Best solution events may be triggered in a rapid succession, -especially at the start of solving. - -Users of our xref:commercial-editions/commercial-editions.adoc[Enterprise Edition] -may use the xref:using-timefold-solver/running-the-solver.adoc#throttlingBestSolutionEvents[throttling feature] -to limit the number of best solution events fired over any period of time. - -Open-source users may implement their own throttling mechanism within the `Consumer` itself. -==== From e224676ba5ed9ef730463863999cc6d0585ba1ed Mon Sep 17 00:00:00 2001 From: Tom Cools Date: Thu, 25 Jun 2026 22:22:00 +0200 Subject: [PATCH 02/17] docs: correct missing xref. --- docs/src/modules/ROOT/pages/service/overview.adoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/src/modules/ROOT/pages/service/overview.adoc b/docs/src/modules/ROOT/pages/service/overview.adoc index 7471f459e2c..a1899689b37 100644 --- a/docs/src/modules/ROOT/pages/service/overview.adoc +++ b/docs/src/modules/ROOT/pages/service/overview.adoc @@ -24,7 +24,7 @@ Call this repeatedly to show progress to the end-user, or once after a known tim . *Terminate early* — `DELETE //\{id\}` stops solving and returns the best solution reached. . *Inspect score analysis* — `GET //\{id\}/score-analysis` breaks down which constraints are violated and by how much. -See xref:rest-api.adoc[REST API] for the full endpoint reference and request/response formats. +See xref:service/rest-api.adoc[REST API] for the full endpoint reference and request/response formats. [#foundationsStillApply] == Built on Timefold Solver (the library) From f7524c611dda4e4dcae0ed8d3c604970e70070cd Mon Sep 17 00:00:00 2001 From: Tom Cools Date: Thu, 25 Jun 2026 22:39:52 +0200 Subject: [PATCH 03/17] docs: Small nav fixes --- docs/src/modules/ROOT/nav.adoc | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/docs/src/modules/ROOT/nav.adoc b/docs/src/modules/ROOT/nav.adoc index 667dbd4988c..eb0093c0d55 100644 --- a/docs/src/modules/ROOT/nav.adoc +++ b/docs/src/modules/ROOT/nav.adoc @@ -23,8 +23,7 @@ ** https://github.com/TimefoldAI/timefold-quickstarts#-project-job-scheduling[Project Job Scheduling^] ** https://github.com/TimefoldAI/timefold-quickstarts#-sports-league-scheduling[Sports League Scheduling^] ** https://github.com/TimefoldAI/timefold-quickstarts#-tournament-scheduling[Tournament Scheduling^] -* Using Timefold Solver -** xref:using-timefold-solver/overview.adoc[leveloffset=+1] +* xref:using-timefold-solver/overview.adoc[Using Timefold Solver] ** xref:using-timefold-solver/modeling-planning-problems.adoc[leveloffset=+1] ** xref:service/overview.adoc[Run as a Service (Preview)] *** xref:service/rest-api.adoc[leveloffset=+1] From 2f492f4686397c40f19aadc2149191246dc5c7cc Mon Sep 17 00:00:00 2001 From: Tom Cools Date: Fri, 26 Jun 2026 14:49:31 +0200 Subject: [PATCH 04/17] docs: review comments --- docs/src/modules/ROOT/nav.adoc | 2 +- .../library-integration.adoc | 32 +------------------ .../modeling-planning-problems.adoc | 30 +++++++++++++++++ 3 files changed, 32 insertions(+), 32 deletions(-) diff --git a/docs/src/modules/ROOT/nav.adoc b/docs/src/modules/ROOT/nav.adoc index eb0093c0d55..b351a52a0f8 100644 --- a/docs/src/modules/ROOT/nav.adoc +++ b/docs/src/modules/ROOT/nav.adoc @@ -33,7 +33,6 @@ *** xref:service/exposing-metrics.adoc[leveloffset=+1] ** xref:using-timefold-solver/library-integration.adoc[Use as a Library (Advanced)] *** xref:using-timefold-solver/configuration.adoc[leveloffset=+1] -** xref:using-timefold-solver/running-the-solver.adoc[leveloffset=+1] * Constraints and score ** xref:constraints-and-score/overview.adoc[leveloffset=+1] ** xref:constraints-and-score/score-calculation.adoc[leveloffset=+1] @@ -43,6 +42,7 @@ ** xref:constraints-and-score/performance.adoc[leveloffset=+1] * Tuning the Solver +** xref:using-timefold-solver/running-the-solver.adoc[leveloffset=+1] ** xref:using-timefold-solver/benchmarking-and-tweaking.adoc[leveloffset=+1] ** xref:optimization-algorithms/overview.adoc[Optimization algorithms] *** xref:optimization-algorithms/construction-heuristics.adoc[leveloffset=+1] diff --git a/docs/src/modules/ROOT/pages/using-timefold-solver/library-integration.adoc b/docs/src/modules/ROOT/pages/using-timefold-solver/library-integration.adoc index 9bfab7089fa..6a746872574 100644 --- a/docs/src/modules/ROOT/pages/using-timefold-solver/library-integration.adoc +++ b/docs/src/modules/ROOT/pages/using-timefold-solver/library-integration.adoc @@ -10,7 +10,7 @@ When embedding Timefold Solver as a library, your code drives the entire solving This page covers what that means in practice. [#howLibrarySolvingWorks] -== How library solving works +== Solver basics At its core, a `Solver` takes a planning problem and returns the best solution it finds within the configured time limit: @@ -368,36 +368,6 @@ to limit the number of best solution events fired over any period of time. Open-source users may implement their own throttling mechanism within the `Consumer` itself. ==== -[#noSolverModelInterface] -== No `SolverModel` interface - -In library mode, your `@PlanningSolution` class is a plain Java class with Timefold annotations. -You do not implement any framework interface on the solution class itself. - -[tabs] -==== -Java:: -+ -[source,java,options="nowrap"] ----- -@PlanningSolution -public class Timetable { - - @ProblemFactCollectionProperty - @ValueRangeProvider - private List timeslots; - - @PlanningEntityCollectionProperty - private List lessons; - - @PlanningScore - private HardSoftScore score; - - // Getters, Setters, Constructors -} ----- -==== - [#manualEnrichment] == Manual enrichment diff --git a/docs/src/modules/ROOT/pages/using-timefold-solver/modeling-planning-problems.adoc b/docs/src/modules/ROOT/pages/using-timefold-solver/modeling-planning-problems.adoc index 81fbc37e89e..f514e941959 100644 --- a/docs/src/modules/ROOT/pages/using-timefold-solver/modeling-planning-problems.adoc +++ b/docs/src/modules/ROOT/pages/using-timefold-solver/modeling-planning-problems.adoc @@ -2404,6 +2404,36 @@ whereas a planning solution will. ==== +[#noSolverModelInterface] +=== No `SolverModel` interface + +Your `@PlanningSolution` class is a plain Java class with Timefold annotations. +You do not implement any framework interface on the solution class itself. + +[tabs] +==== +Java:: ++ +[source,java,options="nowrap"] +---- +@PlanningSolution +public class Timetable { + + @ProblemFactCollectionProperty + @ValueRangeProvider + private List timeslots; + + @PlanningEntityCollectionProperty + private List lessons; + + @PlanningScore + private HardSoftScore score; + + // Getters, Setters, Constructors +} +---- +==== + [#planningEntitiesOfASolution] === Planning entities of a solution (`@PlanningEntityCollectionProperty`) From c1964a6f46c4ac37f2a78dc29a7400489791ae13 Mon Sep 17 00:00:00 2001 From: Tom Cools Date: Fri, 26 Jun 2026 16:42:44 +0200 Subject: [PATCH 05/17] docs: small restructuring --- docs/src/modules/ROOT/nav.adoc | 1 + .../benchmarking-and-tweaking.adoc | 2 +- .../running-the-solver.adoc | 548 +---------------- .../solver-diagnostics.adoc | 551 ++++++++++++++++++ 4 files changed, 555 insertions(+), 547 deletions(-) create mode 100644 docs/src/modules/ROOT/pages/using-timefold-solver/solver-diagnostics.adoc diff --git a/docs/src/modules/ROOT/nav.adoc b/docs/src/modules/ROOT/nav.adoc index b351a52a0f8..59d844284cd 100644 --- a/docs/src/modules/ROOT/nav.adoc +++ b/docs/src/modules/ROOT/nav.adoc @@ -33,6 +33,7 @@ *** xref:service/exposing-metrics.adoc[leveloffset=+1] ** xref:using-timefold-solver/library-integration.adoc[Use as a Library (Advanced)] *** xref:using-timefold-solver/configuration.adoc[leveloffset=+1] +** xref:using-timefold-solver/solver-diagnostics.adoc[leveloffset=+1] * Constraints and score ** xref:constraints-and-score/overview.adoc[leveloffset=+1] ** xref:constraints-and-score/score-calculation.adoc[leveloffset=+1] diff --git a/docs/src/modules/ROOT/pages/using-timefold-solver/benchmarking-and-tweaking.adoc b/docs/src/modules/ROOT/pages/using-timefold-solver/benchmarking-and-tweaking.adoc index b70c097eddc..51c0f25150f 100644 --- a/docs/src/modules/ROOT/pages/using-timefold-solver/benchmarking-and-tweaking.adoc +++ b/docs/src/modules/ROOT/pages/using-timefold-solver/benchmarking-and-tweaking.adoc @@ -1,5 +1,5 @@ [#benchmarker] -= Benchmarking and tweaking += Benchmarking :page-aliases: benchmarking-and-tweaking/benchmarking-and-tweaking.adoc :doctype: book :sectnums: diff --git a/docs/src/modules/ROOT/pages/using-timefold-solver/running-the-solver.adoc b/docs/src/modules/ROOT/pages/using-timefold-solver/running-the-solver.adoc index 145be43bf7d..1fd71e74fb4 100644 --- a/docs/src/modules/ROOT/pages/using-timefold-solver/running-the-solver.adoc +++ b/docs/src/modules/ROOT/pages/using-timefold-solver/running-the-solver.adoc @@ -1,6 +1,6 @@ [#useTheSolver] [#runningTimefoldSolver] -= Tuning and diagnostics += Multi-threaded solving :doctype: book :sectnums: :icons: font @@ -237,7 +237,7 @@ plug in a <>. [IMPORTANT] ==== -A xref:using-timefold-solver/running-the-solver.adoc#logging[logging level] of `debug` or `trace` causes congestion in multi-threaded Partitioned Search +A xref:using-timefold-solver/solver-diagnostics.adoc#logging[logging level] of `debug` or `trace` causes congestion in multi-threaded Partitioned Search and slows down the xref:constraints-and-score/performance.adoc#moveEvaluationSpeed[move evaluation speed]. ==== @@ -377,547 +377,3 @@ and the <> with it: ... ---- - -[#environmentMode] -== Environment mode: are there bugs in my code? - -The environment mode allows you to detect common bugs in your implementation. -It does not affect the <>. - -You can set the environment mode: - -[tabs] -==== -Service / Quarkus:: -+ -Add the following to your `application.properties`: -+ -[source,properties,options="nowrap"] ----- -quarkus.timefold.solver.environment-mode=STEP_ASSERT ----- - -Spring Boot:: -+ -Add the following to your `application.properties`: -+ -[source,properties,options="nowrap"] ----- -timefold.solver.environment-mode=STEP_ASSERT ----- - -Java:: -+ -[source,java,options="nowrap"] ----- -SolverConfig solverConfig = new SolverConfig() - ... - .withEnvironmentMode(EnvironmentMode.STEP_ASSERT); ----- - -XML:: -+ -[source,xml,options="nowrap"] ----- - - STEP_ASSERT - ... - ----- -==== - -A solver has a single `Random` instance. -Some solver configurations use the `Random` instance a lot more than others. -For example, Simulated Annealing depends highly on random numbers, while Tabu Search only depends on it to deal with score ties. -The environment mode influences the seed of that `Random` instance. - - -[#environmentModeReproducibility] -=== Reproducibility - -For the environment mode to be reproducible, -any two runs of the same dataset with the same solver configuration must have the same result at every step. -Choosing a reproducible environment mode enables you to reproduce bugs consistently. -It also allows you to benchmark certain refactorings (such as a score constraint performance optimization) fairly across runs. - -Regardless of whether the chosen environment mode itself is reproducible, -your application might still not be fully reproducible because of: - -* Use of `HashSet` (or another `Collection` which has an undefined iteration order between JVM runs) -for collections of planning entities or planning values (but not normal problem facts), -especially in the solution implementation. -Replace it with a `SequencedSet` or `SequencedCollection` implementation respectively, -such as `LinkedHashSet` or `ArrayList`. -** This also applies to `HashMap`, -which can be replaced by a `SequencedMap` implementation such as `LinkedHashMap`. -* Combining a time gradient-dependent algorithm (most notably xref:optimization-algorithms/local-search.adoc#simulatedAnnealing[Simulated Annealing]) together with -xref:optimization-algorithms/overview.adoc#timeMillisSpentTermination[time spent termination]. -A large enough difference in allocated CPU time will influence the time gradient values. -Replace Simulated Annealing with Late Acceptance, -or replace time spent termination with step count termination. - - -[#environmentModeAvailableModes] -=== Available environment modes - -The following environment modes are available, -in the order from least strict to most strict: - -- `<>` -- `<>` -- `<>` (default) -- `<>` -- `<>` -- `<>` -- `<>` - -As the environment mode becomes stricter, -the solver becomes slower, but gains more error-detection capabilities. -`STEP_ASSERT` is already slow enough to prevent its use in production. - -All modes other than `NON_REPRODUCIBLE` are <>. - - -[#environmentModeTrackedFullAssert] -==== `TRACKED_FULL_ASSERT` - -The `TRACKED_FULL_ASSERT` mode turns on all the <> assertions -and additionally tracks changes to the working solution. -This allows the solver to report exactly what variables were corrupted. - -In particular, the solver will recalculate all shadow variables from scratch on the solution after the undo and then report: - -- Genuine and shadow variables that are different between "before" and "undo". -After an undo move is evaluated, it is expected to exactly match the original working solution. - -- Variables that are different between "from scratch" and "before". -This is to detect if the solution was corrupted before the move was evaluated due to shadow variable corruption. - -- Variables that are different between "from scratch" and "undo". -This is to detect if recalculating the shadow variables from scratch is different from the incremental shadow variable calculation. - -- Missing variable events for the actual move. -Any variable that changed between the "before move" solution and the "after move" solution -without the solver being notified of the change. - -- Missing events for undo move. -Any variable that changed between the "after move" solution and "after undo move" solution -without the solver being notified of the change. - -This mode is <> (see the reproducible mode). -It is also intrusive because it calls the method `calculateScore()` more frequently than a non-assert mode. - -The `TRACKED_FULL_ASSERT` mode is by far the slowest mode, -because it clones solutions before and after each move. - - -[#environmentModeFullAssert] -==== `FULL_ASSERT` - -The `FULL_ASSERT` mode turns on all assertions and will fail-fast on a bug in a Move implementation, -a constraint, the engine itself, ... -It is also intrusive -because it calls the method `calculateScore()` more frequently than a `<>` mode, -making the `FULL_ASSERT` mode very slow. - -This mode is <>. - -NOTE: This mode is neither better nor worse than `<>` - each can catch different types of errors, on account of performing score calculations at different times. - - -[#environmentModeNonIntrusiveFullAssert] -==== `NON_INTRUSIVE_FULL_ASSERT` - -The `NON_INTRUSIVE_FULL_ASSERT` mode turns on most assertions and will fail-fast on a bug in a Move implementation, -a constraint, the engine itself, ... -It is not intrusive, -as it does not call the method `calculateScore()` more frequently than a `<>` mode. - -This mode is <>. -This mode is also very slow, on account of all the additional checks performed. - -NOTE: This mode is neither better nor worse than `<>` - each can catch different types of errors, on account of performing score calculations at different times. - - -[#environmentModeStepAssert] -==== `STEP_ASSERT` - -The `STEP_ASSERT` mode turns on most assertions (such as assert that an undoMove's score is the same as before the Move) -to fail-fast on a bug in a Move implementation, a constraint, the engine itself, ... -This makes it slow. - -This mode is <>. -It is also intrusive because it calls the method `calculateScore()` more frequently than a non-assert mode. - -TIP: We recommend that you write a test case that does a short run of your planning problem with the `STEP_ASSERT` mode on. - - -[#environmentModePhaseAssert] -==== `PHASE_ASSERT` (default) - -The `PHASE_ASSERT` is the default mode because it is recommended during development. -This mode is <> -and it gives you the benefit of quickly checking for score corruptions. -If you can guarantee that your code is and will remain bug-free, -you can switch to the `NO_ASSERT` mode for a marginal performance gain. - -In practice, this mode disables certain concurrency optimizations, such as work stealing. - - -[#environmentModeNoAssert] -==== `NO_ASSERT` - -The `NO_ASSERT` environment mode behaves in all aspects like the default `<>` mode, -except that it does not give you any protection against score corruption bugs. -As such, it can be negligibly faster. - - -[#environmentModeNonReproducible] -==== `NON_REPRODUCIBLE` - -This mode can be slightly faster than any of the other modes, -but it is not <>. -Avoid using it during development as it makes debugging and bug fixing painful. -If your production environment doesn't care about reproducibility, use this mode in production. - -Unlike all the other modes, -this mode doesn't use any fixed <> unless one is provided. - - -[#environmentModeBestPractices] -=== Best practices - -There are several best practices to follow throughout the lifecycle of your application: - -**In production**:: Use the `PHASE_ASSERT` mode if you need <>, otherwise use `NON_REPRODUCIBLE`. -**In development**:: - - Use the `PHASE_ASSERT` mode to catch bugs early. - - Write a test case that does a short run of your planning problem in `STEP_ASSERT` mode. - - Have nightly builds that run for several minutes in `FULL_ASSERT` and `NON_INTRUSIVE_FULL_ASSERT` modes. - - -[#logging] -== Logging level: what is the `Solver` doing? - -The best way to illuminate the black box that is a ``Solver``, is to play with the logging level: - -* **error**: Log errors, except those that are thrown to the calling code as a ``RuntimeException``. -+ -[NOTE] -==== -**If an error happens, Timefold Solver normally fails fast**: it throws a subclass of `RuntimeException` with a detailed message to the calling code. -It does not log it as an error itself to avoid duplicate log messages. -Except if the calling code explicitly catches and eats that ``RuntimeException``, a ``Thread``'s default `ExceptionHandler` will log it as an error anyway. -Meanwhile, the code is disrupted from doing further harm or obfuscating the error. -==== -* **warn**: Log suspicious circumstances. -* **info**: Log every phase and the solver itself. See xref:optimization-algorithms/overview.adoc#scopeOverview[scope overview]. -* **debug**: Log every step of every phase. See xref:optimization-algorithms/overview.adoc#scopeOverview[scope overview]. -* **trace**: Log every move of every step of every phase. See xref:optimization-algorithms/overview.adoc#scopeOverview[scope overview]. - -[NOTE] -==== -Turning on `trace` logging, will slow down performance considerably: it is often four times slower. -However, it is invaluable during development to discover a bottleneck. - -Even `debug` logging can slow down performance considerably for fast stepping algorithms -(such as Late Acceptance and Simulated Annealing), -but not for slow stepping algorithms (such as Tabu Search). - -Both trace logging and debug logging cause congestion in xref:using-timefold-solver/running-the-solver.adoc#multithreadedSolving[multi-threaded solving] with most appenders, -see below. - -In Eclipse, `debug` logging to the console tends to cause congestion with move evaluation speeds above 10 000 per second. -Nor IntelliJ, nor the Maven command line suffer from this problem. -==== - -For example, set it to `debug` logging, to see when the phases end and how fast steps are taken: - -[source,options="nowrap"] ----- -INFO Solving started: time spent (31), best score (0hard/0soft), environment mode (PHASE_ASSERT), move thread count (NONE), random (JDK with seed 0). -INFO Problem scale: entity count (4), variable count (8), approximate value count (4), approximate problem scale (256). -DEBUG CH step (0), time spent (47), score (0hard/0soft), selected move count (4), picked move ([Math(0) {null -> Room A}, Math(0) {null -> MONDAY 08:30}]). -DEBUG CH step (1), time spent (50), score (0hard/0soft), selected move count (4), picked move ([Physics(1) {null -> Room A}, Physics(1) {null -> MONDAY 09:30}]). -DEBUG CH step (2), time spent (51), score (-1hard/-1soft), selected move count (4), picked move ([Chemistry(2) {null -> Room B}, Chemistry(2) {null -> MONDAY 08:30}]). -DEBUG CH step (3), time spent (52), score (-2hard/-1soft), selected move count (4), picked move ([Biology(3) {null -> Room A}, Biology(3) {null -> MONDAY 08:30}]). -INFO Construction Heuristic phase (0) ended: time spent (53), best score (-2hard/-1soft), move evaluation speed (1066/sec), step total (4). -DEBUG LS step (0), time spent (56), score (-2hard/0soft), new best score (-2hard/0soft), accepted/selected move count (1/1), picked move (Chemistry(2) {Room B, MONDAY 08:30} <-> Physics(1) {Room A, MONDAY 09:30}). -DEBUG LS step (1), time spent (60), score (-2hard/1soft), new best score (-2hard/1soft), accepted/selected move count (1/2), picked move (Math(0) {Room A, MONDAY 08:30} <-> Physics(1) {Room B, MONDAY 08:30}). -DEBUG LS step (2), time spent (60), score (-2hard/0soft), best score (-2hard/1soft), accepted/selected move count (1/1), picked move (Math(0) {Room B, MONDAY 08:30} <-> Physics(1) {Room A, MONDAY 08:30}). -... -INFO Local Search phase (1) ended: time spent (100), best score (0hard/1soft), move evaluation speed (2021/sec), step total (59). -INFO Solving ended: time spent (100), best score (0hard/1soft), move evaluation speed (1100/sec), phase total (2), environment mode (PHASE_ASSERT), move thread count (NONE). ----- -All time spent values are in milliseconds. - -[tabs] -==== -Java:: -+ -Everything is logged to http://www.slf4j.org/[SLF4J], which is a simple logging facade -which delegates every log message to Logback, Apache Commons Logging, Log4j or java.util.logging. -Add a dependency to the logging adaptor for your logging framework of choice. -+ -If you are not using any logging framework yet, use Logback by adding this Maven dependency (there is no need to add an extra bridge dependency): -+ -[source,xml,options="nowrap"] ----- - - ch.qos.logback - logback-classic - 1.x - ----- -+ -Configure the logging level on the `ai.timefold.solver` package in your `logback.xml` file: -+ -[source,xml,options="nowrap"] ----- - - - - - ... - - ----- -+ -If it isn't picked up, temporarily add the system property `-Dlogback.debug=true` to figure out why. -==== - -[NOTE] -==== -When running multiple solvers or a xref:using-timefold-solver/running-the-solver.adoc#multithreadedSolving[multi-threaded solver], -most appenders (including the console) cause congestion with `debug` and `trace` logging. -Switch to an async appender to avoid this problem or turn off `debug` logging. -==== - -[NOTE] -==== -In a multitenant application, multiple `Solver` instances might be running at the same time. -To separate their logging into distinct files, surround the `solve()` call with an http://logback.qos.ch/manual/mdc.html[MDC]: - -[source,java,options="nowrap"] ----- - MDC.put("tenant.name", tenantName); - MySolution bestSolution = solver.solve(problem); - MDC.remove("tenant.name"); ----- - -Then configure your logger to use different files for each ``${tenant.name}``. -In Logback, use a `SiftingAppender` in ``logback.xml``: - -[source,xml,options="nowrap"] ----- - - - tenant.name - unknown - - - - local/log/timefold-solver-${tenant.name}.log - ... - - - ----- -==== - -[#monitoring] -== Monitoring the solver - -Timefold Solver exposes metrics through https://micrometer.io/[Micrometer] which you can use to monitor the solver. - -[tabs] -==== -Service:: -+ -Micrometer is automatically configured in the service module. -See xref:service/exposing-metrics.adoc[Exposing Metrics] for setup instructions and the list of available metrics. - -Quarkus / Spring Boot:: -+ -Timefold automatically connects to configured Micrometer registries in Quarkus and Spring Boot. -Add the appropriate Micrometer registry dependency for your monitoring system. - -Java:: -+ -When using plain Java, you must add the metrics registry to the global registry manually. - -.Prerequisites -* You have configured a Micrometer registry. For information about configuring Micrometer registries, see the https://micrometer.io[Micrometer] web site. - -.Procedure -. Add configuration information for the Micrometer registry for your desired monitoring system to the global registry. -. Add the following line below the configuration information, where `` is the name of the registry that you configured: -+ -[source,java,options="nowrap"] ----- -Metrics.addRegistry(); ----- -The following example shows how to add the Prometheus registry: -+ -[source,java,options="nowrap"] ----- -PrometheusMeterRegistry prometheusRegistry = new PrometheusMeterRegistry(PrometheusConfig.DEFAULT); - -try { - HttpServer server = HttpServer.create(new InetSocketAddress(8080), 0); - server.createContext("/prometheus", httpExchange -> { - String response = prometheusRegistry.scrape(); (1) - httpExchange.sendResponseHeaders(200, response.getBytes().length); - try (OutputStream os = httpExchange.getResponseBody()) { - os.write(response.getBytes()); - } - }); - - new Thread(server::start).start(); -} catch (IOException e) { - throw new RuntimeException(e); -} - -Metrics.addRegistry(prometheusRegistry); ----- -==== - -The following metrics are exposed: -+ -[NOTE] -==== -The names and format of the metrics vary depending on the registry. -==== -+ -* `timefold.solver.errors.total`: the total number of errors that occurred while solving since the start -of the measuring. -* `timefold.solver.solve.duration.active-count`: the number of solvers currently solving. -* `timefold.solver.solve.duration.seconds-max`: run time of the -longest-running currently active solver. -* `timefold.solver.solve.duration.seconds-duration-sum`: the sum of each active solver's solve duration. For example, if there are two active solvers, one running for three minutes and the other for one minute, the total solve time is four minutes. - -=== Additional metrics - -For more detailed monitoring, Timefold Solver can be configured to monitor additional metrics at a performance cost. - -[source,xml,options="nowrap"] ----- - - - BEST_SCORE - SCORE_CALCULATION_COUNT - ... - - ... - ----- - -The following metrics are available: - -- `SOLVE_DURATION` (default, Micrometer meter id: "timefold.solver.solve.duration"): -Measurse the duration of solving for the longest active solver, the number of active solvers and the cumulative duration of all active solvers. - -- `ERROR_COUNT` (default, Micrometer meter id: "timefold.solver.errors"): -Measures the number of errors that occur while solving. - -- `SCORE_CALCULATION_COUNT` (default, Micrometer meter id: "timefold.solver.score.calculation.count"): -Measures the number of score calculations Timefold Solver performed. - -- `MOVE_EVALUATION_COUNT` (default, Micrometer meter id: "timefold.solver.move.evaluation.count"): -Measures the number of move evaluations Timefold Solver performed. - -- `PROBLEM_ENTITY_COUNT` (default, Micrometer meter id: "timefold.solver.problem.entities"): -Measures the number of entities in the problem submitted to Timefold Solver. - -- `PROBLEM_VARIABLE_COUNT` (default, Micrometer meter id: "timefold.solver.problem.variables"): -Measures the number of genuine variables in the problem submitted to Timefold Solver. - -- `PROBLEM_VALUE_COUNT` (default, Micrometer meter id: "timefold.solver.problem.values"): -Measures the approximate number of planning values in the problem submitted to Timefold Solver. - -- `PROBLEM_SIZE_LOG` (default, Micrometer meter id: "timefold.solver.problem.size.log"): -Measures the approximate log 10 of the search space size for the problem submitted to Timefold Solver. - -- `BEST_SCORE` (Micrometer meter id: "timefold.solver.best.score.*"): -Measures the score of the best solution Timefold Solver found so far. -There are separate meters for each level of the score. -For instance, for a `HardSoftScore`, there are `timefold.solver.best.score.hard.score` and `timefold.solver.best.score.soft.score` meters. - -- `STEP_SCORE` (Micrometer meter id: "timefold.solver.step.score.*"): -Measures the score of each step Timefold Solver takes. -There are separate meters for each level of the score. -For instance, for a `HardSoftScore`, there are `timefold.solver.step.score.hard.score` and `timefold.solver.step.score.soft.score` meters. - -- `BEST_SOLUTION_MUTATION` (Micrometer meter id: "timefold.solver.best.solution.mutation"): -Measures the number of changed planning variables between consecutive best solutions. - -- `MOVE_COUNT_PER_STEP` (Micrometer meter id: "timefold.solver.step.move.count"): -Measures the number of moves evaluated in a step. - -- `MOVE_COUNT_PER_TYPE` (Micrometer meter id: "timefold.solver.move.type.count"): -Measures the number of moves evaluated per move type. - -- `MEMORY_USE` (Micrometer meter id: "jvm.memory.used"): -Measures the amount of memory used across the JVM. -This does not measure the amount of memory used by a solver; two solvers on the same JVM will report the same value for this metric. - -- `CONSTRAINT_MATCH_TOTAL_BEST_SCORE` (Micrometer meter id: "timefold.solver.constraint.match.best.score.*"): -Measures the score impact of each constraint on the best solution Timefold Solver found so far. -There are separate meters for each level of the score, with tags for each constraint. -For instance, for a `HardSoftScore` for a constraint "Minimize Cost", -there are `timefold.solver.constraint.match.best.score.hard.score` and `timefold.solver.constraint.match.best.score.soft.score` meters with a tag "constraint.id=Minimize Cost". - -- `CONSTRAINT_MATCH_TOTAL_STEP_SCORE` (Micrometer meter id: "timefold.solver.constraint.match.step.score.*"): -Measures the score impact of each constraint on the current step. -There are separate meters for each level of the score, with tags for each constraint. -For instance, for a `HardSoftScore` for a constraint "Minimize Cost", -there are `timefold.solver.constraint.match.step.score.hard.score` and `timefold.solver.constraint.match.step.score.soft.score` meters with a tag "constraint.id=Minimize Cost". - -- `PICKED_MOVE_TYPE_BEST_SCORE_DIFF` (Micrometer meter id: "timefold.solver.move.type.best.score.diff.*"): -Measures how much a particular move type improves the best solution. -There are separate meters for each level of the score, with a tag for the move type. -For instance, for a `HardSoftScore` and a `ChangeMove` for the room of a lesson, -there are `timefold.solver.move.type.best.score.diff.hard.score` and `timefold.solver.move.type.best.score.diff.soft.score` meters with the tag `move.type=ChangeMove(Lesson.room)`. - -- `PICKED_MOVE_TYPE_STEP_SCORE_DIFF` (Micrometer meter id: "timefold.solver.move.type.step.score.diff.*"): -Measures how much a particular move type improves the best solution. -There are separate meters for each level of the score, with a tag for the move type. -For instance, for a `HardSoftScore` and a `ChangeMove` for the room of a lesson, -there are `timefold.solver.move.type.step.score.diff.hard.score` and `timefold.solver.move.type.step.score.diff.soft.score` meters with the tag `move.type=ChangeMove(Lesson.room)`. - -[#randomNumberGenerator] -== Random number generator - -Many heuristics and metaheuristics depend on a pseudorandom number generator for move selection, to resolve score ties, probability based move acceptance, ... During solving, the same `RandomGenerator` instance is reused to improve reproducibility, performance and uniform distribution of random values. - -To change the random seed of that `RandomGenerator` instance, specify a ``randomSeed``: - -[tabs] -==== -Service / Quarkus:: -+ -[source,properties,options="nowrap"] ----- -quarkus.timefold.solver.random-seed=0 ----- - -Spring Boot:: -+ -[source,properties,options="nowrap"] ----- -timefold.solver.random-seed=0 ----- - -XML:: -+ -[source,xml,options="nowrap"] ----- - - 0 - ... - ----- -==== diff --git a/docs/src/modules/ROOT/pages/using-timefold-solver/solver-diagnostics.adoc b/docs/src/modules/ROOT/pages/using-timefold-solver/solver-diagnostics.adoc new file mode 100644 index 00000000000..b07dcd4b128 --- /dev/null +++ b/docs/src/modules/ROOT/pages/using-timefold-solver/solver-diagnostics.adoc @@ -0,0 +1,551 @@ +[#solverDiagnostics] += Solver diagnostics +:doctype: book +:sectnums: +:icons: font + + +[#environmentMode] +== Environment mode: are there bugs in my code? + +The environment mode allows you to detect common bugs in your implementation. +It does not affect the <>. + +You can set the environment mode: + +[tabs] +==== +Service / Quarkus:: ++ +Add the following to your `application.properties`: ++ +[source,properties,options="nowrap"] +---- +quarkus.timefold.solver.environment-mode=STEP_ASSERT +---- + +Spring Boot:: ++ +Add the following to your `application.properties`: ++ +[source,properties,options="nowrap"] +---- +timefold.solver.environment-mode=STEP_ASSERT +---- + +Java:: ++ +[source,java,options="nowrap"] +---- +SolverConfig solverConfig = new SolverConfig() + ... + .withEnvironmentMode(EnvironmentMode.STEP_ASSERT); +---- + +XML:: ++ +[source,xml,options="nowrap"] +---- + + STEP_ASSERT + ... + +---- +==== + +A solver has a single `Random` instance. +Some solver configurations use the `Random` instance a lot more than others. +For example, Simulated Annealing depends highly on random numbers, while Tabu Search only depends on it to deal with score ties. +The environment mode influences the seed of that `Random` instance. + + +[#environmentModeReproducibility] +=== Reproducibility + +For the environment mode to be reproducible, +any two runs of the same dataset with the same solver configuration must have the same result at every step. +Choosing a reproducible environment mode enables you to reproduce bugs consistently. +It also allows you to benchmark certain refactorings (such as a score constraint performance optimization) fairly across runs. + +Regardless of whether the chosen environment mode itself is reproducible, +your application might still not be fully reproducible because of: + +* Use of `HashSet` (or another `Collection` which has an undefined iteration order between JVM runs) +for collections of planning entities or planning values (but not normal problem facts), +especially in the solution implementation. +Replace it with a `SequencedSet` or `SequencedCollection` implementation respectively, +such as `LinkedHashSet` or `ArrayList`. +** This also applies to `HashMap`, +which can be replaced by a `SequencedMap` implementation such as `LinkedHashMap`. +* Combining a time gradient-dependent algorithm (most notably xref:optimization-algorithms/local-search.adoc#simulatedAnnealing[Simulated Annealing]) together with +xref:optimization-algorithms/overview.adoc#timeMillisSpentTermination[time spent termination]. +A large enough difference in allocated CPU time will influence the time gradient values. +Replace Simulated Annealing with Late Acceptance, +or replace time spent termination with step count termination. + + +[#environmentModeAvailableModes] +=== Available environment modes + +The following environment modes are available, +in the order from least strict to most strict: + +- `<>` +- `<>` +- `<>` (default) +- `<>` +- `<>` +- `<>` +- `<>` + +As the environment mode becomes stricter, +the solver becomes slower, but gains more error-detection capabilities. +`STEP_ASSERT` is already slow enough to prevent its use in production. + +All modes other than `NON_REPRODUCIBLE` are <>. + + +[#environmentModeTrackedFullAssert] +==== `TRACKED_FULL_ASSERT` + +The `TRACKED_FULL_ASSERT` mode turns on all the <> assertions +and additionally tracks changes to the working solution. +This allows the solver to report exactly what variables were corrupted. + +In particular, the solver will recalculate all shadow variables from scratch on the solution after the undo and then report: + +- Genuine and shadow variables that are different between "before" and "undo". +After an undo move is evaluated, it is expected to exactly match the original working solution. + +- Variables that are different between "from scratch" and "before". +This is to detect if the solution was corrupted before the move was evaluated due to shadow variable corruption. + +- Variables that are different between "from scratch" and "undo". +This is to detect if recalculating the shadow variables from scratch is different from the incremental shadow variable calculation. + +- Missing variable events for the actual move. +Any variable that changed between the "before move" solution and the "after move" solution +without the solver being notified of the change. + +- Missing events for undo move. +Any variable that changed between the "after move" solution and "after undo move" solution +without the solver being notified of the change. + +This mode is <> (see the reproducible mode). +It is also intrusive because it calls the method `calculateScore()` more frequently than a non-assert mode. + +The `TRACKED_FULL_ASSERT` mode is by far the slowest mode, +because it clones solutions before and after each move. + + +[#environmentModeFullAssert] +==== `FULL_ASSERT` + +The `FULL_ASSERT` mode turns on all assertions and will fail-fast on a bug in a Move implementation, +a constraint, the engine itself, ... +It is also intrusive +because it calls the method `calculateScore()` more frequently than a `<>` mode, +making the `FULL_ASSERT` mode very slow. + +This mode is <>. + +NOTE: This mode is neither better nor worse than `<>` - each can catch different types of errors, on account of performing score calculations at different times. + + +[#environmentModeNonIntrusiveFullAssert] +==== `NON_INTRUSIVE_FULL_ASSERT` + +The `NON_INTRUSIVE_FULL_ASSERT` mode turns on most assertions and will fail-fast on a bug in a Move implementation, +a constraint, the engine itself, ... +It is not intrusive, +as it does not call the method `calculateScore()` more frequently than a `<>` mode. + +This mode is <>. +This mode is also very slow, on account of all the additional checks performed. + +NOTE: This mode is neither better nor worse than `<>` - each can catch different types of errors, on account of performing score calculations at different times. + + +[#environmentModeStepAssert] +==== `STEP_ASSERT` + +The `STEP_ASSERT` mode turns on most assertions (such as assert that an undoMove's score is the same as before the Move) +to fail-fast on a bug in a Move implementation, a constraint, the engine itself, ... +This makes it slow. + +This mode is <>. +It is also intrusive because it calls the method `calculateScore()` more frequently than a non-assert mode. + +TIP: We recommend that you write a test case that does a short run of your planning problem with the `STEP_ASSERT` mode on. + + +[#environmentModePhaseAssert] +==== `PHASE_ASSERT` (default) + +The `PHASE_ASSERT` is the default mode because it is recommended during development. +This mode is <> +and it gives you the benefit of quickly checking for score corruptions. +If you can guarantee that your code is and will remain bug-free, +you can switch to the `NO_ASSERT` mode for a marginal performance gain. + +In practice, this mode disables certain concurrency optimizations, such as work stealing. + + +[#environmentModeNoAssert] +==== `NO_ASSERT` + +The `NO_ASSERT` environment mode behaves in all aspects like the default `<>` mode, +except that it does not give you any protection against score corruption bugs. +As such, it can be negligibly faster. + + +[#environmentModeNonReproducible] +==== `NON_REPRODUCIBLE` + +This mode can be slightly faster than any of the other modes, +but it is not <>. +Avoid using it during development as it makes debugging and bug fixing painful. +If your production environment doesn't care about reproducibility, use this mode in production. + +Unlike all the other modes, +this mode doesn't use any fixed <> unless one is provided. + + +[#environmentModeBestPractices] +=== Best practices + +There are several best practices to follow throughout the lifecycle of your application: + +**In production**:: Use the `PHASE_ASSERT` mode if you need <>, otherwise use `NON_REPRODUCIBLE`. +**In development**:: + - Use the `PHASE_ASSERT` mode to catch bugs early. + - Write a test case that does a short run of your planning problem in `STEP_ASSERT` mode. + - Have nightly builds that run for several minutes in `FULL_ASSERT` and `NON_INTRUSIVE_FULL_ASSERT` modes. + + +[#randomNumberGenerator] +== Random number generator + +Many heuristics and metaheuristics depend on a pseudorandom number generator for move selection, to resolve score ties, probability based move acceptance, ... During solving, the same `RandomGenerator` instance is reused to improve reproducibility, performance and uniform distribution of random values. + +To change the random seed of that `RandomGenerator` instance, specify a ``randomSeed``: + +[tabs] +==== +Service / Quarkus:: ++ +[source,properties,options="nowrap"] +---- +quarkus.timefold.solver.random-seed=0 +---- + +Spring Boot:: ++ +[source,properties,options="nowrap"] +---- +timefold.solver.random-seed=0 +---- + +XML:: ++ +[source,xml,options="nowrap"] +---- + + 0 + ... + +---- +==== + + +[#logging] +== Logging level: what is the `Solver` doing? + +The best way to illuminate the black box that is a ``Solver``, is to play with the logging level: + +* **error**: Log errors, except those that are thrown to the calling code as a ``RuntimeException``. ++ +[NOTE] +==== +**If an error happens, Timefold Solver normally fails fast**: it throws a subclass of `RuntimeException` with a detailed message to the calling code. +It does not log it as an error itself to avoid duplicate log messages. +Except if the calling code explicitly catches and eats that ``RuntimeException``, a ``Thread``'s default `ExceptionHandler` will log it as an error anyway. +Meanwhile, the code is disrupted from doing further harm or obfuscating the error. +==== +* **warn**: Log suspicious circumstances. +* **info**: Log every phase and the solver itself. See xref:optimization-algorithms/overview.adoc#scopeOverview[scope overview]. +* **debug**: Log every step of every phase. See xref:optimization-algorithms/overview.adoc#scopeOverview[scope overview]. +* **trace**: Log every move of every step of every phase. See xref:optimization-algorithms/overview.adoc#scopeOverview[scope overview]. + +[NOTE] +==== +Turning on `trace` logging, will slow down performance considerably: it is often four times slower. +However, it is invaluable during development to discover a bottleneck. + +Even `debug` logging can slow down performance considerably for fast stepping algorithms +(such as Late Acceptance and Simulated Annealing), +but not for slow stepping algorithms (such as Tabu Search). + +Both trace logging and debug logging cause congestion in xref:using-timefold-solver/running-the-solver.adoc#multithreadedSolving[multi-threaded solving] with most appenders, +see below. + +In Eclipse, `debug` logging to the console tends to cause congestion with move evaluation speeds above 10 000 per second. +Nor IntelliJ, nor the Maven command line suffer from this problem. +==== + +For example, set it to `debug` logging, to see when the phases end and how fast steps are taken: + +[source,options="nowrap"] +---- +INFO Solving started: time spent (31), best score (0hard/0soft), environment mode (PHASE_ASSERT), move thread count (NONE), random (JDK with seed 0). +INFO Problem scale: entity count (4), variable count (8), approximate value count (4), approximate problem scale (256). +DEBUG CH step (0), time spent (47), score (0hard/0soft), selected move count (4), picked move ([Math(0) {null -> Room A}, Math(0) {null -> MONDAY 08:30}]). +DEBUG CH step (1), time spent (50), score (0hard/0soft), selected move count (4), picked move ([Physics(1) {null -> Room A}, Physics(1) {null -> MONDAY 09:30}]). +DEBUG CH step (2), time spent (51), score (-1hard/-1soft), selected move count (4), picked move ([Chemistry(2) {null -> Room B}, Chemistry(2) {null -> MONDAY 08:30}]). +DEBUG CH step (3), time spent (52), score (-2hard/-1soft), selected move count (4), picked move ([Biology(3) {null -> Room A}, Biology(3) {null -> MONDAY 08:30}]). +INFO Construction Heuristic phase (0) ended: time spent (53), best score (-2hard/-1soft), move evaluation speed (1066/sec), step total (4). +DEBUG LS step (0), time spent (56), score (-2hard/0soft), new best score (-2hard/0soft), accepted/selected move count (1/1), picked move (Chemistry(2) {Room B, MONDAY 08:30} <-> Physics(1) {Room A, MONDAY 09:30}). +DEBUG LS step (1), time spent (60), score (-2hard/1soft), new best score (-2hard/1soft), accepted/selected move count (1/2), picked move (Math(0) {Room A, MONDAY 08:30} <-> Physics(1) {Room B, MONDAY 08:30}). +DEBUG LS step (2), time spent (60), score (-2hard/0soft), best score (-2hard/1soft), accepted/selected move count (1/1), picked move (Math(0) {Room B, MONDAY 08:30} <-> Physics(1) {Room A, MONDAY 08:30}). +... +INFO Local Search phase (1) ended: time spent (100), best score (0hard/1soft), move evaluation speed (2021/sec), step total (59). +INFO Solving ended: time spent (100), best score (0hard/1soft), move evaluation speed (1100/sec), phase total (2), environment mode (PHASE_ASSERT), move thread count (NONE). +---- +All time spent values are in milliseconds. + +[tabs] +==== +Java:: ++ +Everything is logged to http://www.slf4j.org/[SLF4J], which is a simple logging facade +which delegates every log message to Logback, Apache Commons Logging, Log4j or java.util.logging. +Add a dependency to the logging adaptor for your logging framework of choice. ++ +If you are not using any logging framework yet, use Logback by adding this Maven dependency (there is no need to add an extra bridge dependency): ++ +[source,xml,options="nowrap"] +---- + + ch.qos.logback + logback-classic + 1.x + +---- ++ +Configure the logging level on the `ai.timefold.solver` package in your `logback.xml` file: ++ +[source,xml,options="nowrap"] +---- + + + + + ... + + +---- ++ +If it isn't picked up, temporarily add the system property `-Dlogback.debug=true` to figure out why. +==== + +[NOTE] +==== +When running multiple solvers or a xref:using-timefold-solver/running-the-solver.adoc#multithreadedSolving[multi-threaded solver], +most appenders (including the console) cause congestion with `debug` and `trace` logging. +Switch to an async appender to avoid this problem or turn off `debug` logging. +==== + +[NOTE] +==== +In a multitenant application, multiple `Solver` instances might be running at the same time. +To separate their logging into distinct files, surround the `solve()` call with an http://logback.qos.ch/manual/mdc.html[MDC]: + +[source,java,options="nowrap"] +---- + MDC.put("tenant.name", tenantName); + MySolution bestSolution = solver.solve(problem); + MDC.remove("tenant.name"); +---- + +Then configure your logger to use different files for each ``${tenant.name}``. +In Logback, use a `SiftingAppender` in ``logback.xml``: + +[source,xml,options="nowrap"] +---- + + + tenant.name + unknown + + + + local/log/timefold-solver-${tenant.name}.log + ... + + + +---- +==== + +[#monitoring] +== Monitoring the solver + +Timefold Solver exposes metrics through https://micrometer.io/[Micrometer] which you can use to monitor the solver. + +[tabs] +==== +Service:: ++ +Micrometer is automatically configured in the service module. +See xref:service/exposing-metrics.adoc[Exposing Metrics] for setup instructions and the list of available metrics. + +Quarkus / Spring Boot:: ++ +Timefold automatically connects to configured Micrometer registries in Quarkus and Spring Boot. +Add the appropriate Micrometer registry dependency for your monitoring system. + +Java:: ++ +When using plain Java, you must add the metrics registry to the global registry manually. + +.Prerequisites +* You have configured a Micrometer registry. For information about configuring Micrometer registries, see the https://micrometer.io[Micrometer] web site. + +.Procedure +. Add configuration information for the Micrometer registry for your desired monitoring system to the global registry. +. Add the following line below the configuration information, where `` is the name of the registry that you configured: ++ +[source,java,options="nowrap"] +---- +Metrics.addRegistry(); +---- +The following example shows how to add the Prometheus registry: ++ +[source,java,options="nowrap"] +---- +PrometheusMeterRegistry prometheusRegistry = new PrometheusMeterRegistry(PrometheusConfig.DEFAULT); + +try { + HttpServer server = HttpServer.create(new InetSocketAddress(8080), 0); + server.createContext("/prometheus", httpExchange -> { + String response = prometheusRegistry.scrape(); (1) + httpExchange.sendResponseHeaders(200, response.getBytes().length); + try (OutputStream os = httpExchange.getResponseBody()) { + os.write(response.getBytes()); + } + }); + + new Thread(server::start).start(); +} catch (IOException e) { + throw new RuntimeException(e); +} + +Metrics.addRegistry(prometheusRegistry); +---- +==== + +The following metrics are exposed: ++ +[NOTE] +==== +The names and format of the metrics vary depending on the registry. +==== ++ +* `timefold.solver.errors.total`: the total number of errors that occurred while solving since the start +of the measuring. +* `timefold.solver.solve.duration.active-count`: the number of solvers currently solving. +* `timefold.solver.solve.duration.seconds-max`: run time of the +longest-running currently active solver. +* `timefold.solver.solve.duration.seconds-duration-sum`: the sum of each active solver's solve duration. For example, if there are two active solvers, one running for three minutes and the other for one minute, the total solve time is four minutes. + +=== Additional metrics + +For more detailed monitoring, Timefold Solver can be configured to monitor additional metrics at a performance cost. + +[source,xml,options="nowrap"] +---- + + + BEST_SCORE + SCORE_CALCULATION_COUNT + ... + + ... + +---- + +The following metrics are available: + +- `SOLVE_DURATION` (default, Micrometer meter id: "timefold.solver.solve.duration"): +Measurse the duration of solving for the longest active solver, the number of active solvers and the cumulative duration of all active solvers. + +- `ERROR_COUNT` (default, Micrometer meter id: "timefold.solver.errors"): +Measures the number of errors that occur while solving. + +- `SCORE_CALCULATION_COUNT` (default, Micrometer meter id: "timefold.solver.score.calculation.count"): +Measures the number of score calculations Timefold Solver performed. + +- `MOVE_EVALUATION_COUNT` (default, Micrometer meter id: "timefold.solver.move.evaluation.count"): +Measures the number of move evaluations Timefold Solver performed. + +- `PROBLEM_ENTITY_COUNT` (default, Micrometer meter id: "timefold.solver.problem.entities"): +Measures the number of entities in the problem submitted to Timefold Solver. + +- `PROBLEM_VARIABLE_COUNT` (default, Micrometer meter id: "timefold.solver.problem.variables"): +Measures the number of genuine variables in the problem submitted to Timefold Solver. + +- `PROBLEM_VALUE_COUNT` (default, Micrometer meter id: "timefold.solver.problem.values"): +Measures the approximate number of planning values in the problem submitted to Timefold Solver. + +- `PROBLEM_SIZE_LOG` (default, Micrometer meter id: "timefold.solver.problem.size.log"): +Measures the approximate log 10 of the search space size for the problem submitted to Timefold Solver. + +- `BEST_SCORE` (Micrometer meter id: "timefold.solver.best.score.*"): +Measures the score of the best solution Timefold Solver found so far. +There are separate meters for each level of the score. +For instance, for a `HardSoftScore`, there are `timefold.solver.best.score.hard.score` and `timefold.solver.best.score.soft.score` meters. + +- `STEP_SCORE` (Micrometer meter id: "timefold.solver.step.score.*"): +Measures the score of each step Timefold Solver takes. +There are separate meters for each level of the score. +For instance, for a `HardSoftScore`, there are `timefold.solver.step.score.hard.score` and `timefold.solver.step.score.soft.score` meters. + +- `BEST_SOLUTION_MUTATION` (Micrometer meter id: "timefold.solver.best.solution.mutation"): +Measures the number of changed planning variables between consecutive best solutions. + +- `MOVE_COUNT_PER_STEP` (Micrometer meter id: "timefold.solver.step.move.count"): +Measures the number of moves evaluated in a step. + +- `MOVE_COUNT_PER_TYPE` (Micrometer meter id: "timefold.solver.move.type.count"): +Measures the number of moves evaluated per move type. + +- `MEMORY_USE` (Micrometer meter id: "jvm.memory.used"): +Measures the amount of memory used across the JVM. +This does not measure the amount of memory used by a solver; two solvers on the same JVM will report the same value for this metric. + +- `CONSTRAINT_MATCH_TOTAL_BEST_SCORE` (Micrometer meter id: "timefold.solver.constraint.match.best.score.*"): +Measures the score impact of each constraint on the best solution Timefold Solver found so far. +There are separate meters for each level of the score, with tags for each constraint. +For instance, for a `HardSoftScore` for a constraint "Minimize Cost", +there are `timefold.solver.constraint.match.best.score.hard.score` and `timefold.solver.constraint.match.best.score.soft.score` meters with a tag "constraint.id=Minimize Cost". + +- `CONSTRAINT_MATCH_TOTAL_STEP_SCORE` (Micrometer meter id: "timefold.solver.constraint.match.step.score.*"): +Measures the score impact of each constraint on the current step. +There are separate meters for each level of the score, with tags for each constraint. +For instance, for a `HardSoftScore` for a constraint "Minimize Cost", +there are `timefold.solver.constraint.match.step.score.hard.score` and `timefold.solver.constraint.match.step.score.soft.score` meters with a tag "constraint.id=Minimize Cost". + +- `PICKED_MOVE_TYPE_BEST_SCORE_DIFF` (Micrometer meter id: "timefold.solver.move.type.best.score.diff.*"): +Measures how much a particular move type improves the best solution. +There are separate meters for each level of the score, with a tag for the move type. +For instance, for a `HardSoftScore` and a `ChangeMove` for the room of a lesson, +there are `timefold.solver.move.type.best.score.diff.hard.score` and `timefold.solver.move.type.best.score.diff.soft.score` meters with the tag `move.type=ChangeMove(Lesson.room)`. + +- `PICKED_MOVE_TYPE_STEP_SCORE_DIFF` (Micrometer meter id: "timefold.solver.move.type.step.score.diff.*"): +Measures how much a particular move type improves the best solution. +There are separate meters for each level of the score, with a tag for the move type. +For instance, for a `HardSoftScore` and a `ChangeMove` for the room of a lesson, +there are `timefold.solver.move.type.step.score.diff.hard.score` and `timefold.solver.move.type.step.score.diff.soft.score` meters with the tag `move.type=ChangeMove(Lesson.room)`. From 4fd6b567f3e44d68ea12149a29b0f24e17903213 Mon Sep 17 00:00:00 2001 From: Tom Cools Date: Fri, 26 Jun 2026 16:53:03 +0200 Subject: [PATCH 06/17] docs: add abstract class reference. --- .../modeling-planning-problems.adoc | 128 +++++++++++++----- 1 file changed, 93 insertions(+), 35 deletions(-) diff --git a/docs/src/modules/ROOT/pages/using-timefold-solver/modeling-planning-problems.adoc b/docs/src/modules/ROOT/pages/using-timefold-solver/modeling-planning-problems.adoc index f514e941959..ca0b2181774 100644 --- a/docs/src/modules/ROOT/pages/using-timefold-solver/modeling-planning-problems.adoc +++ b/docs/src/modules/ROOT/pages/using-timefold-solver/modeling-planning-problems.adoc @@ -2353,9 +2353,9 @@ It's an uninitialized solution. [#solutionClass] === Solution class -A solution class holds all problem facts, planning entities and a score. -It is annotated with a `@PlanningSolution` annotation. -For example, an `Timetable` instance holds a list of all timeslots, all rooms and all `Lesson` instances: +A solution class is annotated with `@PlanningSolution` and implements the `SolverModel` interface. +It holds all problem facts, planning entities and a score. +For example, a `Timetable` class holds a list of all timeslots, all rooms and all `Lesson` instances: [tabs] ==== @@ -2364,74 +2364,132 @@ Java:: [source,java,options="nowrap"] ---- @PlanningSolution -public class Timetable { - - private String name; +public class Timetable implements SolverModel { - // Problem facts + @ProblemFactCollectionProperty + @ValueRangeProvider private List timeslots; + + @ProblemFactCollectionProperty private List rooms; - // Planning entities + @PlanningEntityCollectionProperty private List lessons; + @PlanningScore private HardSoftScore score; - ... + @Override + public HardSoftScore getScore() { + return score; + } + + // other Getters/Setters/Constructors } ---- +Kotlin:: ++ +[source,kotlin,options="nowrap"] +---- +@PlanningSolution +class Timetable : SolverModel { -==== + @ProblemFactCollectionProperty + @ValueRangeProvider + val timeslots: List = emptyList() -The solver configuration needs to declare the planning solution class: + @ProblemFactCollectionProperty + val rooms: List = emptyList() -[source,xml,options="nowrap"] ----- - - ... - org.acme.schooltimetabling.Timetable - ... - ----- + @PlanningEntityCollectionProperty + val lessons: List = emptyList() -[NOTE] -==== -Solution class must not be of Java's `enum` or `record` types. -Those are immutable by design and therefore cannot change during planning, -whereas a planning solution will. -==== + private var _score: HardSoftScore? = null + @PlanningScore + override fun getScore(): HardSoftScore? = _score -[#noSolverModelInterface] -=== No `SolverModel` interface + // other Getters/Setters/Constructors +} +---- +==== -Your `@PlanningSolution` class is a plain Java class with Timefold annotations. -You do not implement any framework interface on the solution class itself. +[TIP] +==== +If you don't need to control the score type, extend `AbstractSimpleModel` instead. +It implements `SolverModel` and includes the `@PlanningScore` field, +so you only need to define your problem facts and planning entities: [tabs] -==== +===== Java:: + [source,java,options="nowrap"] ---- @PlanningSolution -public class Timetable { +public class Timetable extends AbstractSimpleModel { @ProblemFactCollectionProperty @ValueRangeProvider private List timeslots; + @ProblemFactCollectionProperty + private List rooms; + @PlanningEntityCollectionProperty private List lessons; - @PlanningScore - private HardSoftScore score; - // Getters, Setters, Constructors } ---- + +Kotlin:: ++ +[source,kotlin,options="nowrap"] +---- +@PlanningSolution +class Timetable : AbstractSimpleModel() { + + @ProblemFactCollectionProperty + @ValueRangeProvider + val timeslots: List = emptyList() + + @ProblemFactCollectionProperty + val rooms: List = emptyList() + + @PlanningEntityCollectionProperty + val lessons: List = emptyList() + + // other Getters/Setters/Constructors +} +---- +===== +==== + +[NOTE] +==== +When xref:using-timefold-solver/library-integration.adoc[using Timefold Solver as a library], the `SolverModel` interface is not available. +Your `@PlanningSolution` class is a plain Java class with no framework interface to implement. +==== + +The solver configuration needs to declare the planning solution class: + +[source,xml,options="nowrap"] +---- + + ... + org.acme.schooltimetabling.Timetable + ... + +---- + +[NOTE] +==== +Solution class must not be of Java's `enum` or `record` types. +Those are immutable by design and therefore cannot change during planning, +whereas a planning solution will. ==== [#planningEntitiesOfASolution] From 5ade070c53a3337791a1a49ec834f102ed12f4ef Mon Sep 17 00:00:00 2001 From: Tom Cools Date: Fri, 26 Jun 2026 16:59:45 +0200 Subject: [PATCH 07/17] docs: remove excessive detail. --- .../modeling-planning-problems.adoc | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/docs/src/modules/ROOT/pages/using-timefold-solver/modeling-planning-problems.adoc b/docs/src/modules/ROOT/pages/using-timefold-solver/modeling-planning-problems.adoc index ca0b2181774..2711703970d 100644 --- a/docs/src/modules/ROOT/pages/using-timefold-solver/modeling-planning-problems.adoc +++ b/docs/src/modules/ROOT/pages/using-timefold-solver/modeling-planning-problems.adoc @@ -2473,18 +2473,6 @@ When xref:using-timefold-solver/library-integration.adoc[using Timefold Solver a Your `@PlanningSolution` class is a plain Java class with no framework interface to implement. ==== -The solver configuration needs to declare the planning solution class: - -[source,xml,options="nowrap"] ----- - - ... - org.acme.schooltimetabling.Timetable - ... - ----- - [NOTE] ==== Solution class must not be of Java's `enum` or `record` types. From 5b4cce130929ce7387de14ccb08c41d6a19e47b2 Mon Sep 17 00:00:00 2001 From: Tom Cools Date: Fri, 26 Jun 2026 17:24:32 +0200 Subject: [PATCH 08/17] docs: move moving and refactoring --- docs/src/modules/ROOT/nav.adoc | 55 ++++++++----------- docs/src/modules/ROOT/pages/.index.adoc | 2 +- .../commercial-editions.adoc | 8 +-- .../performance-improvements.adoc | 2 +- .../constraint-configuration.adoc | 4 +- .../pages/constraints-and-score/overview.adoc | 6 +- .../constraints-and-score/performance.adoc | 16 +++--- .../score-calculation.adoc | 34 ++++++------ .../design-patterns/design-patterns.adoc | 16 +++--- .../pages/integration/_config-properties.adoc | 6 +- .../ROOT/pages/integration/integration.adoc | 4 +- .../construction-heuristics.adoc | 36 ++++++------ .../exhaustive-search.adoc | 14 ++--- .../optimization-algorithms/local-search.adoc | 14 ++--- .../move-selector-reference.adoc | 26 ++++----- .../neighborhoods.adoc | 20 +++---- .../optimization-algorithms/overview.adoc | 26 ++++----- .../hello-world/hello-world-quickstart.adoc | 2 +- .../vehicle-routing-solution.adoc | 2 +- .../_school-timetabling-model.adoc | 2 +- ...abling-solution-value-range-providers.adoc | 2 +- .../responding-to-change.adoc | 20 +++---- .../benchmarking-and-tweaking.adoc | 8 +-- .../configuration.adoc | 6 +- .../library-integration.adoc | 6 +- .../modeling-planning-problems.adoc | 14 ++--- .../multithreaded-solving.adoc} | 14 ++--- .../overview.adoc | 2 +- .../solver-diagnostics.adoc | 5 +- .../modules/ROOT/pages/service/overview.adoc | 4 +- ...d-variables-to-planning-list-variable.adoc | 8 +-- ...-listeners-to-custom-shadow-variables.adoc | 8 +-- .../upgrade-from-v1.adoc | 8 +-- .../.using-timefold-solver.adoc | 11 ---- 34 files changed, 194 insertions(+), 217 deletions(-) rename docs/src/modules/ROOT/pages/{using-timefold-solver => running-timefold-solver}/benchmarking-and-tweaking.adoc (98%) rename docs/src/modules/ROOT/pages/{using-timefold-solver => running-timefold-solver}/configuration.adoc (93%) rename docs/src/modules/ROOT/pages/{using-timefold-solver => running-timefold-solver}/library-integration.adoc (97%) rename docs/src/modules/ROOT/pages/{using-timefold-solver => running-timefold-solver}/modeling-planning-problems.adoc (98%) rename docs/src/modules/ROOT/pages/{using-timefold-solver/running-the-solver.adoc => running-timefold-solver/multithreaded-solving.adoc} (93%) rename docs/src/modules/ROOT/pages/{using-timefold-solver => running-timefold-solver}/overview.adoc (96%) rename docs/src/modules/ROOT/pages/{using-timefold-solver => running-timefold-solver}/solver-diagnostics.adoc (98%) delete mode 100644 docs/src/modules/ROOT/pages/using-timefold-solver/.using-timefold-solver.adoc diff --git a/docs/src/modules/ROOT/nav.adoc b/docs/src/modules/ROOT/nav.adoc index 59d844284cd..b6555042b36 100644 --- a/docs/src/modules/ROOT/nav.adoc +++ b/docs/src/modules/ROOT/nav.adoc @@ -2,49 +2,38 @@ * xref:planning-ai-concepts.adoc[leveloffset=+1] * Getting started ** xref:quickstart/overview.adoc[leveloffset=+1] -** xref:quickstart/service/getting-started.adoc[Run as a service (Preview)] +** xref:quickstart/service/getting-started.adoc[Service Quickstart (Preview)] ** Embed as a library *** xref:quickstart/hello-world/hello-world-quickstart.adoc[Hello World Guide] *** xref:quickstart/quarkus/quarkus-quickstart.adoc[Quarkus Guide] *** xref:quickstart/spring-boot/spring-boot-quickstart.adoc[Spring Boot Guide] * Example use cases ** xref:quickstart/quarkus-vehicle-routing/quarkus-vehicle-routing-quickstart.adoc[Vehicle Routing (Guide)] -** https://github.com/TimefoldAI/timefold-quickstarts#-employee-scheduling[Employee Scheduling^] -** https://github.com/TimefoldAI/timefold-quickstarts#-maintenance-scheduling[Maintenance Scheduling^] -** https://github.com/TimefoldAI/timefold-quickstarts#-food-packaging[Food Packaging^] -** https://github.com/TimefoldAI/timefold-quickstarts#-order-picking[Order Picking^] -** https://github.com/TimefoldAI/timefold-quickstarts#-school-timetabling[School Timetabling^] -** https://github.com/TimefoldAI/timefold-quickstarts#-facility-location-problem[Facility Location^] -** https://github.com/TimefoldAI/timefold-quickstarts#-conference-scheduling[Conference Scheduling^] -** https://github.com/TimefoldAI/timefold-quickstarts#-bed-allocation-scheduling[Bed Allocation^] -** https://github.com/TimefoldAI/timefold-quickstarts#-flight-crew-scheduling[Flight Crew Scheduling^] -** https://github.com/TimefoldAI/timefold-quickstarts#-meeting-scheduling[Meeting Scheduling^] -** https://github.com/TimefoldAI/timefold-quickstarts#-task-assigning[Task Assigning^] -** https://github.com/TimefoldAI/timefold-quickstarts#-project-job-scheduling[Project Job Scheduling^] -** https://github.com/TimefoldAI/timefold-quickstarts#-sports-league-scheduling[Sports League Scheduling^] -** https://github.com/TimefoldAI/timefold-quickstarts#-tournament-scheduling[Tournament Scheduling^] -* xref:using-timefold-solver/overview.adoc[Using Timefold Solver] -** xref:using-timefold-solver/modeling-planning-problems.adoc[leveloffset=+1] -** xref:service/overview.adoc[Run as a Service (Preview)] -*** xref:service/rest-api.adoc[leveloffset=+1] -*** xref:service/modeling-changes.adoc[leveloffset=+1] -*** xref:service/constraint-weights.adoc[leveloffset=+1] -*** xref:service/demo-data.adoc[leveloffset=+1] -*** xref:service/exposing-metrics.adoc[leveloffset=+1] -** xref:using-timefold-solver/library-integration.adoc[Use as a Library (Advanced)] -*** xref:using-timefold-solver/configuration.adoc[leveloffset=+1] -** xref:using-timefold-solver/solver-diagnostics.adoc[leveloffset=+1] -* Constraints and score -** xref:constraints-and-score/overview.adoc[leveloffset=+1] +** https://github.com/TimefoldAI/timefold-quickstarts[More examples on GitHub^] + +* xref:running-timefold-solver/modeling-planning-problems.adoc[leveloffset=+1] + +* xref:constraints-and-score/overview.adoc[Constraints and score] ** xref:constraints-and-score/score-calculation.adoc[leveloffset=+1] ** xref:constraints-and-score/understanding-the-score.adoc[Understanding the score] ** xref:constraints-and-score/constraint-configuration.adoc[leveloffset=+1] ** xref:constraints-and-score/load-balancing-and-fairness.adoc[leveloffset=+1] ** xref:constraints-and-score/performance.adoc[leveloffset=+1] +* xref:running-timefold-solver/overview.adoc[Running the Solver] +** xref:service/overview.adoc[Service Reference (Preview)] +*** xref:service/rest-api.adoc[leveloffset=+1] +*** xref:service/modeling-changes.adoc[leveloffset=+1] +*** xref:service/constraint-weights.adoc[leveloffset=+1] +*** xref:service/demo-data.adoc[leveloffset=+1] +*** xref:service/exposing-metrics.adoc[leveloffset=+1] +** xref:running-timefold-solver/library-integration.adoc[Use as a Library (Advanced)] +*** xref:running-timefold-solver/configuration.adoc[leveloffset=+1] + * Tuning the Solver -** xref:using-timefold-solver/running-the-solver.adoc[leveloffset=+1] -** xref:using-timefold-solver/benchmarking-and-tweaking.adoc[leveloffset=+1] +** xref:running-timefold-solver/benchmarking-and-tweaking.adoc[leveloffset=+1] +** xref:running-timefold-solver/solver-diagnostics.adoc[leveloffset=+1] +** xref:running-timefold-solver/multithreaded-solving.adoc#multithreadedIncrementalSolving[Multithreaded solving] ** xref:optimization-algorithms/overview.adoc[Optimization algorithms] *** xref:optimization-algorithms/construction-heuristics.adoc[leveloffset=+1] *** xref:optimization-algorithms/local-search.adoc[leveloffset=+1] @@ -71,8 +60,8 @@ ** xref:constraints-and-score/understanding-the-score.adoc[Score analysis] ** xref:responding-to-change/responding-to-change.adoc#assignmentRecommendationAPI[Recommendation API] ** xref:optimization-algorithms/move-selector-reference.adoc#nearbySelection[Nearby selection] -** xref:using-timefold-solver/running-the-solver.adoc#multithreadedIncrementalSolving[Multithreaded solving] -** xref:using-timefold-solver/running-the-solver.adoc#partitionedSearch[Partitioned search] +** xref:running-timefold-solver/multithreaded-solving.adoc#multithreadedIncrementalSolving[Multithreaded solving] +** xref:running-timefold-solver/multithreaded-solving.adoc#partitionedSearch[Partitioned search] ** xref:constraints-and-score/performance.adoc#constraintProfiling[Constraint profiling] ** xref:commercial-editions/multistage-moves.adoc[leveloffset=+1] -** xref:using-timefold-solver/library-integration.adoc#throttlingBestSolutionEvents[Throttling best solution events] +** xref:running-timefold-solver/library-integration.adoc#throttlingBestSolutionEvents[Throttling best solution events] diff --git a/docs/src/modules/ROOT/pages/.index.adoc b/docs/src/modules/ROOT/pages/.index.adoc index b7934587e4c..c3a45bc123e 100644 --- a/docs/src/modules/ROOT/pages/.index.adoc +++ b/docs/src/modules/ROOT/pages/.index.adoc @@ -24,7 +24,7 @@ endif::[] include::introduction.adoc[leveloffset=+1] include::quickstart/.quickstart.adoc[leveloffset=+1] include::planning-ai-concepts.adoc[leveloffset=+1] -include::using-timefold-solver/.using-timefold-solver.adoc[leveloffset=+1] +include::running-timefold-solver/.running-timefold-solver.adoc[leveloffset=+1] include::constraints-and-score/.constraints-and-score.adoc[leveloffset=+1] include::optimization-algorithms/.optimization-algorithms.adoc[leveloffset=+1] include::responding-to-change/responding-to-change.adoc[leveloffset=+1] diff --git a/docs/src/modules/ROOT/pages/commercial-editions/commercial-editions.adoc b/docs/src/modules/ROOT/pages/commercial-editions/commercial-editions.adoc index 2519f4ea973..27b5125e28a 100644 --- a/docs/src/modules/ROOT/pages/commercial-editions/commercial-editions.adoc +++ b/docs/src/modules/ROOT/pages/commercial-editions/commercial-editions.adoc @@ -5,7 +5,7 @@ :icons: font _Timefold Solver Plus_ and _Timefold Solver Enterprise_ are commercial products which offer additional features, -such as xref:constraints-and-score/understanding-the-score.adoc[score analysis], xref:optimization-algorithms/move-selector-reference.adoc#nearbySelection[nearby selection] and xref:using-timefold-solver/running-the-solver.adoc#multithreadedIncrementalSolving[multithreaded solving]. +such as xref:constraints-and-score/understanding-the-score.adoc[score analysis], xref:optimization-algorithms/move-selector-reference.adoc#nearbySelection[nearby selection] and xref:running-timefold-solver/multithreaded-solving.adoc#multithreadedIncrementalSolving[multithreaded solving]. These features are essential to scale out to large datasets and build trust with your users. Unlike _Timefold Solver Community Edition_, these commercial editions are not open-source. @@ -37,10 +37,10 @@ See our https://licenses.timefold.ai/[license portal] to get your license, then | xref:optimization-algorithms/move-selector-reference.adoc#nearbySelection[Nearby selection] | | ✓ -| xref:using-timefold-solver/running-the-solver.adoc#multithreadedIncrementalSolving[Multithreaded solving] +| xref:running-timefold-solver/multithreaded-solving.adoc#multithreadedIncrementalSolving[Multithreaded solving] | | ✓ -| xref:using-timefold-solver/running-the-solver.adoc#partitionedSearch[Partitioned search] +| xref:running-timefold-solver/multithreaded-solving.adoc#partitionedSearch[Partitioned search] | | ✓ | xref:constraints-and-score/performance.adoc#constraintProfiling[Constraint profiling] @@ -49,7 +49,7 @@ See our https://licenses.timefold.ai/[license portal] to get your license, then | xref:commercial-editions/multistage-moves.adoc[Multistage moves] | | ✓ -| xref:using-timefold-solver/library-integration.adoc#throttlingBestSolutionEvents[Throttling best solution events] +| xref:running-timefold-solver/library-integration.adoc#throttlingBestSolutionEvents[Throttling best solution events] | | ✓ |=== \ No newline at end of file diff --git a/docs/src/modules/ROOT/pages/commercial-editions/performance-improvements.adoc b/docs/src/modules/ROOT/pages/commercial-editions/performance-improvements.adoc index 75ecbfe7763..fbcf1b836eb 100644 --- a/docs/src/modules/ROOT/pages/commercial-editions/performance-improvements.adoc +++ b/docs/src/modules/ROOT/pages/commercial-editions/performance-improvements.adoc @@ -8,7 +8,7 @@ Timefold Solver Enterprise brings many performance improvements. These are short include::./_only-enterprise.adoc[] Updates to shadow variables happen incrementally with Timefold Solver Enterprise. -For models making intensive use of xref:using-timefold-solver/modeling-planning-problems.adoc#customShadowVariable[shadow variables], this should be visible as a serious performance improvement out-of-the-box. +For models making intensive use of xref:running-timefold-solver/modeling-planning-problems.adoc#customShadowVariable[shadow variables], this should be visible as a serious performance improvement out-of-the-box. This is enabled by default and doesn't require any special considerations. diff --git a/docs/src/modules/ROOT/pages/constraints-and-score/constraint-configuration.adoc b/docs/src/modules/ROOT/pages/constraints-and-score/constraint-configuration.adoc index f833a34b043..0c2827df974 100644 --- a/docs/src/modules/ROOT/pages/constraints-and-score/constraint-configuration.adoc +++ b/docs/src/modules/ROOT/pages/constraints-and-score/constraint-configuration.adoc @@ -109,7 +109,7 @@ public class VehicleRoutePlan { We've just introduced a new field of type `ConstraintWeightOverrides`, and we provided a getter and a setter for it. -The field will be automatically exposed as a xref:using-timefold-solver/modeling-planning-problems.adoc#problemFacts[problem fact], +The field will be automatically exposed as a xref:running-timefold-solver/modeling-planning-problems.adoc#problemFacts[problem fact], there is no need to add a `@ProblemFactProperty` annotation. But we need to fill it with the desired constraint weights: @@ -218,7 +218,7 @@ public class MyPlanningSolution { ---- ==== -This will expose the `ConstraintParameters` as a xref:using-timefold-solver/modeling-planning-problems.adoc#problemFacts[problem fact], +This will expose the `ConstraintParameters` as a xref:running-timefold-solver/modeling-planning-problems.adoc#problemFacts[problem fact], making it available to the constraints. Finally, use the xref:constraints-and-score/score-calculation.adoc#constraintStreamsJoin[join building block] to adjust the constraint implementation to use the parameters: diff --git a/docs/src/modules/ROOT/pages/constraints-and-score/overview.adoc b/docs/src/modules/ROOT/pages/constraints-and-score/overview.adoc index dc823e99603..03cacea86af 100644 --- a/docs/src/modules/ROOT/pages/constraints-and-score/overview.adoc +++ b/docs/src/modules/ROOT/pages/constraints-and-score/overview.adoc @@ -434,11 +434,11 @@ The score calculation must be deterministic and free of side-effects. It must not change the planning entities or the problem facts in any way. For example, it must not call a setter method on a planning entity in the score calculation. -Timefold Solver does not recalculate the score of a solution if it can predict it (unless an xref:using-timefold-solver/running-the-solver.adoc#environmentMode[environmentMode assertion] is enabled). +Timefold Solver does not recalculate the score of a solution if it can predict it (unless an xref:running-timefold-solver/multithreaded-solving.adoc#environmentMode[environmentMode assertion] is enabled). For example, after a winning step is done, there is no need to calculate the score because that move was done and undone earlier. As a result, there is no guarantee that changes applied during score calculation actually happen. -To update planning entities when the planning variable change, use xref:using-timefold-solver/modeling-planning-problems.adoc#shadowVariable[shadow variables] instead. +To update planning entities when the planning variable change, use xref:running-timefold-solver/modeling-planning-problems.adoc#shadowVariable[shadow variables] instead. ==== [#initializingScoreTrend] @@ -481,7 +481,7 @@ Alternatively, you can also specify the trend for each score level separately: [#invalidScoreDetection] === Invalid score detection -When you put the xref:using-timefold-solver/running-the-solver.adoc#environmentMode[`environmentMode`] in `FULL_ASSERT` (or ``STEP_ASSERT``), +When you put the xref:running-timefold-solver/multithreaded-solving.adoc#environmentMode[`environmentMode`] in `FULL_ASSERT` (or ``STEP_ASSERT``), it will detect score corruption in the xref:constraints-and-score/performance.adoc#incrementalScoreCalculationPerformance[incremental score calculation]. However, that will not verify that your score calculator actually implements your score constraints as your business desires. For example, one constraint might consistently match the wrong pattern. diff --git a/docs/src/modules/ROOT/pages/constraints-and-score/performance.adoc b/docs/src/modules/ROOT/pages/constraints-and-score/performance.adoc index 6511571e716..5d4b91c2962 100644 --- a/docs/src/modules/ROOT/pages/constraints-and-score/performance.adoc +++ b/docs/src/modules/ROOT/pages/constraints-and-score/performance.adoc @@ -76,7 +76,7 @@ The network latency will kill your move evaluation performance. Cache the results of those remote services if possible. If some parts of a constraint can be calculated once, when the `Solver` starts, and never change during solving, -then turn them into xref:using-timefold-solver/modeling-planning-problems.adoc#cachedProblemFact[cached problem facts]. +then turn them into xref:running-timefold-solver/modeling-planning-problems.adoc#cachedProblemFact[cached problem facts]. [#pointlessConstraints] @@ -107,7 +107,7 @@ and you can set the weights accordingly. Instead of implementing a hard constraint, it can sometimes be built in. For example, if `Lecture` A should never be assigned to `Room` X, but it uses `ValueRangeProvider` on Solution, so the `Solver` will often try to assign it to `Room` X too (only to find out that it breaks a hard constraint). -Use xref:using-timefold-solver/modeling-planning-problems.adoc#valueRangeProviderOnPlanningEntity[a ValueRangeProvider on the planning entity] +Use xref:running-timefold-solver/modeling-planning-problems.adoc#valueRangeProviderOnPlanningEntity[a ValueRangeProvider on the planning entity] or xref:optimization-algorithms/move-selector-reference.adoc#filteredSelection[filtered selection] to define that Course A should only be assigned a `Room` different than X. @@ -316,7 +316,7 @@ For that reason, it is generally recommended putting Joiners based on enum field == Indexing, Hashing, Looping The code on the hot path of your application needs to be as fast as possible. -* When you use xref:using-timefold-solver/modeling-planning-problems.adoc#shadowVariable[Shadow Variables], you will provide a `supplier` method that computes the value of the shadow variable. +* When you use xref:running-timefold-solver/modeling-planning-problems.adoc#shadowVariable[Shadow Variables], you will provide a `supplier` method that computes the value of the shadow variable. * This method will be called very often during score calculation, so it needs to be fast. * If you need to do a lookup in a collection to find an element, ideally you are indexing into an array. * The second best option is to use a `HashMap` or `HashSet`. @@ -326,7 +326,7 @@ The code on the hot path of your application needs to be as fast as possible. == Benchmark Whatever you do, benchmark on a large and diverse set of inputs. -JVM performance may differ by as much as 20% between runs. To decide whether your changes helped or made things worse, make sure to always average the move evaluation speed from multiple runs on the same machine with the same solver configuration. We provide a xref:using-timefold-solver/benchmarking-and-tweaking.adoc#benchmarker[Benchmarker] to help you with that. +JVM performance may differ by as much as 20% between runs. To decide whether your changes helped or made things worse, make sure to always average the move evaluation speed from multiple runs on the same machine with the same solver configuration. We provide a xref:running-timefold-solver/benchmarking-and-tweaking.adoc#benchmarker[Benchmarker] to help you with that. [#constraintProfiling] == Constraint Profiling @@ -372,10 +372,10 @@ quarkus.timefold.solver.constraint-stream-profiling-enabled=true ==== Constraint profiling will log its results to the `ai.timefold.solver.enterprise.core.api.ConstraintProfiler` class at the end of solving. -For the report to be generated, xref:using-timefold-solver/running-the-solver.adoc#logging[the particular logging levels must be configured and enabled]. +For the report to be generated, xref:running-timefold-solver/multithreaded-solving.adoc#logging[the particular logging levels must be configured and enabled]. IMPORTANT: Constraint profiling is only supported for xref:constraints-and-score/score-calculation.adoc#constraintStreams[constraint stream score calculation] -when xref:using-timefold-solver/running-the-solver.adoc#multithreadedSolving[multi-threaded solving] is disabled. +when xref:running-timefold-solver/multithreaded-solving.adoc#multithreadedSolving[multi-threaded solving] is disabled. === Understanding the report @@ -498,7 +498,7 @@ NOTE: Traditional profiling tools such as Java Mission Control will report the i [#fullAssert] == Validate the implementation using FULL_ASSERT -When you are done optimizing your score calculation, make sure to validate it using xref:using-timefold-solver/running-the-solver.adoc#environmentModeFullAssert[`FULL_ASSERT`] mode. Make sure not to run with this mode in production. +When you are done optimizing your score calculation, make sure to validate it using xref:running-timefold-solver/multithreaded-solving.adoc#environmentModeFullAssert[`FULL_ASSERT`] mode. Make sure not to run with this mode in production. [#otherScoreCalculationPerformanceTricks] == Other score calculation performance tricks @@ -571,6 +571,6 @@ For example, move multiple items from the same container to another container. Not all score constraints have the same performance cost. Sometimes one score constraint can ruin performance outright. -Use the xref:using-timefold-solver/benchmarking-and-tweaking.adoc#benchmarker[Benchmarker] +Use the xref:running-timefold-solver/benchmarking-and-tweaking.adoc#benchmarker[Benchmarker] to do a one minute run and check what happens to the move evaluation speed if you comment out all but one of the score constraints. diff --git a/docs/src/modules/ROOT/pages/constraints-and-score/score-calculation.adoc b/docs/src/modules/ROOT/pages/constraints-and-score/score-calculation.adoc index 9e4f53d65c7..bd8622fc05d 100644 --- a/docs/src/modules/ROOT/pages/constraints-and-score/score-calculation.adoc +++ b/docs/src/modules/ROOT/pages/constraints-and-score/score-calculation.adoc @@ -56,9 +56,9 @@ Java:: ==== -This constraint stream iterates over all instances of class `Shift` in the xref:using-timefold-solver/modeling-planning-problems.adoc#problemFacts[problem facts] and -xref:using-timefold-solver/modeling-planning-problems.adoc#planningEntity[planning entities] -in the xref:using-timefold-solver/modeling-planning-problems.adoc#planningProblemAndPlanningSolution[planning problem]. +This constraint stream iterates over all instances of class `Shift` in the xref:running-timefold-solver/modeling-planning-problems.adoc#problemFacts[problem facts] and +xref:running-timefold-solver/modeling-planning-problems.adoc#planningEntity[planning entities] +in the xref:running-timefold-solver/modeling-planning-problems.adoc#planningProblemAndPlanningSolution[planning problem]. It finds every `Shift` which is assigned to employee `Ann` and for every such instance (also called a match), it adds a soft penalty of `1` to the overall xref:constraints-and-score/overview.adoc#calculateTheScore[score]. The following figure illustrates this process on a problem with 4 different shifts: @@ -261,14 +261,14 @@ This constraint stream penalizes each known and initialized instance of `Shift`. === ForEach The `.forEach(T)` building block selects every `T` instance that -is in a xref:using-timefold-solver/modeling-planning-problems.adoc#problemFacts[problem fact collection] -or a xref:using-timefold-solver/modeling-planning-problems.adoc#planningEntitiesOfASolution[planning entity collection] -that is xref:using-timefold-solver/modeling-planning-problems.adoc#detectingInconsistencies[consistent] +is in a xref:running-timefold-solver/modeling-planning-problems.adoc#problemFacts[problem fact collection] +or a xref:running-timefold-solver/modeling-planning-problems.adoc#planningEntitiesOfASolution[planning entity collection] +that is xref:running-timefold-solver/modeling-planning-problems.adoc#detectingInconsistencies[consistent] and has no `null` genuine planning variables. -To include instances with a `null` xref:using-timefold-solver/modeling-planning-problems.adoc#planningVariable[genuine planning variable], +To include instances with a `null` xref:running-timefold-solver/modeling-planning-problems.adoc#planningVariable[genuine planning variable], or planning values -not assigned to any xref:using-timefold-solver/modeling-planning-problems.adoc#planningListVariable[planning list variable], +not assigned to any xref:running-timefold-solver/modeling-planning-problems.adoc#planningListVariable[planning list variable], replace the `forEach()` building block by `forEachIncludingUnassigned()`: [tabs] @@ -301,16 +301,16 @@ Java:: ---- ==== -In cases utilizing the xref:using-timefold-solver/modeling-planning-problems.adoc#planningListVariable[planning list variable], +In cases utilizing the xref:running-timefold-solver/modeling-planning-problems.adoc#planningListVariable[planning list variable], you may want -to include an xref:using-timefold-solver/modeling-planning-problems.adoc#listVariableShadowVariablesInverseRelation[inverse relation shadow variable] +to include an xref:running-timefold-solver/modeling-planning-problems.adoc#listVariableShadowVariablesInverseRelation[inverse relation shadow variable] to maximize performance of your constraints. [#constraintStreamsPenaltiesRewards] === Penalties and rewards -The purpose of constraint streams is to build up a xref:constraints-and-score/overview.adoc#whatIsAScore[score] for a xref:using-timefold-solver/modeling-planning-problems.adoc#planningProblemAndPlanningSolution[solution]. +The purpose of constraint streams is to build up a xref:constraints-and-score/overview.adoc#whatIsAScore[score] for a xref:running-timefold-solver/modeling-planning-problems.adoc#planningProblemAndPlanningSolution[solution]. To do this, every constraint stream must contain a call to either a `penalize()` or a `reward()` building block. The `penalize()` building block makes the score worse and the `reward()` building block improves the score. @@ -2004,7 +2004,7 @@ In this case, the `given(...)` call takes the `vehicleA`, `visit1` and `visit2` Alternatively, you can use a `givenSolution(...)` method here and provide a planning solution instead. It's important to understand -that calling `givenSolution(...)` does not update any xref:using-timefold-solver/modeling-planning-problems.adoc#shadowVariable[shadow variables]. +that calling `givenSolution(...)` does not update any xref:running-timefold-solver/modeling-planning-problems.adoc#shadowVariable[shadow variables]. This is the default behavior and can be modified by invoking `settingAllShadowVariables()`. [tabs] @@ -2110,7 +2110,7 @@ remains consistent as your code base evolves. It is therefore necessary for the `given(...)` method to list all planning entities and problem facts, or provide the entire planning solution instead. -To test all constraints while updating all xref:using-timefold-solver/modeling-planning-problems.adoc#shadowVariable[shadow variables], +To test all constraints while updating all xref:running-timefold-solver/modeling-planning-problems.adoc#shadowVariable[shadow variables], call `giveSolution(...)` and `settingAllShadowVariables()`. This approach ensures that constraints which depend on the related variables are evaluated accordingly. @@ -2207,8 +2207,8 @@ Configure it in the solver configuration: ---- To configure values of an `EasyScoreCalculator` dynamically in the solver configuration -(so the xref:using-timefold-solver/benchmarking-and-tweaking.adoc#benchmarker[Benchmarker] can tweak those parameters), -add the `easyScoreCalculatorCustomProperties` element and use xref:using-timefold-solver/configuration.adoc#customPropertiesConfiguration[custom properties]: +(so the xref:running-timefold-solver/benchmarking-and-tweaking.adoc#benchmarker[Benchmarker] can tweak those parameters), +add the `easyScoreCalculatorCustomProperties` element and use xref:running-timefold-solver/configuration.adoc#customPropertiesConfiguration[custom properties]: [source,xml,options="nowrap"] ---- @@ -2287,8 +2287,8 @@ the assertions triggered by the ``environmentMode``. ==== To configure values of an `IncrementalScoreCalculator` dynamically in the solver configuration -(so the xref:using-timefold-solver/benchmarking-and-tweaking.adoc#benchmarker[Benchmarker] can tweak those parameters), -add the `incrementalScoreCalculatorCustomProperties` element and use xref:using-timefold-solver/configuration.adoc#customPropertiesConfiguration[custom properties]: +(so the xref:running-timefold-solver/benchmarking-and-tweaking.adoc#benchmarker[Benchmarker] can tweak those parameters), +add the `incrementalScoreCalculatorCustomProperties` element and use xref:running-timefold-solver/configuration.adoc#customPropertiesConfiguration[custom properties]: [source,xml,options="nowrap"] ---- diff --git a/docs/src/modules/ROOT/pages/design-patterns/design-patterns.adoc b/docs/src/modules/ROOT/pages/design-patterns/design-patterns.adoc index c3ddf300d82..8a13d916b24 100644 --- a/docs/src/modules/ROOT/pages/design-patterns/design-patterns.adoc +++ b/docs/src/modules/ROOT/pages/design-patterns/design-patterns.adoc @@ -31,13 +31,13 @@ For example, in employee rostering, the `Shift` to `Employee` relationship chang However, other relationships, such as from `Employee` to `Skill`, are immutable during planning because Timefold Solver cannot assign an extra skill to an employee. -. *If there are multiple relationships (or fields), check for xref:using-timefold-solver/modeling-planning-problems.adoc#shadowVariable[shadow variables]*. +. *If there are multiple relationships (or fields), check for xref:running-timefold-solver/modeling-planning-problems.adoc#shadowVariable[shadow variables]*. A shadow variable changes during planning, but its value can be calculated based on one or more genuine planning variables, without dispute. Color shadow relationships (or fields) purple. + [NOTE] ==== Only one side of a bi-directional relationship can be a genuine planning variable. -The other side will become an xref:using-timefold-solver/modeling-planning-problems.adoc#bidirectionalVariable[inverse relation shadow variable] later on. +The other side will become an xref:running-timefold-solver/modeling-planning-problems.adoc#bidirectionalVariable[inverse relation shadow variable] later on. Keep bi-directional relationships orange. ==== @@ -57,7 +57,7 @@ image::design-patterns/employeeShiftRosteringModelingGuideA.png[align="center"] [NOTE] ==== Timefold Solver does not currently support a `@PlanningVariable` annotation on a collection. -xref:using-timefold-solver/modeling-planning-problems.adoc#planningListVariable[Planning list variable] is not a means of achieving a many-to-one relationship; +xref:running-timefold-solver/modeling-planning-problems.adoc#planningListVariable[Planning list variable] is not a means of achieving a many-to-one relationship; it serves to indicate that these values happen in a sequence one after another. ==== @@ -192,14 +192,14 @@ a vehicle drives from customer to customer (thus it handles one customer at a ti The focus in this pattern is on deciding the order of a set of elements instead of assigning them to a specific date and time. However, the time coordinate of each element can be deduced from its position in the sequence. If the elements’ position on time axis affects the score, -use a xref:using-timefold-solver/modeling-planning-problems.adoc#shadowVariable[shadow variable] to calculate the time. +use a xref:running-timefold-solver/modeling-planning-problems.adoc#shadowVariable[shadow variable] to calculate the time. -This pattern is implemented using the xref:using-timefold-solver/modeling-planning-problems.adoc#planningListVariable[planning list variable]. +This pattern is implemented using the xref:running-timefold-solver/modeling-planning-problems.adoc#planningListVariable[planning list variable]. The planning entity determines the starting time of the first element in its planning list variable. The second element's starting time is calculated based on the starting time and duration of the first element. For example, in task assignment, Beth (the entity) starts working at 8:00, thus her first task starts at 8:00. It lasts 52 minutes, therefore her second task starts at 8:52. -The starting time of an element is usually a xref:using-timefold-solver/modeling-planning-problems.adoc#shadowVariable[shadow variable]. +The starting time of an element is usually a xref:running-timefold-solver/modeling-planning-problems.adoc#shadowVariable[shadow variable]. [#chainedThroughTimePatternGaps] ==== Chained through time pattern: creating gaps @@ -229,7 +229,7 @@ if those two customers ordered from the same restaurant around the same time and image::design-patterns/chainedThroughTimeAutomaticCollapse.png[align="center"] -Implement the automatic collapse in the xref:using-timefold-solver/modeling-planning-problems.adoc#shadowVariable[shadow variables] +Implement the automatic collapse in the xref:running-timefold-solver/modeling-planning-problems.adoc#shadowVariable[shadow variables] that calculate the start and end times of each task. @@ -245,7 +245,7 @@ For example when assembling furniture, assembling a bed is a two-person job. image::design-patterns/chainedThroughTimeAutomaticDelayUntilLast.png[align="center"] -Implement the automatic delay in the xref:using-timefold-solver/modeling-planning-problems.adoc#shadowVariable[shadow variables] +Implement the automatic delay in the xref:running-timefold-solver/modeling-planning-problems.adoc#shadowVariable[shadow variables] that calculates the arrival, start and end times of each task. *Separate the arrival time from the start time.* Additionally, add loop detection to avoid an infinite loop: diff --git a/docs/src/modules/ROOT/pages/integration/_config-properties.adoc b/docs/src/modules/ROOT/pages/integration/_config-properties.adoc index 9624c49185a..f495985e2cf 100644 --- a/docs/src/modules/ROOT/pages/integration/_config-properties.adoc +++ b/docs/src/modules/ROOT/pages/integration/_config-properties.adoc @@ -13,7 +13,7 @@ Defaults to `AUTO`. endif::[] {property_prefix}timefold.solver.{solver_name_prefix}solver-config-xml:: -A classpath resource to read the xref:using-timefold-solver/configuration.adoc#solverConfiguration[solver configuration XML]. +A classpath resource to read the xref:running-timefold-solver/configuration.adoc#solverConfiguration[solver configuration XML]. Defaults to `solverConfig.xml`. If a resource is specified, it must be located in the classpath, or the configuration will fail. If the property is not specified, the file `solverConfig.xml` is used when found on the classpath. Otherwise, the @@ -25,7 +25,7 @@ located in the classpath. The random seed to be used in the solving process. {property_prefix}timefold.solver.{solver_name_prefix}environment-mode:: -Enable xref:using-timefold-solver/running-the-solver.adoc#environmentMode[runtime assertions] to detect common bugs in your +Enable xref:running-timefold-solver/multithreaded-solving.adoc#environmentMode[runtime assertions] to detect common bugs in your implementation during development. {property_prefix}timefold.solver.{solver_name_prefix}constraint-stream-profiling-enabled:: @@ -38,7 +38,7 @@ This is often useful for xref:responding-to-change/responding-to-change.adoc#rea Defaults to `false`. {property_prefix}timefold.solver.{solver_name_prefix}move-thread-count:: -Enable xref:using-timefold-solver/running-the-solver.adoc#multithreadedIncrementalSolving[multithreaded incremental solving] for a single problem, which increases CPU consumption. +Enable xref:running-timefold-solver/multithreaded-solving.adoc#multithreadedIncrementalSolving[multithreaded incremental solving] for a single problem, which increases CPU consumption. Defaults to `NONE`. {property_prefix}timefold.solver.{solver_name_prefix}nearby-distance-meter-class:: diff --git a/docs/src/modules/ROOT/pages/integration/integration.adoc b/docs/src/modules/ROOT/pages/integration/integration.adoc index 78c4192c8b2..e5fcda49d09 100644 --- a/docs/src/modules/ROOT/pages/integration/integration.adoc +++ b/docs/src/modules/ROOT/pages/integration/integration.adoc @@ -845,7 +845,7 @@ To avoid this limitation, https://docs.jboss.org/hibernate/orm/current/userguide In JPA and Hibernate, there is usually a `@ManyToOne` relationship from most problem fact classes to the planning solution class. Therefore, the problem fact classes reference the planning solution class, -which implies that when the solution is xref:using-timefold-solver/modeling-planning-problems.adoc#cloningASolution[planning cloned], they need to be cloned too. +which implies that when the solution is xref:running-timefold-solver/modeling-planning-problems.adoc#cloningASolution[planning cloned], they need to be cloned too. Use an `@DeepPlanningClone` on each such problem fact class to enforce that: [source,java,options="nowrap"] @@ -1128,7 +1128,7 @@ Understand these guidelines to decide the hardware for a Timefold Solver service * **RAM memory**: Provision plenty, but no need to provide more. ** The problem dataset, loaded before Timefold Solver is called, often consumes the most memory. It depends on the problem scale. *** If this is a problem, review the domain class structure: remove classes or fields that Timefold Solver doesn't need during solving. -*** Timefold Solver usually has up to three solution instances: the internal working solution, the best solution and the old best solution (when it's being replaced). However, these are all a xref:using-timefold-solver/modeling-planning-problems.adoc#cloningASolution[planning clone] of each other, so many problem fact instances are shared between those solution instances. +*** Timefold Solver usually has up to three solution instances: the internal working solution, the best solution and the old best solution (when it's being replaced). However, these are all a xref:running-timefold-solver/modeling-planning-problems.adoc#cloningASolution[planning clone] of each other, so many problem fact instances are shared between those solution instances. ** During solving, the memory is very volatile, because solving creates many short-lived objects. The Garbage Collector deletes these in bulk and therefore needs some heap space as a buffer. ** The maximum size of the JVM heap space can be in three states: *** **Insufficient**: An `OutOfMemoryException` is thrown (often because the Garbage Collector is using more than 98% of the CPU time). diff --git a/docs/src/modules/ROOT/pages/optimization-algorithms/construction-heuristics.adoc b/docs/src/modules/ROOT/pages/optimization-algorithms/construction-heuristics.adoc index 3db2d8f7f2f..5720a8dfd64 100644 --- a/docs/src/modules/ROOT/pages/optimization-algorithms/construction-heuristics.adoc +++ b/docs/src/modules/ROOT/pages/optimization-algorithms/construction-heuristics.adoc @@ -77,7 +77,7 @@ meaning that "more difficult" planning entities are added earlier in the list wh image::optimization-algorithms/construction-heuristics/firstFitDecreasingNQueens04.png[align="center"] Requires the model -to support xref:using-timefold-solver/modeling-planning-problems.adoc#planningEntitySorting[planning entity sorting]. +to support xref:running-timefold-solver/modeling-planning-problems.adoc#planningEntitySorting[planning entity sorting]. [NOTE] ==== @@ -127,7 +127,7 @@ It sorts the planning values in ascending order by any given metric, meaning that "weaker" planning values are added earlier in the list while "stronger" values are included later. Requires the model -to support xref:using-timefold-solver/modeling-planning-problems.adoc#planningValueSorting[planning value sorting]. +to support xref:running-timefold-solver/modeling-planning-problems.adoc#planningValueSorting[planning value sorting]. [NOTE] ==== @@ -175,8 +175,8 @@ Combines <> and <> and <> for a single entity class with one variable: @@ -481,7 +481,7 @@ This is the default if the xref:constraints-and-score/overview.adoc#initializing If there are only negative constraints, but the xref:constraints-and-score/overview.adoc#initializingScoreTrend[InitializingScoreTrend] is strictly not ``ONLY_DOWN``, it can sometimes make sense to apply FIRST_NON_DETERIORATING_SCORE. -Use the xref:using-timefold-solver/benchmarking-and-tweaking.adoc#benchmarker[Benchmarker] +Use the xref:running-timefold-solver/benchmarking-and-tweaking.adoc#benchmarker[Benchmarker] to decide if the score quality loss is worth the time gain. ==== * ``FIRST_FEASIBLE_SCORE``: Initialize the variable(s) with the first move that has a feasible score. @@ -724,7 +724,7 @@ For scaling out, see < ---- * `ValueSelector` supports: -** ``ASCENDING``: Sorts the planning values in ascending order based on a given metric (xref:using-timefold-solver/modeling-planning-problems.adoc#planningValueSorting[planning value sorting]). Requires that planning value is annotated on the domain model. +** ``ASCENDING``: Sorts the planning values in ascending order based on a given metric (xref:running-timefold-solver/modeling-planning-problems.adoc#planningValueSorting[planning value sorting]). Requires that planning value is annotated on the domain model. + [source,xml,options="nowrap"] ---- @@ -1025,7 +1025,7 @@ or how likely are the nearest elements to be selected based on their distance fr [NOTE] ==== Only tweak the default settings if you are prepared -to back your choices by extensive xref:using-timefold-solver/benchmarking-and-tweaking.adoc#benchmarker[benchmarking]. +to back your choices by extensive xref:running-timefold-solver/benchmarking-and-tweaking.adoc#benchmarker[benchmarking]. ==== The following ``NearbySelectionDistributionType``s are supported: @@ -1080,7 +1080,7 @@ only set the distribution type (so without a `distributionSizeMaximum` parameter == Move selectors for basic variables These moves are applicable to planning variables that aren’t part of a list, -also called xref:using-timefold-solver/modeling-planning-problems.adoc#planningVariable[basic variables]. +also called xref:running-timefold-solver/modeling-planning-problems.adoc#planningVariable[basic variables]. [#changeMoveSelector] === `ChangeMoveSelector` @@ -1346,7 +1346,7 @@ Also, the size of the sub-pillars is limited in length of up to 1000 entities. The `RuinRecreateMove` selects a subset of entities and sets their values to null, effectively unassigning them. Then it runs a construction heuristic to assign them again. -If xref:using-timefold-solver/modeling-planning-problems.adoc#planningVariableAllowingUnassigned[unassigned values] are allowed, +If xref:running-timefold-solver/modeling-planning-problems.adoc#planningVariableAllowingUnassigned[unassigned values] are allowed, it may leave them unassigned. This coarse-grained move is useful to help the solver to escape from a local optimum. @@ -1375,7 +1375,7 @@ To enable it, add the following to the `localSearch` section of the solver confi ==== The default values have been determined by extensive benchmarking. That said, the optimal values may vary depending on the problem, available solving time, and dataset at hand. -We recommend that you xref:using-timefold-solver/benchmarking-and-tweaking.adoc#benchmarker[experiment with these values] +We recommend that you xref:running-timefold-solver/benchmarking-and-tweaking.adoc#benchmarker[experiment with these values] to find the best fit for your problem. ==== @@ -1489,7 +1489,7 @@ public class MyStageProvider implements BasicVariableStageProvider ---- -Switching xref:using-timefold-solver/running-the-solver.adoc#environmentMode[EnvironmentMode] can heavily impact when this termination ends. +Switching xref:running-timefold-solver/multithreaded-solving.adoc#environmentMode[EnvironmentMode] can heavily impact when this termination ends. [#moveCountTermination] ===== `MoveCountTermination` @@ -731,7 +731,7 @@ This is useful for benchmarking. ---- -Switching xref:using-timefold-solver/running-the-solver.adoc#environmentMode[EnvironmentMode] can heavily impact when this termination ends. +Switching xref:running-timefold-solver/multithreaded-solving.adoc#environmentMode[EnvironmentMode] can heavily impact when this termination ends. [#diminishedReturnsTermination] ===== `DiminishedReturnsTermination` @@ -851,7 +851,7 @@ For example, to implement a custom Construction Heuristic without implementing a ==== Most of the time, a custom solver phase is not worth the investment of development time. xref:optimization-algorithms/construction-heuristics.adoc#constructionHeuristics[Construction Heuristics] are configurable and support partially initialized solutions too. -You can use the xref:using-timefold-solver/benchmarking-and-tweaking.adoc#benchmarker[Benchmarker] to tweak them. +You can use the xref:running-timefold-solver/benchmarking-and-tweaking.adoc#benchmarker[Benchmarker] to tweak them. ==== The `PhaseCommand` interface appears as follows: @@ -888,7 +888,7 @@ Returns `true` if the `PhaseCommand` should terminate. Returns the working object that corresponds to the given external object. This is useful when the `PhaseCommand` remembers an object from some previous working solution, and needs to find the corresponding object in the current working solution, -which may have been xref:using-timefold-solver/modeling-planning-problems.adoc#cloningASolution[planning-cloned] since. +which may have been xref:running-timefold-solver/modeling-planning-problems.adoc#cloningASolution[planning-cloned] since. Any change on the planning entities in a `PhaseCommand` must be done through the `execute` or `executeTemporarily` methods, to avoid corrupting the `Solver`. @@ -926,8 +926,8 @@ the best solution will not be changed. Effectively nothing will have changed for the next `Phase` or `PhaseCommand`. To configure values of a `PhaseCommand` dynamically in the solver configuration -(so the xref:using-timefold-solver/benchmarking-and-tweaking.adoc#benchmarker[Benchmarker] can tweak those parameters), -add the `customProperties` element and use xref:using-timefold-solver/configuration.adoc#customPropertiesConfiguration[custom properties]: +(so the xref:running-timefold-solver/benchmarking-and-tweaking.adoc#benchmarker[Benchmarker] can tweak those parameters), +add the `customProperties` element and use xref:running-timefold-solver/configuration.adoc#customPropertiesConfiguration[custom properties]: [source,xml,options="nowrap"] ---- @@ -988,8 +988,8 @@ Therefore, all the optimization algorithms are confronted with `Move` selection: ==== Which move types might be missing in my implementation? To determine which move types might be missing in your implementation, -run a xref:using-timefold-solver/benchmarking-and-tweaking.adoc#benchmarker[Benchmarker] __for a short amount of time__ -and xref:using-timefold-solver/benchmarking-and-tweaking.adoc#writeTheOutputSolutionOfBenchmarkRuns[configure it to write the best solutions to disk]. +run a xref:running-timefold-solver/benchmarking-and-tweaking.adoc#benchmarker[Benchmarker] __for a short amount of time__ +and xref:running-timefold-solver/benchmarking-and-tweaking.adoc#writeTheOutputSolutionOfBenchmarkRuns[configure it to write the best solutions to disk]. Take a look at such a best solution: it will likely be a local optima. Try to figure out if there's a move that could get out of that local optima faster. diff --git a/docs/src/modules/ROOT/pages/quickstart/hello-world/hello-world-quickstart.adoc b/docs/src/modules/ROOT/pages/quickstart/hello-world/hello-world-quickstart.adoc index bd5b312c095..74159394664 100644 --- a/docs/src/modules/ROOT/pages/quickstart/hello-world/hello-world-quickstart.adoc +++ b/docs/src/modules/ROOT/pages/quickstart/hello-world/hello-world-quickstart.adoc @@ -96,7 +96,7 @@ include::../shared/_java-prerequisites.adoc[] Create a Maven or Gradle build file and add these dependencies: * `timefold-solver-core` (compile scope) to solve the school timetable problem. -* A xref:using-timefold-solver/running-the-solver.adoc#logging[logging] implementation, such as `logback-classic` (runtime scope), to see what Timefold Solver is doing. +* A xref:running-timefold-solver/multithreaded-solving.adoc#logging[logging] implementation, such as `logback-classic` (runtime scope), to see what Timefold Solver is doing. [tabs] ==== diff --git a/docs/src/modules/ROOT/pages/quickstart/quarkus-vehicle-routing/vehicle-routing-solution.adoc b/docs/src/modules/ROOT/pages/quickstart/quarkus-vehicle-routing/vehicle-routing-solution.adoc index 4a269af4e21..31ef2152fe4 100644 --- a/docs/src/modules/ROOT/pages/quickstart/quarkus-vehicle-routing/vehicle-routing-solution.adoc +++ b/docs/src/modules/ROOT/pages/quickstart/quarkus-vehicle-routing/vehicle-routing-solution.adoc @@ -171,7 +171,7 @@ However, this class is also the output of the solution: The `visits` field is a value range provider. It holds the `Visit` instances which Timefold Solver can pick from to assign to the `visits` field of `Vehicle` instances. The `visits` field has an `@ValueRangeProvider` annotation to connect the `@PlanningListVariable` with the `@ValueRangeProvider`, -by matching the type of the planning list variable with the type returned by the xref:using-timefold-solver/modeling-planning-problems.adoc#planningValueRangeProvider[value range provider]. +by matching the type of the planning list variable with the type returned by the xref:running-timefold-solver/modeling-planning-problems.adoc#planningValueRangeProvider[value range provider]. == Distance calculation diff --git a/docs/src/modules/ROOT/pages/quickstart/shared/school-timetabling/_school-timetabling-model.adoc b/docs/src/modules/ROOT/pages/quickstart/shared/school-timetabling/_school-timetabling-model.adoc index 49e0ee94d76..2bb4ba9cc18 100644 --- a/docs/src/modules/ROOT/pages/quickstart/shared/school-timetabling/_school-timetabling-model.adoc +++ b/docs/src/modules/ROOT/pages/quickstart/shared/school-timetabling/_school-timetabling-model.adoc @@ -296,7 +296,7 @@ because it contains one or more planning variables. The `timeslot` field has an `@PlanningVariable` annotation, so Timefold Solver knows that it can change its value. In order to find potential `Timeslot` instances to assign to this field, -Timefold Solver uses the variable type to connect to a xref:using-timefold-solver/modeling-planning-problems.adoc#planningValueRangeProvider[value range provider] +Timefold Solver uses the variable type to connect to a xref:running-timefold-solver/modeling-planning-problems.adoc#planningValueRangeProvider[value range provider] that provides a `List` to pick from. The `room` field also has an `@PlanningVariable` annotation, for the same reasons. diff --git a/docs/src/modules/ROOT/pages/quickstart/shared/school-timetabling/_school-timetabling-solution-value-range-providers.adoc b/docs/src/modules/ROOT/pages/quickstart/shared/school-timetabling/_school-timetabling-solution-value-range-providers.adoc index 8f5e9ea5ba7..b902a0e32cc 100644 --- a/docs/src/modules/ROOT/pages/quickstart/shared/school-timetabling/_school-timetabling-solution-value-range-providers.adoc +++ b/docs/src/modules/ROOT/pages/quickstart/shared/school-timetabling/_school-timetabling-solution-value-range-providers.adoc @@ -3,6 +3,6 @@ The `timeslots` field is a value range provider. It holds the `Timeslot` instances which Timefold Solver can pick from to assign to the `timeslot` field of `Lesson` instances. The `timeslots` field has an `@ValueRangeProvider` annotation to connect the `@PlanningVariable` with the `@ValueRangeProvider`, -by matching the type of the planning variable with the type returned by the xref:using-timefold-solver/modeling-planning-problems.adoc#planningValueRangeProvider[value range provider]. +by matching the type of the planning variable with the type returned by the xref:running-timefold-solver/modeling-planning-problems.adoc#planningValueRangeProvider[value range provider]. Following the same logic, the `rooms` field also has an `@ValueRangeProvider` annotation. diff --git a/docs/src/modules/ROOT/pages/responding-to-change/responding-to-change.adoc b/docs/src/modules/ROOT/pages/responding-to-change/responding-to-change.adoc index bbfee7fda4e..5d53a917d24 100644 --- a/docs/src/modules/ROOT/pages/responding-to-change/responding-to-change.adoc +++ b/docs/src/modules/ROOT/pages/responding-to-change/responding-to-change.adoc @@ -79,7 +79,7 @@ This is called overconstrained planning. By default, Timefold Solver assigns all planning entities, overloads the planning values, and therefore breaks hard constraints. There are two ways to avoid this: -* Use xref:using-timefold-solver/modeling-planning-problems.adoc#planningVariableAllowingUnassigned[planning variables with unassigned values], +* Use xref:running-timefold-solver/modeling-planning-problems.adoc#planningVariableAllowingUnassigned[planning variables with unassigned values], so that some entities are unassigned. * Add virtual values to catch the unassigned entities. @@ -94,9 +94,9 @@ image::responding-to-change/overconstrainedPlanning.png[align="center"] To implement this: . Add a score level (usually a medium level between the hard and soft level) by switching xref:constraints-and-score/overview.adoc#scoreType[`Score` type]. -. Make the planning variable xref:using-timefold-solver/modeling-planning-problems.adoc#planningVariableAllowingUnassigned[allow unassigned values]. +. Make the planning variable xref:running-timefold-solver/modeling-planning-problems.adoc#planningVariableAllowingUnassigned[allow unassigned values]. . Add a score constraint on the new level (usually a medium constraint) to penalize the number of unassigned entities (or a weighted sum of them). -Use xref:constraints-and-score/score-calculation.adoc#constraintStreamsForEach[`forEachIncludingUnassigned`] and check if the xref:using-timefold-solver/modeling-planning-problems.adoc#planningVariable[planning variable] or xref:using-timefold-solver/modeling-planning-problems.adoc#bidirectionalVariable[inverse relation shadow variable] is `null`: +Use xref:constraints-and-score/score-calculation.adoc#constraintStreamsForEach[`forEachIncludingUnassigned`] and check if the xref:running-timefold-solver/modeling-planning-problems.adoc#planningVariable[planning variable] or xref:running-timefold-solver/modeling-planning-problems.adoc#bidirectionalVariable[inverse relation shadow variable] is `null`: [tabs] ==== @@ -256,7 +256,7 @@ use xref:responding-to-change/responding-to-change.adoc#problemChange[ProblemCha [#partiallyPinnedPlanningListVariable] ==== Pinning a planning list variable -There are cases where pinning only a part of xref:using-timefold-solver/modeling-planning-problems.adoc#planningListVariable[planning list variable] is necessary. +There are cases where pinning only a part of xref:running-timefold-solver/modeling-planning-problems.adoc#planningListVariable[planning list variable] is necessary. For example, if some customer visits have already happened but are still in the list, it makes sense to pin them down. @@ -487,7 +487,7 @@ The `ProblemChangeDirector` must be updated with any change on the problem facts ==== To write a `ProblemChange` correctly, -it is important to understand the behavior of xref:using-timefold-solver/modeling-planning-problems.adoc#cloningASolution[a planning clone]. +it is important to understand the behavior of xref:running-timefold-solver/modeling-planning-problems.adoc#cloningASolution[a planning clone]. A planning clone of a solution must fulfill these requirements: @@ -501,14 +501,14 @@ When implementing problem changes, consider the following: . Any change in a `ProblemChange` must be done on the `@PlanningSolution` instance provided to the `ProblemChange` implementation. -. The `workingSolution` is xref:using-timefold-solver/modeling-planning-problems.adoc#cloningASolution[a planning clone] of the ``BestSolutionChangedEvent``'s ``bestSolution``. +. The `workingSolution` is xref:running-timefold-solver/modeling-planning-problems.adoc#cloningASolution[a planning clone] of the ``BestSolutionChangedEvent``'s ``bestSolution``. * The `workingSolution` in the `Solver` is never the same solution instance as in the rest of your application: it is a planning clone. * A planning clone also clones the planning entities and planning entity collections. + Thus, any change on the planning entities must happen on the `workingSolution` instance passed to the `ProblemChange.doChange(Solution_ workingSolution, ProblemChangeDirector problemChangeDirector)` method. . Use the method `ProblemChangeDirector.lookUpWorkingObject()` to translate and retrieve the working solution's instance of an object. -This requires xref:using-timefold-solver/modeling-planning-problems.adoc#planningId[annotating a property of that class as the @PlanningId]. +This requires xref:running-timefold-solver/modeling-planning-problems.adoc#planningId[annotating a property of that class as the @PlanningId]. . A planning clone does not clone the problem facts, nor the problem fact collections. _Therefore the ``__workingSolution__`` and the ``__bestSolution__`` share the same problem fact instances and the same problem fact list instances._ @@ -540,7 +540,7 @@ This is a _warm start_ because its initial solution is the adjusted best solutio + This implies the construction heuristic runs again, but because little or no planning variables are uninitialized -(unless you have a xref:using-timefold-solver/modeling-planning-problems.adoc#planningVariableAllowingUnassigned[planning variable with unassigned values]), +(unless you have a xref:running-timefold-solver/modeling-planning-problems.adoc#planningVariableAllowingUnassigned[planning variable with unassigned values]), it finishes much quicker than in a cold start. . Each configured `Termination` resets (both in solver and phase configuration), but a previous call to `terminateEarly()` is not undone. @@ -687,7 +687,7 @@ If required, <> can be used to optimize Assignment Recommendation API requires the `SolutionManager` to be configured with a xref:optimization-algorithms/construction-heuristics.adoc[construction heuristic] as the first phase, as it uses that construction heuristic to find the best fit. -If there are multiple construction heuristics phases in the xref:using-timefold-solver/configuration.adoc#solverConfiguration[solver configuration], +If there are multiple construction heuristics phases in the xref:running-timefold-solver/configuration.adoc#solverConfiguration[solver configuration], or if the first phase is not a construction heuristic (perhaps a xref:optimization-algorithms/optimization-algorithms.adoc#customSolverPhase[custom initializer]), the API will fail fast. @@ -753,7 +753,7 @@ But problems with the same publication deadline, solved by different organizatio are also initially better off with multi-stage planning, because of Conway's law and the high risk associated with unifying such groups. -Similarly to xref:using-timefold-solver/running-the-solver.adoc#partitionedSearch[Partitioned Search], multi-stage planning leads to suboptimal results. +Similarly to xref:running-timefold-solver/multithreaded-solving.adoc#partitionedSearch[Partitioned Search], multi-stage planning leads to suboptimal results. Nevertheless, it might be beneficial in order to simplify the maintenance, ownership, and help to start a project. Do not confuse multi-stage planning with xref:optimization-algorithms/optimization-algorithms.adoc#solverPhase[multi-phase solving]. diff --git a/docs/src/modules/ROOT/pages/using-timefold-solver/benchmarking-and-tweaking.adoc b/docs/src/modules/ROOT/pages/running-timefold-solver/benchmarking-and-tweaking.adoc similarity index 98% rename from docs/src/modules/ROOT/pages/using-timefold-solver/benchmarking-and-tweaking.adoc rename to docs/src/modules/ROOT/pages/running-timefold-solver/benchmarking-and-tweaking.adoc index 51c0f25150f..d91872e7109 100644 --- a/docs/src/modules/ROOT/pages/using-timefold-solver/benchmarking-and-tweaking.adoc +++ b/docs/src/modules/ROOT/pages/running-timefold-solver/benchmarking-and-tweaking.adoc @@ -419,7 +419,7 @@ To write those solutions in the ``benchmarkDirectory``, enable ``writeOutputSolu [#benchmarkLogging] === Benchmark logging -Benchmark logging is configured like xref:using-timefold-solver/running-the-solver.adoc#logging[solver logging]. +Benchmark logging is configured like xref:running-timefold-solver/multithreaded-solving.adoc#logging[solver logging]. To separate the log messages of each single benchmark run into a separate file, use the http://logback.qos.ch/manual/mdc.html[MDC] with key `subSingleBenchmark.name` in a sifting appender. For example with Logback in ``logback.xml``: @@ -1069,7 +1069,7 @@ for example when running benchmarks on an application server or a cloud platform ---- -NOTE: This feature is independent of xref:using-timefold-solver/running-the-solver.adoc#multithreadedIncrementalSolving[multi-threaded incremental solving] +NOTE: This feature is independent of xref:running-timefold-solver/multithreaded-solving.adoc#multithreadedIncrementalSolving[multi-threaded incremental solving] and can be used in the open source version of Timefold Solver. @@ -1104,8 +1104,8 @@ The `subSingleCount` defaults to `1` (so no statistical benchmarking). [NOTE] ==== If `subSingleCount` is higher than ``1``, -the benchmarker will automatically use a _different_ xref:using-timefold-solver/running-the-solver.adoc#randomNumberGenerator[`Random` seed] for every sub single run, -without losing reproducibility (for each sub single index) in xref:using-timefold-solver/running-the-solver.adoc#environmentMode[EnvironmentMode] ``NO_ASSERT`` and lower. +the benchmarker will automatically use a _different_ xref:running-timefold-solver/multithreaded-solving.adoc#randomNumberGenerator[`Random` seed] for every sub single run, +without losing reproducibility (for each sub single index) in xref:running-timefold-solver/multithreaded-solving.adoc#environmentMode[EnvironmentMode] ``NO_ASSERT`` and lower. ==== diff --git a/docs/src/modules/ROOT/pages/using-timefold-solver/configuration.adoc b/docs/src/modules/ROOT/pages/running-timefold-solver/configuration.adoc similarity index 93% rename from docs/src/modules/ROOT/pages/using-timefold-solver/configuration.adoc rename to docs/src/modules/ROOT/pages/running-timefold-solver/configuration.adoc index a849a5e6efd..762c28db2a9 100644 --- a/docs/src/modules/ROOT/pages/using-timefold-solver/configuration.adoc +++ b/docs/src/modules/ROOT/pages/running-timefold-solver/configuration.adoc @@ -30,7 +30,7 @@ Alternatively, a `SolverFactory` can be created from a ``File`` with ``SolverFac However, for portability reasons, a classpath resource is recommended. Both a `Solver` and a `SolverFactory` have a generic type called ``Solution_``, which is the class -representing a xref:using-timefold-solver/modeling-planning-problems.adoc#planningProblemAndPlanningSolution[planning problem and solution]. +representing a xref:running-timefold-solver/modeling-planning-problems.adoc#planningProblemAndPlanningSolution[planning problem and solution]. A solver configuration XML file looks like this: @@ -69,7 +69,7 @@ Notice the three parts in it: These various parts of a configuration are explained further in this manual. -*Timefold Solver makes it relatively easy to switch optimization algorithm(s) just by changing the configuration.* There is even a xref:using-timefold-solver/benchmarking-and-tweaking.adoc#benchmarker[Benchmarker] which allows you to play out different configurations against each other and report the most appropriate configuration for your use case. +*Timefold Solver makes it relatively easy to switch optimization algorithm(s) just by changing the configuration.* There is even a xref:running-timefold-solver/benchmarking-and-tweaking.adoc#benchmarker[Benchmarker] which allows you to play out different configurations against each other and report the most appropriate configuration for your use case. [#solverConfigurationAsCode] == Solver configuration as code @@ -155,7 +155,7 @@ This manual focuses on the first manner, but every feature supports both, even i == Custom properties configuration Solver configuration elements, that instantiate classes and explicitly mention it, support custom properties. -Custom properties are useful to tweak dynamic values through the xref:using-timefold-solver/benchmarking-and-tweaking.adoc#benchmarker[Benchmarker]. +Custom properties are useful to tweak dynamic values through the xref:running-timefold-solver/benchmarking-and-tweaking.adoc#benchmarker[Benchmarker]. For example, presume your `EasyScoreCalculator` has heavy calculations (which are cached) and you want to increase the cache size in one benchmark: diff --git a/docs/src/modules/ROOT/pages/using-timefold-solver/library-integration.adoc b/docs/src/modules/ROOT/pages/running-timefold-solver/library-integration.adoc similarity index 97% rename from docs/src/modules/ROOT/pages/using-timefold-solver/library-integration.adoc rename to docs/src/modules/ROOT/pages/running-timefold-solver/library-integration.adoc index 6a746872574..b00ee21b4a4 100644 --- a/docs/src/modules/ROOT/pages/using-timefold-solver/library-integration.adoc +++ b/docs/src/modules/ROOT/pages/running-timefold-solver/library-integration.adoc @@ -33,7 +33,7 @@ xref:optimization-algorithms/overview.adoc#doesTimefoldFindTheOptimalSolution[th [NOTE] ==== The instance passed to `solve(solution)` is modified by the Solver — do not treat it as the best solution. -The returned instance is most likely xref:using-timefold-solver/modeling-planning-problems.adoc#cloningASolution[a planning clone] of the input. +The returned instance is most likely xref:running-timefold-solver/modeling-planning-problems.adoc#cloningASolution[a planning clone] of the input. ==== NOTE: The input may be partially or fully initialized, which is common in xref:responding-to-change/responding-to-change.adoc[repeated planning]. @@ -82,7 +82,7 @@ For example, if set to `4`, submitting five problems has four problems solving immediately, and the fifth one starts when another one ends. If those problems solve for 5 minutes each, the fifth problem takes 10 minutes to finish. By default, `parallelSolverCount` is set to `AUTO`, which resolves to half the CPU cores, -regardless of the xref:using-timefold-solver/running-the-solver.adoc#multithreadedIncrementalSolving[`moveThreadCount`] of the solvers. +regardless of the xref:running-timefold-solver/multithreaded-solving.adoc#multithreadedIncrementalSolving[`moveThreadCount`] of the solvers. To retrieve the best solution, after solving terminates normally, use `SolverJob.getFinalBestSolution()`: @@ -256,7 +256,7 @@ To handle errors that may arise during the solving process, set up the handling logic by defining `withExceptionHandler(...)`. Finally, to build an instance of the solver, -xref:using-timefold-solver/configuration.adoc[a configuration step] is necessary. +xref:running-timefold-solver/configuration.adoc[a configuration step] is necessary. These settings are static and applied to any related solving execution. If you want to override certain settings for a particular job, such as the termination configuration, you can use the `withConfigOverride(...)` method. diff --git a/docs/src/modules/ROOT/pages/using-timefold-solver/modeling-planning-problems.adoc b/docs/src/modules/ROOT/pages/running-timefold-solver/modeling-planning-problems.adoc similarity index 98% rename from docs/src/modules/ROOT/pages/using-timefold-solver/modeling-planning-problems.adoc rename to docs/src/modules/ROOT/pages/running-timefold-solver/modeling-planning-problems.adoc index 2711703970d..8040b44847d 100644 --- a/docs/src/modules/ROOT/pages/using-timefold-solver/modeling-planning-problems.adoc +++ b/docs/src/modules/ROOT/pages/running-timefold-solver/modeling-planning-problems.adoc @@ -224,7 +224,7 @@ Alternatively, you can sometimes also introduce < ---- -Also xref:using-timefold-solver/modeling-planning-problems.adoc#planningId[add a `@PlanningId` annotation] +Also xref:running-timefold-solver/modeling-planning-problems.adoc#planningId[add a `@PlanningId` annotation] on every planning entity class and planning value class. There are several ways to <>. @@ -237,7 +237,7 @@ plug in a <>. [IMPORTANT] ==== -A xref:using-timefold-solver/solver-diagnostics.adoc#logging[logging level] of `debug` or `trace` causes congestion in multi-threaded Partitioned Search +A xref:running-timefold-solver/solver-diagnostics.adoc#logging[logging level] of `debug` or `trace` causes congestion in multi-threaded Partitioned Search and slows down the xref:constraints-and-score/performance.adoc#moveEvaluationSpeed[move evaluation speed]. ==== @@ -297,8 +297,8 @@ This can be decided dynamically, for example, based on the size of the non-parti The `partCount` is unrelated to the `runnablePartThreadLimit`. To configure values of a `SolutionPartitioner` dynamically in the solver configuration -(so the xref:using-timefold-solver/benchmarking-and-tweaking.adoc#benchmarker[Benchmarker] can tweak those parameters), -add the `solutionPartitionerCustomProperties` element and use xref:using-timefold-solver/configuration.adoc#customPropertiesConfiguration[custom properties]: +(so the xref:running-timefold-solver/benchmarking-and-tweaking.adoc#benchmarker[Benchmarker] can tweak those parameters), +add the `solutionPartitionerCustomProperties` element and use xref:running-timefold-solver/configuration.adoc#customPropertiesConfiguration[custom properties]: [source,xml,options="nowrap"] ---- diff --git a/docs/src/modules/ROOT/pages/using-timefold-solver/overview.adoc b/docs/src/modules/ROOT/pages/running-timefold-solver/overview.adoc similarity index 96% rename from docs/src/modules/ROOT/pages/using-timefold-solver/overview.adoc rename to docs/src/modules/ROOT/pages/running-timefold-solver/overview.adoc index e8e16832c76..0743dab09ab 100644 --- a/docs/src/modules/ROOT/pages/using-timefold-solver/overview.adoc +++ b/docs/src/modules/ROOT/pages/running-timefold-solver/overview.adoc @@ -29,7 +29,7 @@ xref:service/overview.adoc[→ Run as a Service] Embed Timefold Solver directly in your application for full control over the solving lifecycle. Use this approach when you need deep integration with existing infrastructure or have a use case or architecture where the recommended approach above doesn't apply to you. -xref:using-timefold-solver/configuration.adoc[→ Use as a Library] +xref:running-timefold-solver/configuration.adoc[→ Use as a Library] [#commonFoundation] == Common foundation diff --git a/docs/src/modules/ROOT/pages/using-timefold-solver/solver-diagnostics.adoc b/docs/src/modules/ROOT/pages/running-timefold-solver/solver-diagnostics.adoc similarity index 98% rename from docs/src/modules/ROOT/pages/using-timefold-solver/solver-diagnostics.adoc rename to docs/src/modules/ROOT/pages/running-timefold-solver/solver-diagnostics.adoc index b07dcd4b128..2dc0d20565b 100644 --- a/docs/src/modules/ROOT/pages/using-timefold-solver/solver-diagnostics.adoc +++ b/docs/src/modules/ROOT/pages/running-timefold-solver/solver-diagnostics.adoc @@ -4,7 +4,6 @@ :sectnums: :icons: font - [#environmentMode] == Environment mode: are there bugs in my code? @@ -288,7 +287,7 @@ Even `debug` logging can slow down performance considerably for fast stepping al (such as Late Acceptance and Simulated Annealing), but not for slow stepping algorithms (such as Tabu Search). -Both trace logging and debug logging cause congestion in xref:using-timefold-solver/running-the-solver.adoc#multithreadedSolving[multi-threaded solving] with most appenders, +Both trace logging and debug logging cause congestion in xref:running-timefold-solver/multithreaded-solving.adoc#multithreadedSolving[multi-threaded solving] with most appenders, see below. In Eclipse, `debug` logging to the console tends to cause congestion with move evaluation speeds above 10 000 per second. @@ -352,7 +351,7 @@ If it isn't picked up, temporarily add the system property `-Dlogback.debug=true [NOTE] ==== -When running multiple solvers or a xref:using-timefold-solver/running-the-solver.adoc#multithreadedSolving[multi-threaded solver], +When running multiple solvers or a xref:running-timefold-solver/multithreaded-solving.adoc#multithreadedSolving[multi-threaded solver], most appenders (including the console) cause congestion with `debug` and `trace` logging. Switch to an async appender to avoid this problem or turn off `debug` logging. ==== diff --git a/docs/src/modules/ROOT/pages/service/overview.adoc b/docs/src/modules/ROOT/pages/service/overview.adoc index a1899689b37..8c855e11267 100644 --- a/docs/src/modules/ROOT/pages/service/overview.adoc +++ b/docs/src/modules/ROOT/pages/service/overview.adoc @@ -41,7 +41,7 @@ Your modeling and constraint knowledge is the same, only the plumbing changes. The following areas of the documentation apply equally to both service and library mode: -xref:../using-timefold-solver/modeling-planning-problems.adoc[*Modeling planning problems*]:: +xref:../running-timefold-solver/modeling-planning-problems.adoc[*Modeling planning problems*]:: How to annotate your domain with `@PlanningSolution`, `@PlanningEntity`, `@PlanningVariable`, and related annotations. This is identical whether you run as a service or embed as a library. @@ -49,7 +49,7 @@ xref:../constraints-and-score/overview.adoc#constraintsAndScoreOverview[*Constra How to define hard and soft constraints, choose a score type, and analyze solution quality. Constraint streams and score calculation work identically in both modes. -xref:../using-timefold-solver/running-the-solver.adoc[*Tuning and diagnostics*]:: +xref:../running-timefold-solver/multithreaded-solving.adoc[*Tuning and diagnostics*]:: Environment modes, logging, monitoring, multi-threaded solving, and random seed configuration. These concepts and most configuration options apply regardless of how you run the solver. diff --git a/docs/src/modules/ROOT/pages/upgrading-timefold-solver/migration-guides/chained-variables-to-planning-list-variable.adoc b/docs/src/modules/ROOT/pages/upgrading-timefold-solver/migration-guides/chained-variables-to-planning-list-variable.adoc index 68772d16510..556abad4756 100644 --- a/docs/src/modules/ROOT/pages/upgrading-timefold-solver/migration-guides/chained-variables-to-planning-list-variable.adoc +++ b/docs/src/modules/ROOT/pages/upgrading-timefold-solver/migration-guides/chained-variables-to-planning-list-variable.adoc @@ -4,7 +4,7 @@ :icons: font This section explains how to update your planning model to use -the xref:using-timefold-solver/modeling-planning-problems.adoc#planningListVariable[planning list variable] approach +the xref:running-timefold-solver/modeling-planning-problems.adoc#planningListVariable[planning list variable] approach instead of the chained planning variable, which has been deprecated in Timefold Solver 1.x and removed entirely in Timefold Solver 2.0. @@ -42,7 +42,7 @@ public class Vehicle { === 2. Convert the chained entity to use shadow variables On the child element class (e.g. Customer), we need to replace some of the existing annotations with -xref:using-timefold-solver/modeling-planning-problems.adoc#listVariableShadowVariables[planning list shadow variables] to +xref:running-timefold-solver/modeling-planning-problems.adoc#listVariableShadowVariables[planning list shadow variables] to mirror the relationships now implied by the list assignment: * Parent reference: Replace the existing `@AnchorShadowVariable` @@ -76,7 +76,7 @@ public class Customer { === 3. Update the planning solution -By moving to xref:using-timefold-solver/modeling-planning-problems.adoc#planningListVariable[planning list variables], +By moving to xref:running-timefold-solver/modeling-planning-problems.adoc#planningListVariable[planning list variables], we no longer need two `@ValueRangeProviders`. [source,java] @@ -193,4 +193,4 @@ The solver ensures that a customer is in at most one vehicle’s list at a time. [#migrateChainedNext] == Next -* xref:using-timefold-solver/modeling-planning-problems.adoc#planningListVariable[Planning list variable reference] \ No newline at end of file +* xref:running-timefold-solver/modeling-planning-problems.adoc#planningListVariable[Planning list variable reference] \ No newline at end of file diff --git a/docs/src/modules/ROOT/pages/upgrading-timefold-solver/migration-guides/variable-listeners-to-custom-shadow-variables.adoc b/docs/src/modules/ROOT/pages/upgrading-timefold-solver/migration-guides/variable-listeners-to-custom-shadow-variables.adoc index 83cbf676cc1..8bb5af51147 100644 --- a/docs/src/modules/ROOT/pages/upgrading-timefold-solver/migration-guides/variable-listeners-to-custom-shadow-variables.adoc +++ b/docs/src/modules/ROOT/pages/upgrading-timefold-solver/migration-guides/variable-listeners-to-custom-shadow-variables.adoc @@ -3,7 +3,7 @@ :doctype: book :icons: font -This section explains how to update your planning model to use the new declarative xref:using-timefold-solver/modeling-planning-problems.adoc#customShadowVariable[custom shadow variable] +This section explains how to update your planning model to use the new declarative xref:running-timefold-solver/modeling-planning-problems.adoc#customShadowVariable[custom shadow variable] approach instead of the custom `VariableListener` pattern, which has been deprecated in Timefold Solver 1.x and removed entirely in Timefold Solver 2.0. @@ -240,7 +240,7 @@ public class Visit { Timefold Solver triggers the supplier whenever either `vehicle` or `previous` changes. -Read the xref:using-timefold-solver/modeling-planning-problems.adoc#shadowSourcesPaths[full @ShadowSources reference here]. +Read the xref:running-timefold-solver/modeling-planning-problems.adoc#shadowSourcesPaths[full @ShadowSources reference here]. === Variable listeners that updated multiple fields @@ -391,5 +391,5 @@ In many cases, storing derived data directly on the planning entity leads to sim [#migrateVariableListenersNext] == Next -* xref:using-timefold-solver/modeling-planning-problems.adoc#customShadowVariable[Custom shadow variables] -* xref:using-timefold-solver/modeling-planning-problems.adoc#shadowSourcesPaths[@ShadowSources paths] \ No newline at end of file +* xref:running-timefold-solver/modeling-planning-problems.adoc#customShadowVariable[Custom shadow variables] +* xref:running-timefold-solver/modeling-planning-problems.adoc#shadowSourcesPaths[@ShadowSources paths] \ No newline at end of file diff --git a/docs/src/modules/ROOT/pages/upgrading-timefold-solver/upgrade-from-v1.adoc b/docs/src/modules/ROOT/pages/upgrading-timefold-solver/upgrade-from-v1.adoc index 517982f8591..a97c1344093 100644 --- a/docs/src/modules/ROOT/pages/upgrading-timefold-solver/upgrade-from-v1.adoc +++ b/docs/src/modules/ROOT/pages/upgrading-timefold-solver/upgrade-from-v1.adoc @@ -290,7 +290,7 @@ We invite you to read the xref:upgrading-timefold-solver/migration-guides/chaine [%collapsible%open] ==== The variable listener mechanism has been removed. -Variable listeners were deprecated since 1.26.0 in favor of xref:using-timefold-solver/modeling-planning-problems.adoc#customShadowVariable[custom shadow variables]. +Variable listeners were deprecated since 1.26.0 in favor of xref:running-timefold-solver/modeling-planning-problems.adoc#customShadowVariable[custom shadow variables]. We invite you to read the xref:upgrading-timefold-solver/migration-guides/variable-listeners-to-custom-shadow-variables.adoc[variable listeners to custom shadow variables migration guide]. ==== @@ -596,10 +596,10 @@ The `lookUpStrategyType` attribute has been removed from `@PlanningSolution`. Remove `lookUpStrategyType` from your `@PlanningSolution` annotations and ensure that your planning entities and problem facts have a `@PlanningId`-annotated field. -`LookupStrategyType` was used in xref:using-timefold-solver/running-the-solver.adoc#multithreadedIncrementalSolving[multi-threaded incremental solving] +`LookupStrategyType` was used in xref:running-timefold-solver/multithreaded-solving.adoc#multithreadedIncrementalSolving[multi-threaded incremental solving] to specify how the solver should match entities and facts between parent and child score directors. The default value was `PLANNING_ID_OR_NONE`, which meant -that the solver would look up entities by their xref:using-timefold-solver/modeling-planning-problems.adoc#planningId[planning ID]. +that the solver would look up entities by their xref:running-timefold-solver/modeling-planning-problems.adoc#planningId[planning ID]. This behavior is now the default and only behavior. Before in `Timetable.java`: @@ -830,7 +830,7 @@ void onFinalBestSolution(FinalBestSolutionEvent event) { } ---- -Users of xref:using-timefold-solver/library-integration.adoc#throttlingBestSolutionEvents[solution throttling] +Users of xref:running-timefold-solver/library-integration.adoc#throttlingBestSolutionEvents[solution throttling] should also update their code to use `ThrottlingBestSolutionEventConsumer` instead of the now removed `ThrottlingBestSolutionConsumer`. ==== diff --git a/docs/src/modules/ROOT/pages/using-timefold-solver/.using-timefold-solver.adoc b/docs/src/modules/ROOT/pages/using-timefold-solver/.using-timefold-solver.adoc deleted file mode 100644 index 56096fce517..00000000000 --- a/docs/src/modules/ROOT/pages/using-timefold-solver/.using-timefold-solver.adoc +++ /dev/null @@ -1,11 +0,0 @@ -[#usingTimefoldSolver] -= Using Timefold Solver -:doctype: book -:sectnums: -:icons: font - -include::overview.adoc[leveloffset=+1] -include::configuration.adoc[leveloffset=+1] -include::modeling-planning-problems.adoc[leveloffset=+1] -include::running-the-solver.adoc[leveloffset=+1] -include::benchmarking-and-tweaking.adoc[leveloffset=+1] From 0cbefcfa7ac81e2235823f982e21ab4afa580bc2 Mon Sep 17 00:00:00 2001 From: Tom Cools Date: Fri, 26 Jun 2026 18:22:25 +0200 Subject: [PATCH 09/17] docs: rework constraint configuration overrides --- docs/src/modules/ROOT/nav.adoc | 3 +- .../constraint-configuration.adoc | 247 ++++++++++++++++-- .../pages/service/constraint-weights.adoc | 199 +------------- 3 files changed, 234 insertions(+), 215 deletions(-) diff --git a/docs/src/modules/ROOT/nav.adoc b/docs/src/modules/ROOT/nav.adoc index b6555042b36..4c47e80bc45 100644 --- a/docs/src/modules/ROOT/nav.adoc +++ b/docs/src/modules/ROOT/nav.adoc @@ -24,11 +24,12 @@ ** xref:service/overview.adoc[Service Reference (Preview)] *** xref:service/rest-api.adoc[leveloffset=+1] *** xref:service/modeling-changes.adoc[leveloffset=+1] -*** xref:service/constraint-weights.adoc[leveloffset=+1] +*** xref:constraints-and-score/constraint-configuration.adoc#serviceWeightOverrides[Constraint weights] *** xref:service/demo-data.adoc[leveloffset=+1] *** xref:service/exposing-metrics.adoc[leveloffset=+1] ** xref:running-timefold-solver/library-integration.adoc[Use as a Library (Advanced)] *** xref:running-timefold-solver/configuration.adoc[leveloffset=+1] +*** xref:constraints-and-score/constraint-configuration.adoc#libraryWeightOverrides[Constraint weights] * Tuning the Solver ** xref:running-timefold-solver/benchmarking-and-tweaking.adoc[leveloffset=+1] diff --git a/docs/src/modules/ROOT/pages/constraints-and-score/constraint-configuration.adoc b/docs/src/modules/ROOT/pages/constraints-and-score/constraint-configuration.adoc index 0c2827df974..b0add211659 100644 --- a/docs/src/modules/ROOT/pages/constraints-and-score/constraint-configuration.adoc +++ b/docs/src/modules/ROOT/pages/constraints-and-score/constraint-configuration.adoc @@ -4,9 +4,6 @@ :sectnums: :icons: font -:relevance: library-and-service -:notes: service module has some different patterns to deal with this. - Deciding the correct xref:constraints-and-score/overview.adoc#scoreConstraintWeight[weight] and @@ -21,9 +18,10 @@ so the business managers can tweak the constraint weights themselves: image::constraints-and-score/constraint-configuration/parameterizeTheScoreWeights.png[align="center"] +[#definingConstraintWeights] [#createAConstraintConfiguration] [#definingAndOverridingConstraintWeights] -== Defining and overriding constraint weights +== Defining constraint weights Let's define three constraints: @@ -42,6 +40,10 @@ Java:: ---- public class VehicleRoutingConstraintProvider implements ConstraintProvider { + public static final String VEHICLE_CAPACITY = "Vehicle capacity"; + public static final String SERVICE_FINISHED_AFTER_MAX_END_TIME = "Service finished after max end time"; + public static final String MINIMIZE_TRAVEL_TIME = "Minimize travel time"; + ... @Override @@ -57,29 +59,36 @@ public class VehicleRoutingConstraintProvider implements ConstraintProvider { return factory.forEach(Vehicle.class) ... .penalize(HardSoftScore.ONE_HARD, ...) - .asConstraint("Vehicle capacity"); + .asConstraint(VEHICLE_CAPACITY); } Constraint serviceFinishedAfterMaxEndTime(ConstraintFactory factory) { return factory.forEach(Visit.class) ... .penalize(HardSoftScore.ONE_HARD, ...) - .asConstraint("Service finished after max end time"); + .asConstraint(SERVICE_FINISHED_AFTER_MAX_END_TIME); } Constraint minimizeTravelTime(ConstraintFactory factory) { return factory.forEach(Vehicle.class) ... .penalize(HardSoftScore.ONE_SOFT, ...) - .asConstraint("Minimize travel time"); + .asConstraint(MINIMIZE_TRAVEL_TIME); } } ---- ==== -Without anything else, the constraint weights are fixed to the values we've given them in our `ConstraintProvider`. -To be able to override these weights at runtime, we need to introduce the `ConstraintWeightOverrides` class -to our planning solution class: +NOTE: Using static string constants for constraint names is recommended. +It prevents typos and ensures the constraint name is consistent across the `ConstraintProvider` +and any code that references constraints by name, such as when applying overrides. + +[#enablingWeightOverrides] +== Enabling weight overrides + +Without anything else, the constraint weights are fixed to the values specified in the `ConstraintProvider`. +To be able to override these weights at runtime, add a `ConstraintWeightOverrides` field +to the planning solution class: [tabs] ==== @@ -107,11 +116,18 @@ public class VehicleRoutePlan { ---- ==== -We've just introduced a new field of type `ConstraintWeightOverrides`, -and we provided a getter and a setter for it. The field will be automatically exposed as a xref:running-timefold-solver/modeling-planning-problems.adoc#problemFacts[problem fact], there is no need to add a `@ProblemFactProperty` annotation. -But we need to fill it with the desired constraint weights: + +[#applyingOverrides] +== Applying overrides + +How overrides are applied depends on whether Timefold Solver is used as a library or as a service. + +[#libraryWeightOverrides] +=== Using the library + +Populate `ConstraintWeightOverrides` with the desired weights and set it on the solution before solving: [tabs] ==== @@ -123,8 +139,8 @@ Java:: var constraintWeightOverrides = ConstraintWeightOverrides.of( Map.of( - "Vehicle capacity", HardSoftScore.ofHard(2), - "Service finished after max end time", HardSoftScore.ZERO + VehicleRoutingConstraintProvider.VEHICLE_CAPACITY, HardSoftScore.ofHard(2), + VehicleRoutingConstraintProvider.SERVICE_FINISHED_AFTER_MAX_END_TIME, HardSoftScore.ZERO ) ); @@ -142,6 +158,7 @@ and therefore will be disabled entirely. NOTE: The string keys passed to `ConstraintWeightOverrides.of(...)` must match the constraint ID, which is the value given to `asConstraint(...)` when building your constraints. +Using the static string constants defined in the `ConstraintProvider` eliminates this risk. In this way, you can solve the same problem by applying different constraint weights to each instance. @@ -149,7 +166,7 @@ Once solved, you can compare the results and decide which set of weights is the most suitable for your use case. [#constraintWeightOverridesSerialization] -=== Sending overrides over the wire +==== Sending overrides over the wire Overrides are part of the planning solution, and as such they are automatically serialized into JSON using Jackson, @@ -164,6 +181,200 @@ This is because we have no way of knowing which `Score` implementation you may b However, deserialization is easy to implement yourself by extending `AbstractConstraintWeightOverridesDeserializer` and registering it with Jackson's `ObjectMapper`. +[#serviceWeightOverrides] +=== Using the service + +include::../service/_preview-note.adoc[] + +Constraint weights are always an interpretation by the modeler. +It might be that the consumer of the model would like to see the constraints weighed differently. +`ModelConfigOverrides` allows consumers of a model to tailor constraint weights to their use case. + +[NOTE] +Be careful not to make your model overly configurable as that impacts usability. +Usually, it doesn't make sense to allow weight overrides for _hard_ constraints. + +Implement the `ModelConfigOverrides` interface. This is a marker interface, meaning it has no methods but can be discovered by the SDK. +The implementation should have fields that refer to specific constraints using the `@ConstraintReference` annotation, +referencing the static string constants defined in the `ConstraintProvider`: + +.The ConstraintProvider class. +[tabs] +==== +Java:: ++ +-- +[source,java,options="nowrap"] +---- +public class TimetableConstraintProvider implements ConstraintProvider { + + public static final String TEACHER_CONFLICT = "Teacher conflict"; + public static final String ROOM_CONFLICT = "Room conflict"; + + Constraint roomConflict(ConstraintFactory constraintFactory) { + return constraintFactory + // constraint implementation excluded + .asConstraint(ROOM_CONFLICT); + } + + Constraint teacherConflict(ConstraintFactory constraintFactory) { + return constraintFactory + // constraint implementation excluded + .asConstraint(TEACHER_CONFLICT); + } + + // other constraints excluded +} +---- +-- + +Kotlin:: ++ +-- +[source,kotlin,options="nowrap"] +---- +class TimetableConstraintProvider : ConstraintProvider { + + companion object { + const val TEACHER_CONFLICT = "Teacher conflict" + const val ROOM_CONFLICT = "Room conflict" + } + + fun roomConflict(constraintFactory: ConstraintFactory): Constraint { + return constraintFactory + // constraint implementation excluded + .asConstraint(ROOM_CONFLICT) + } + + fun teacherConflict(constraintFactory: ConstraintFactory): Constraint { + return constraintFactory + // constraint implementation excluded + .asConstraint(TEACHER_CONFLICT) + } + + // other constraints excluded +} +---- +-- +==== + +.The ModelConfigOverrides class. +[tabs] +==== +Java:: ++ +-- +[source,java,options="nowrap"] +---- +public final class TimetableConfigOverrides implements ModelConfigOverrides { + + public static final long DEFAULT_WEIGHT_ZERO = 0L; + public static final long DEFAULT_WEIGHT_ONE = 1L; + + @ConstraintReference(TimetableConstraintProvider.TEACHER_CONFLICT) + private long teacherConflictWeight = DEFAULT_WEIGHT_ONE; + + @ConstraintReference(TimetableConstraintProvider.ROOM_CONFLICT) + private long roomConflictWeight = DEFAULT_WEIGHT_ONE; + + // getter/setter excluded + +} +---- +-- + +Kotlin:: ++ +-- +[source,kotlin,options="nowrap"] +---- +data class TimetableConfigOverrides( + @ConstraintReference(TimetableConstraintProvider.TEACHER_CONFLICT) + val teacherConflictWeight: Long = DEFAULT_WEIGHT_ONE, + @ConstraintReference(TimetableConstraintProvider.ROOM_CONFLICT) + val roomConflictWeight: Long = DEFAULT_WEIGHT_ONE +) : ModelConfigOverrides { + + companion object { + const val DEFAULT_WEIGHT_ZERO = 0L + const val DEFAULT_WEIGHT_ONE = 1L + } + +} +---- +-- +==== + +The default constraint weight for these constraints is `1`. +This can now be overridden by the consumer by passing the overrides object in a request. +For example, to make the Teacher conflict 10 times more impactful, override the weight to 10: + +.Example request to change the weight. +[source,json] +---- +{ + "config": { + "run": { + "name": "run name", + + }, + "model": { + "overrides": { + "teacherConflictWeight": 10 + } + } + }, + "modelInput" : "" +} +---- + +Next, in the xref:service/rest-api.adoc#modelConverter[model converter], map the overrides +to a `ConstraintWeightOverrides` object and set it on the `@PlanningSolution` class +xref:#enablingWeightOverrides[as described above]: + +.As part of the ModelConverter. +[tabs] +==== +Java:: ++ +-- +[source,java,options="nowrap"] +---- +TimetableConfigOverrides modelConfigOverrides = modelConfig.overrides(); + +ConstraintWeightOverrides constraintWeightOverrides = ConstraintWeightOverrides.of( + Map.ofEntries( + Map.entry(TimetableConstraintProvider.TEACHER_CONFLICT, + HardMediumSoftLongScore.ofHard(modelConfigOverrides.getTeacherConflictWeight())), + Map.entry(TimetableConstraintProvider.ROOM_CONFLICT, + HardMediumSoftLongScore.ofSoft(modelConfigOverrides.getRoomConflictWeight())) + ) +); + +solverModel.setConstraintWeightOverrides(constraintWeightOverrides); +---- +-- + +Kotlin:: ++ +-- +[source,kotlin,options="nowrap"] +---- +val modelConfigOverrides = modelConfig.overrides() + +val constraintWeightOverrides = ConstraintWeightOverrides.of( + mapOf( + TimetableConstraintProvider.TEACHER_CONFLICT to + HardMediumSoftLongScore.ofHard(modelConfigOverrides.teacherConflictWeight), + TimetableConstraintProvider.ROOM_CONFLICT to + HardMediumSoftLongScore.ofSoft(modelConfigOverrides.roomConflictWeight) + ) +) + +solverModel.constraintWeightOverrides = constraintWeightOverrides +---- +-- +==== [#passingParametersToConstraints] == Passing parameters to constraints @@ -174,7 +385,7 @@ For example, a constraint may have to switch the minimum required pause length b based on the laws of the country that the data set is dealing with. To achieve this, you could have many variants of the same constraint in `ConstraintProvider` -and disable some of them using <>. +and disable some of them using <>. To avoid the code duplication that this would have caused, it is arguably better to have a single constraint that can be parameterized. This section shows how to achieve this @@ -247,4 +458,4 @@ public class MyConstraintProvider implements ConstraintProvider { } ---- -==== \ No newline at end of file +==== diff --git a/docs/src/modules/ROOT/pages/service/constraint-weights.adoc b/docs/src/modules/ROOT/pages/service/constraint-weights.adoc index 70429c00366..924703ed3d4 100644 --- a/docs/src/modules/ROOT/pages/service/constraint-weights.adoc +++ b/docs/src/modules/ROOT/pages/service/constraint-weights.adoc @@ -4,202 +4,9 @@ :sectnums: :icons: font -When implementing a model, it's important to set up the weights of the constraints correctly. -Having good default weights makes the model easily reusable and is an expression of your modeling expertise. - include::_preview-note.adoc[] -Constraint weights are always an interpretation by the modeler. It might be that the consumer of the model would like to see the constraints weighed differently. -`ModelConfigOverrides` allows consumers of a model to tailor constraint weights and parameters to their use case. - -[NOTE] -Be careful not to make your model overly configurable as that impacts usability. - -== Adjusting constraint weights - -Implement the `ModelConfigOverrides` interface. This is a marker interface, meaning it has no methods but can be discovered by the SDK. -The implementation should have fields that refer to specific constraints using the `@ConstraintReference` annotation. -To ensure both the constraint and this reference are the same, use a static field to keep the name of the constraint. - -.The example ConstraintProvider class. -[tabs] -==== -Java:: -+ --- -[source,java,options="nowrap"] ----- -public class TimetableConstraintProvider implements ConstraintProvider { - - public static final String TEACHER_CONFLICT = "Teacher conflict"; - public static final String ROOM_CONFLICT = "Room conflict"; - - Constraint roomConflict(ConstraintFactory constraintFactory) { - return constraintFactory - // constraint implementation excluded - .asConstraint(ROOM_CONFLICT); - } - - Constraint teacherConflict(ConstraintFactory constraintFactory) { - return constraintFactory - // constraint implementation excluded - .asConstraint(TEACHER_CONFLICT); - } - - // other constraints excluded -} ----- --- - -Kotlin:: -+ --- -[source,kotlin,options="nowrap"] ----- -class TimetableConstraintProvider : ConstraintProvider { - - companion object { - const val TEACHER_CONFLICT = "Teacher conflict" - const val ROOM_CONFLICT = "Room conflict" - } - - fun roomConflict(constraintFactory: ConstraintFactory): Constraint { - return constraintFactory - // constraint implementation excluded - .asConstraint(ROOM_CONFLICT) - } - - fun teacherConflict(constraintFactory: ConstraintFactory): Constraint { - return constraintFactory - // constraint implementation excluded - .asConstraint(TEACHER_CONFLICT) - } - - // other constraints excluded -} ----- --- -==== - -.The ModelConfigOverrides class. -[tabs] -==== -Java:: -+ --- -[source,java,options="nowrap"] ----- -public final class TimetableConfigOverrides implements ModelConfigOverrides { - - public static final long DEFAULT_WEIGHT_ZERO = 0L; - public static final long DEFAULT_WEIGHT_ONE = 1L; - - @ConstraintReference(TimetableConstraintProvider.TEACHER_CONFLICT) - private long teacherConflictWeight = DEFAULT_WEIGHT_ONE; - - @ConstraintReference(TimetableConstraintProvider.ROOM_CONFLICT) - private long roomConflictWeight = DEFAULT_WEIGHT_ONE; - - // getter/setter excluded - -} ----- --- - -Kotlin:: -+ --- -[source,kotlin,options="nowrap"] ----- -data class TimetableConfigOverrides( - @ConstraintReference(TimetableConstraintProvider.TEACHER_CONFLICT) - val teacherConflictWeight: Long = DEFAULT_WEIGHT_ONE, - @ConstraintReference(TimetableConstraintProvider.ROOM_CONFLICT) - val roomConflictWeight: Long = DEFAULT_WEIGHT_ONE -) : ModelConfigOverrides { - - companion object { - const val DEFAULT_WEIGHT_ZERO = 0L - const val DEFAULT_WEIGHT_ONE = 1L - } - -} ----- --- -==== - -The default constraint weight for these constraints is `1`. This can now be overridden by the consumer by passing in the model overrides object in a request. -For example, to make the Teacher conflict 10 times more impactful, override the weight to 10: - -.Example Request to change the weight. -[source,json] ----- -{ - "config": { - "run": { - "name": "run name", - - }, - "model": { - "overrides": { - "teacherConflictWeight": 10 - } - } - }, - "modelInput" : "" -} ----- - -[NOTE] -Only allow weight overrides if it makes sense. -Usually, it doesn't make sense to allow weight overrides for _hard_ constraints. - -Next, in the xref:./rest-api.adoc#modelConverter[model converter], make sure to map these overrides to a solver specific `ConstraintWeightOverrides` object that must be on the `@PlanningSolution` class. - -.As part of the ModelConvertor -[tabs] -==== -Java:: -+ --- -[source,java,options="nowrap"] ----- -TimetableConfigOverrides modelConfigOverrides = modelConfig.overrides(); - -ConstraintWeightOverrides constraintWeightOverrides = ConstraintWeightOverrides.of( - Map.ofEntries( - Map.entry(TimetableConstraintProvider.TEACHER_CONFLICT, - HardMediumSoftLongScore.ofHard(modelConfigOverrides.getTeacherConflictWeight())), - Map.entry(TimetableConstraintProvider.ROOM_CONFLICT, - HardMediumSoftLongScore.ofSoft(modelConfigOverrides.getRoomConflictWeight())) - ) -); - -solverModel.setConstraintWeightOverrides(constraintWeightOverrides); ----- --- - -Kotlin:: -+ --- -[source,kotlin,options="nowrap"] ----- -val modelConfigOverrides = modelConfig.overrides() - -val constraintWeightOverrides = ConstraintWeightOverrides.of( - mapOf( - TimetableConstraintProvider.TEACHER_CONFLICT to - HardMediumSoftLongScore.ofHard(modelConfigOverrides.teacherConflictWeight), - TimetableConstraintProvider.ROOM_CONFLICT to - HardMediumSoftLongScore.ofSoft(modelConfigOverrides.roomConflictWeight) - ) -) - -solverModel.constraintWeightOverrides = constraintWeightOverrides ----- --- -==== - -For more information, see xref:../constraints-and-score/constraint-configuration.adoc#constraintConfiguration[Adjusting constraints at runtime]. +For information on adjusting constraint weights at runtime when using the service, +see xref:../constraints-and-score/constraint-configuration.adoc#serviceWeightOverrides[Applying overrides — Using the service]. -//TODO === Adjusting Model Parameters https://github.com/TimefoldAI/timefold-solver/issues/2347 \ No newline at end of file +//TODO === Adjusting Model Parameters https://github.com/TimefoldAI/timefold-solver/issues/2347 From 98fda2c3da758c1f15b687722ba6c41376375315 Mon Sep 17 00:00:00 2001 From: Tom Cools Date: Fri, 26 Jun 2026 18:48:25 +0200 Subject: [PATCH 10/17] docs: split out the modeling Design patterns was always a weird section in isolation. This fixes that. --- docs/src/modules/ROOT/nav.adoc | 7 +- .../design-patterns/cloud-architecture.adoc | 22 ++ .../design-patterns/design-patterns.adoc | 285 +----------------- .../design-patterns/domain-modeling.adoc | 80 +++++ .../pages/design-patterns/time-patterns.adoc | 180 +++++++++++ .../modeling-planning-problems.adoc | 2 +- 6 files changed, 292 insertions(+), 284 deletions(-) create mode 100644 docs/src/modules/ROOT/pages/design-patterns/cloud-architecture.adoc create mode 100644 docs/src/modules/ROOT/pages/design-patterns/domain-modeling.adoc create mode 100644 docs/src/modules/ROOT/pages/design-patterns/time-patterns.adoc diff --git a/docs/src/modules/ROOT/nav.adoc b/docs/src/modules/ROOT/nav.adoc index 4c47e80bc45..7e8ed2ab736 100644 --- a/docs/src/modules/ROOT/nav.adoc +++ b/docs/src/modules/ROOT/nav.adoc @@ -11,7 +11,10 @@ ** xref:quickstart/quarkus-vehicle-routing/quarkus-vehicle-routing-quickstart.adoc[Vehicle Routing (Guide)] ** https://github.com/TimefoldAI/timefold-quickstarts[More examples on GitHub^] -* xref:running-timefold-solver/modeling-planning-problems.adoc[leveloffset=+1] +* Building your model +** xref:running-timefold-solver/modeling-planning-problems.adoc[leveloffset=+1] +** xref:design-patterns/domain-modeling.adoc[leveloffset=+1] +** xref:design-patterns/time-patterns.adoc[leveloffset=+1] * xref:constraints-and-score/overview.adoc[Constraints and score] ** xref:constraints-and-score/score-calculation.adoc[leveloffset=+1] @@ -21,6 +24,7 @@ ** xref:constraints-and-score/performance.adoc[leveloffset=+1] * xref:running-timefold-solver/overview.adoc[Running the Solver] +** xref:design-patterns/cloud-architecture.adoc[leveloffset=+1] ** xref:service/overview.adoc[Service Reference (Preview)] *** xref:service/rest-api.adoc[leveloffset=+1] *** xref:service/modeling-changes.adoc[leveloffset=+1] @@ -44,7 +48,6 @@ *** xref:optimization-algorithms/move-selector-reference.adoc[leveloffset=+1] * xref:responding-to-change/responding-to-change.adoc[leveloffset=+1] * xref:integration/integration.adoc[leveloffset=+1] -* xref:design-patterns/design-patterns.adoc[leveloffset=+1] * xref:frequently-asked-questions.adoc[leveloffset=+1] * https://github.com/TimefoldAI/timefold-solver/releases[New and noteworthy][leveloffset=+1] * Upgrading Timefold Solver diff --git a/docs/src/modules/ROOT/pages/design-patterns/cloud-architecture.adoc b/docs/src/modules/ROOT/pages/design-patterns/cloud-architecture.adoc new file mode 100644 index 00000000000..dc327c660a9 --- /dev/null +++ b/docs/src/modules/ROOT/pages/design-patterns/cloud-architecture.adoc @@ -0,0 +1,22 @@ +[#cloudArchitecturePatterns] += Cloud architecture patterns +:doctype: book +:sectnums: +:icons: font + +There are two common usage patterns of Timefold Solver in the cloud: + +* *Batch planning*: +Typically runs at night for hours to solve each tenant's dataset +and deliver each schedule for the upcoming day(s) or week(s). +Only the final best solution is sent back to the client. +This is a good fit for a serverless cloud architecture. + +* *Real-time planning*: +Typically runs during the day, +to handle unexpected problem changes as they occur in real-time +and sends each best solution as they are discovered to the client. + +image::design-patterns/serverlessCloudArchitecture.png[align="center"] + +image::design-patterns/realTimePlanningCloudArchitecture.png[align="center"] diff --git a/docs/src/modules/ROOT/pages/design-patterns/design-patterns.adoc b/docs/src/modules/ROOT/pages/design-patterns/design-patterns.adoc index 8a13d916b24..3cbefdf3e25 100644 --- a/docs/src/modules/ROOT/pages/design-patterns/design-patterns.adoc +++ b/docs/src/modules/ROOT/pages/design-patterns/design-patterns.adoc @@ -4,285 +4,8 @@ :sectnums: :icons: font -:relevance: library-and-service -:notes: Most of this should be in the "how to model" reference. Design patterns is too weak a term. +This page has been split into three separate guides: - - -[#designPatternsIntroduction] -== Design patterns introduction - -Timefold Solver design patterns are generic reusable solutions to common challenges in the model or architecture of projects that perform constraint solving. The design patterns in this section list and solve common design challenges. - -[#domainModelingGuide] -== Domain modeling guidelines - -Follow the guidelines listed in this section to create a well thought-out model that can contribute significantly to the success of your planning. - - -. *Draw a class diagram of your domain model.* -.. Make sure there are no duplications in your data model and that relationships between objects are clearly defined. - -.. Create sample instances for each class. For example, in the employee rostering `Employee` class, create `Ann`, `Bert`, and `Carl`. - -. *Determine which relationships (or fields) change during planning and color them orange.* -One side of these relationships will become a planning variable later on. -For example, in employee rostering, the `Shift` to `Employee` relationship changes during planning, so it is orange. -However, other relationships, such as from `Employee` to `Skill`, are immutable during planning -because Timefold Solver cannot assign an extra skill to an employee. - -. *If there are multiple relationships (or fields), check for xref:running-timefold-solver/modeling-planning-problems.adoc#shadowVariable[shadow variables]*. -A shadow variable changes during planning, but its value can be calculated based on one or more genuine planning variables, without dispute. Color shadow relationships (or fields) purple. -+ -[NOTE] -==== -Only one side of a bi-directional relationship can be a genuine planning variable. -The other side will become an xref:running-timefold-solver/modeling-planning-problems.adoc#bidirectionalVariable[inverse relation shadow variable] later on. -Keep bi-directional relationships orange. -==== - -. *If the goal is to find an optimal order of elements*, use the <>. - -. *If there is an orange many-to-many relationship, replace it -with a one-to-many and a many-to-one relationship to a new intermediate class.* - -+ -The following figure illustrates introducing a `ShiftAssignment` class -to represent the many-to-many relationship between `Shift` and `Employee`. -`Shift` contains every shift time that needs to be filled with an employee. -+ -image::design-patterns/employeeShiftRosteringModelingGuideA.png[align="center"] - -+ -[NOTE] -==== -Timefold Solver does not currently support a `@PlanningVariable` annotation on a collection. -xref:running-timefold-solver/modeling-planning-problems.adoc#planningListVariable[Planning list variable] is not a means of achieving a many-to-one relationship; -it serves to indicate that these values happen in a sequence one after another. -==== - -. *Annotate a many-to-one relationship with a `@PlanningEntity` annotation.* Usually the _many_ side of the relationship is the planning entity class that contains the planning variable. If the relationship is bi-directional, both sides are a planning entity class but usually the _many_ side has the planning variable and the _one_ side has the shadow variable. For example, in employee rostering, the `ShiftAssignment` class has an `@PlanningEntity` annotation. - -. *Make sure that the planning entity class has at least one problem property*. -A planning entity class cannot consist of only planning variables or an ID and only planning variables. -.. Remove any surplus `@PlanningVariable` annotations so that they become problem properties. -Doing this significantly decreases xref:optimization-algorithms/overview.adoc#searchSpaceSize[the search space size] and significantly increases solving efficiency. For example, in employee rostering, the `ShiftAssignment` class should not annotate both the `Shift` and `Employee` relationship with `@PlanningVariable`. -.. Make sure that when all planning variables have a value of `null`, -the planning entity instance is describable to the business people. -Planning variables have a value of `null` when the planning solution is uninitialized. -** A surrogate ID does not suffice as the required minimum of one problem property. -** There is no need to add a hard constraint to assure that two planning entities are different. -They are already different due to their problem properties. -** In some cases, multiple planning entity instances have the same set of problem properties. -In such cases, it can be useful to create an extra problem property to distinguish them. -For example, in employee rostering, -the `ShiftAssignment` class has the problem property `Shift` -as well as the problem property `indexInShift` which is an `int` class. - -. *Choose the model in which the number of planning entities is fixed during planning*. -For example, in the employee rostering, -it is impossible to know in advance how many shifts each employee will have before Timefold Solver solves the model -and the results can differ for each solution found. -On the other hand, the number of employees per shift is known in advance, -so it is better to make the `Shift` relationship a problem property -and the `Employee` relationship a planning variable as shown in the following examples. -+ -image::design-patterns/employeeShiftRosteringModelingGuideB.png[align="center"] - -[#assigningTimeToPlanningEntities] -== Assigning time to planning entities - -Dealing with time and dates in planning problems may be problematic because it is dependent on the needs of your use case. - -There are several representations of timestamps, dates, durations and periods in Java and Kotlin. -Choose the right representation type for your use case: - -* ``DayOfWeek`` with or without ``LocalTime`` if no date is involved. -* ``LocalDate`` if no time is involved. -* ``LocalDateTime`` if your model only works in a single timezone without DST (Daylight Saving Time). -* ``OffsetDateTime`` if your model supports timezones or DST (Daylight Saving Time). -** Avoid ``ZonedDateTime``, it is error-prone. -* Never use ``java.util.Date``: it is a slow, error-prone way to represent timestamps. - -There are also several designs for assigning a planning entity to a starting time (or date): - -* If the starting time is fixed beforehand, it is not a planning variable (in that solver). -** For example: in Bed Allocation Scheduling, the arrival day of each patient is fixed beforehand. -** This is common in xref:responding-to-change/responding-to-change.adoc#multiStagePlanning[multi-stage planning], -when the starting time has been decided already in an earlier planning stage. - -* If the starting time is not fixed, it is a planning variable (genuine or shadow). - -** If all planning entities have the same duration, -use the <>. -*** For example, in school timetabling, all lectures take one hour. Therefore, each timeslot is one hour. -*** Even if the planning entities have different durations, but the same duration per type, it's often appropriate. -**** For example, in conference scheduling, breakout talks take one hour and lab talks take 2 hours. -But there's an enumeration of the timeslots and each timeslot only accepts one talk type. - -** If the duration differs and time is rounded to a specific time granularity (for example 5 minutes) -use the <>. - -** If the duration differs and one task starts immediately after the previous task (assigned to the same executor) finishes, -use the <>. -*** For example, in time windowed vehicle routing, each vehicle departs immediately to the next customer when the delivery for the previous customer finishes. -*** Even if the next task does not always start immediately, but the gap is deterministic, it applies. -**** For example, in vehicle routing, each driver departs immediately to the next customer, -unless it's the first departure after noon, in which case there's first a 1 hour lunch. - -** If the employees need to decide the order of theirs tasks per day, week or SCRUM sprint themselves, -use the <>. -*** For example, in elevator maintenance scheduling, a mechanic gets up to 40 hours worth of tasks per week, -but there's no point in ordering them within 1 week because there's likely to be disruption from entrapments or other elevator outages. - -Choose the right pattern depending on the use case: - -image::design-patterns/assigningTimeToPlanningEntities.png[align="center"] - -image::design-patterns/assigningTimeToPlanningEntities2.png[align="center"] - - -[#timeslotPattern] -=== Timeslot pattern: assign to a fixed-length timeslot - -If all planning entities have *the same duration* (or can be inflated to the same duration), the Timeslot pattern is useful. -The planning entities are assigned to a timeslot rather than time. -For example, in school timetabling, all lectures take one hour. - -The timeslots can start at any time. -For example, the timeslots start at 8:00, 9:00, 10:15 (after a 15-minute break), 11:15, ... They can even overlap, but that is unusual. - -It is also usable if all planning entities can be inflated to the same duration. -For example, in Examination Timetabling, some exams take 90 minutes and others 120 minutes, but all timeslots are 120 minutes. -When an exam of 90 minutes is assigned to a timeslot, for the remaining 30 minutes, its seats are occupied too and cannot be used by another exam. - -Usually there is a second planning variable, for example the room. -In course timetabling, two lectures are in conflict if they share the same room at the same timeslot. -However, in exam timetabling, that is allowed, if there is enough seating capacity in the room (although mixed exam durations in the same room do inflict a soft score penalty). - - -[#timeGrainPattern] -=== TimeGrain pattern: assign to a starting TimeGrain - -Assigning humans to start a meeting at four seconds after 9 o'clock is pointless because most human activities have a time granularity of five minutes or 15 minutes. -Therefore it is not necessary to allow a planning entity to be assigned subsecond, second or even one minute accuracy. -A granularity of 15 minutes, 1 hour or 1 day accuracy suffices for most use cases. -The TimeGrain pattern models such *time accuracy* by partitioning time as time grains. -For example, in Meeting Scheduling, all meetings start/end in hour, half hour, or 15-minute intervals before or after each hour, -therefore the optimal settings for time grains is 15 minutes. - -Each planning entity is assigned to a start time grain. -The end time grain is calculated by adding the duration in grains to the starting time grain. -Overlap of two entities is determined by comparing their start and end time grains. - -*The TimeGrain pattern doesn't scale well*. -Especially with a finer time granularity (such as 1 minute) and a long planning window, -the value range (and therefore xref:optimization-algorithms/overview.adoc#searchSpaceSize[the search space]) is too big to scale well. -It's recommended to use a coarse time granularity (such as 1 week, 1 day, 1 half day, ...) or shorten the planning window size to scale. -To resolve scaling issues, the <> is often a good alternative. - -[#chainedThroughTimePattern] -=== Chained through time pattern: assign in a chain that determines starting time - -If a person or a machine continuously works on **one task at a time in sequence**, -which means starting a task when the previous is finished (or with a deterministic delay), the Chained Through Time pattern is useful. -For example, in xref:quickstart/quarkus-vehicle-routing/quarkus-vehicle-routing-quickstart.adoc[Vehicle routing with time windows], -a vehicle drives from customer to customer (thus it handles one customer at a time). - -The focus in this pattern is on deciding the order of a set of elements instead of assigning them to a specific date and time. -However, the time coordinate of each element can be deduced from its position in the sequence. -If the elements’ position on time axis affects the score, -use a xref:running-timefold-solver/modeling-planning-problems.adoc#shadowVariable[shadow variable] to calculate the time. - -This pattern is implemented using the xref:running-timefold-solver/modeling-planning-problems.adoc#planningListVariable[planning list variable]. -The planning entity determines the starting time of the first element in its planning list variable. -The second element's starting time is calculated based on the starting time and duration of the first element. -For example, in task assignment, Beth (the entity) starts working at 8:00, thus her first task starts at 8:00. -It lasts 52 minutes, therefore her second task starts at 8:52. -The starting time of an element is usually a xref:running-timefold-solver/modeling-planning-problems.adoc#shadowVariable[shadow variable]. - -[#chainedThroughTimePatternGaps] -==== Chained through time pattern: creating gaps - -Between planning entities, there are three ways to create gaps: - -* No gaps: This is common when the anchor is a machine. For example, a build server always starts the next job when the previous finishes, without a break. -* Only deterministic gaps: This is common for humans. For example, any task that crosses the 10:00 barrier gets an extra 15 minutes duration so the human can take a break. -** A deterministic gap can be subjected to complex business logic. -For example, a cross-continent truck driver needs to rest 15 minutes after two hours of driving -(which may also occur during loading or unloading time at a customer location) -and also needs to rest 10 hours after 14 hours of work. -* Planning variable gaps: This is uncommon, because that extra planning variable reduces efficiency and scalability, -(besides impacting the xref:optimization-algorithms/overview.adoc#searchSpaceSize[search space] too). - - -[#chainedThroughTimeAutomaticCollapse] -==== Chained through time: automatic collapse - -In some use case there is an overhead time for certain tasks, -which can be shared by multiple tasks, if those are consecutively scheduled. -Basically, the solver receives a _discount_ if it combines those tasks. - -For example when delivering pizza to two different customers, -a food delivery service combines both deliveries into a single trip, -if those two customers ordered from the same restaurant around the same time and live in the same part of the city. - -image::design-patterns/chainedThroughTimeAutomaticCollapse.png[align="center"] - -Implement the automatic collapse in the xref:running-timefold-solver/modeling-planning-problems.adoc#shadowVariable[shadow variables] -that calculate the start and end times of each task. - - -[#chainedThroughTimeAutomaticDelayUntilLast] - -==== Chained through time: automatic delay until last - -Some tasks require more than one person to execute. -In such cases, both employees need to be there at the same time, -before the work can start. - -For example when assembling furniture, assembling a bed is a two-person job. - -image::design-patterns/chainedThroughTimeAutomaticDelayUntilLast.png[align="center"] - -Implement the automatic delay in the xref:running-timefold-solver/modeling-planning-problems.adoc#shadowVariable[shadow variables] -that calculates the arrival, start and end times of each task. -*Separate the arrival time from the start time.* -Additionally, add loop detection to avoid an infinite loop: - -image::design-patterns/chainedThroughTimeAutomaticDelayUntilLastLoop.png[align="center"] - - -[#timeBucketPattern] -=== Time bucket pattern: assign to a capacitated bucket per time period - -In this pattern, the time of each employee is divided into _buckets_. -For example 1 bucket per week. -Each bucket has a capacity, depending on the FTE (Full Time Equivalent), holidays and the approved vacation of the employee. -For example, a bucket usually has 40 hours for a full time employee and 20 hours for a half time employee -but only 8 hours on a specific week if the employee takes vacation the rest of that week. - -Each task is assigned to a bucket, which determines the employee and the coarse-grained time period for working on it. -_The tasks within one bucket are not ordered_: it's up to the employee to decide the order. -This gives the employee more autonomy, but makes it harder to do certain optimization, -such as minimize travel time between task locations. - -[#cloudArchitecturePatterns] -== Cloud architecture patterns - -There are two common usage patterns of Timefold Solver in the cloud: - -* *Batch planning*: -Typically runs at night for hours to solve each tenant's dataset -and deliver each schedule for the upcoming day(s) or week(s). -Only the final best solution is sent back to the client. -This is a good fit for a serverless cloud architecture. - -* *Real-time planning*: -Typically runs during the day, -to handle unexpected problem changes as they occur in real-time -and sends each best solution as they are discovered to the client. - -image::design-patterns/serverlessCloudArchitecture.png[align="center"] - -image::design-patterns/realTimePlanningCloudArchitecture.png[align="center"] +* xref:design-patterns/domain-modeling.adoc[Domain modeling guide] +* xref:design-patterns/time-patterns.adoc[Time patterns] +* xref:design-patterns/cloud-architecture.adoc[Cloud architecture patterns] diff --git a/docs/src/modules/ROOT/pages/design-patterns/domain-modeling.adoc b/docs/src/modules/ROOT/pages/design-patterns/domain-modeling.adoc new file mode 100644 index 00000000000..c6c91f91c35 --- /dev/null +++ b/docs/src/modules/ROOT/pages/design-patterns/domain-modeling.adoc @@ -0,0 +1,80 @@ +[#domainModelingGuide] += Domain modeling guide +:doctype: book +:sectnums: +:icons: font + +This guide provides reusable solutions to common challenges when modeling a planning problem. +Follow these guidelines to create a well thought-out model that can contribute significantly to the success of your planning. + +[#domainModelingGuidelines] +== Domain modeling guidelines + +. *Draw a class diagram of your domain model.* +.. Make sure there are no duplications in your data model and that relationships between objects are clearly defined. + +.. Create sample instances for each class. For example, in the employee rostering `Employee` class, create `Ann`, `Bert`, and `Carl`. + +. *Determine which relationships (or fields) change during planning and color them orange.* +One side of these relationships will become a planning variable later on. +For example, in employee rostering, the `Shift` to `Employee` relationship changes during planning, so it is orange. +However, other relationships, such as from `Employee` to `Skill`, are immutable during planning +because Timefold Solver cannot assign an extra skill to an employee. + +. *If there are multiple relationships (or fields), check for xref:running-timefold-solver/modeling-planning-problems.adoc#shadowVariable[shadow variables]*. +A shadow variable changes during planning, but its value can be calculated based on one or more genuine planning variables, without dispute. Color shadow relationships (or fields) purple. ++ +[NOTE] +==== +Only one side of a bi-directional relationship can be a genuine planning variable. +The other side will become an xref:running-timefold-solver/modeling-planning-problems.adoc#bidirectionalVariable[inverse relation shadow variable] later on. +Keep bi-directional relationships orange. +==== + +. *If the goal is to find an optimal order of elements*, use the xref:design-patterns/time-patterns.adoc#chainedThroughTimePattern[Chained Through Time pattern]. + +. *If there is an orange many-to-many relationship, replace it +with a one-to-many and a many-to-one relationship to a new intermediate class.* + ++ +The following figure illustrates introducing a `ShiftAssignment` class +to represent the many-to-many relationship between `Shift` and `Employee`. +`Shift` contains every shift time that needs to be filled with an employee. ++ +image::design-patterns/employeeShiftRosteringModelingGuideA.png[align="center"] + ++ +[NOTE] +==== +Timefold Solver does not currently support a `@PlanningVariable` annotation on a collection. +xref:running-timefold-solver/modeling-planning-problems.adoc#planningListVariable[Planning list variable] is not a means of achieving a many-to-one relationship; +it serves to indicate that these values happen in a sequence one after another. +==== + +. *Annotate a many-to-one relationship with a `@PlanningEntity` annotation.* Usually the _many_ side of the relationship is the planning entity class that contains the planning variable. If the relationship is bi-directional, both sides are a planning entity class but usually the _many_ side has the planning variable and the _one_ side has the shadow variable. For example, in employee rostering, the `ShiftAssignment` class has an `@PlanningEntity` annotation. + +. *Make sure that the planning entity class has at least one problem property*. +A planning entity class cannot consist of only planning variables or an ID and only planning variables. +.. Remove any surplus `@PlanningVariable` annotations so that they become problem properties. +Doing this significantly decreases xref:optimization-algorithms/overview.adoc#searchSpaceSize[the search space size] and significantly increases solving efficiency. For example, in employee rostering, the `ShiftAssignment` class should not annotate both the `Shift` and `Employee` relationship with `@PlanningVariable`. +.. Make sure that when all planning variables have a value of `null`, +the planning entity instance is describable to the business people. +Planning variables have a value of `null` when the planning solution is uninitialized. +** A surrogate ID does not suffice as the required minimum of one problem property. +** There is no need to add a hard constraint to assure that two planning entities are different. +They are already different due to their problem properties. +** In some cases, multiple planning entity instances have the same set of problem properties. +In such cases, it can be useful to create an extra problem property to distinguish them. +For example, in employee rostering, +the `ShiftAssignment` class has the problem property `Shift` +as well as the problem property `indexInShift` which is an `int` class. + +. *Choose the model in which the number of planning entities is fixed during planning*. +For example, in the employee rostering, +it is impossible to know in advance how many shifts each employee will have before Timefold Solver solves the model +and the results can differ for each solution found. +On the other hand, the number of employees per shift is known in advance, +so it is better to make the `Shift` relationship a problem property +and the `Employee` relationship a planning variable as shown in the following examples. ++ +image::design-patterns/employeeShiftRosteringModelingGuideB.png[align="center"] diff --git a/docs/src/modules/ROOT/pages/design-patterns/time-patterns.adoc b/docs/src/modules/ROOT/pages/design-patterns/time-patterns.adoc new file mode 100644 index 00000000000..c4f14bc3a04 --- /dev/null +++ b/docs/src/modules/ROOT/pages/design-patterns/time-patterns.adoc @@ -0,0 +1,180 @@ +[#assigningTimeToPlanningEntities] += Time patterns +:doctype: book +:sectnums: +:icons: font + +Dealing with time and dates in planning problems may be problematic because it is dependent on the needs of your use case. + +There are several representations of timestamps, dates, durations and periods in Java and Kotlin. +Choose the right representation type for your use case: + +* ``DayOfWeek`` with or without ``LocalTime`` if no date is involved. +* ``LocalDate`` if no time is involved. +* ``LocalDateTime`` if your model only works in a single timezone without DST (Daylight Saving Time). +* ``OffsetDateTime`` if your model supports timezones or DST (Daylight Saving Time). +** Avoid ``ZonedDateTime``, it is error-prone. +* Never use ``java.util.Date``: it is a slow, error-prone way to represent timestamps. + +There are also several designs for assigning a planning entity to a starting time (or date): + +* If the starting time is fixed beforehand, it is not a planning variable (in that solver). +** For example: in Bed Allocation Scheduling, the arrival day of each patient is fixed beforehand. +** This is common in xref:responding-to-change/responding-to-change.adoc#multiStagePlanning[multi-stage planning], +when the starting time has been decided already in an earlier planning stage. + +* If the starting time is not fixed, it is a planning variable (genuine or shadow). + +** If all planning entities have the same duration, +use the <>. +*** For example, in school timetabling, all lectures take one hour. Therefore, each timeslot is one hour. +*** Even if the planning entities have different durations, but the same duration per type, it's often appropriate. +**** For example, in conference scheduling, breakout talks take one hour and lab talks take 2 hours. +But there's an enumeration of the timeslots and each timeslot only accepts one talk type. + +** If the duration differs and time is rounded to a specific time granularity (for example 5 minutes) +use the <>. + +** If the duration differs and one task starts immediately after the previous task (assigned to the same executor) finishes, +use the <>. +*** For example, in time windowed vehicle routing, each vehicle departs immediately to the next customer when the delivery for the previous customer finishes. +*** Even if the next task does not always start immediately, but the gap is deterministic, it applies. +**** For example, in vehicle routing, each driver departs immediately to the next customer, +unless it's the first departure after noon, in which case there's first a 1 hour lunch. + +** If the employees need to decide the order of theirs tasks per day, week or SCRUM sprint themselves, +use the <>. +*** For example, in elevator maintenance scheduling, a mechanic gets up to 40 hours worth of tasks per week, +but there's no point in ordering them within 1 week because there's likely to be disruption from entrapments or other elevator outages. + +Choose the right pattern depending on the use case: + +image::design-patterns/assigningTimeToPlanningEntities.png[align="center"] + +image::design-patterns/assigningTimeToPlanningEntities2.png[align="center"] + + +[#timeslotPattern] +== Timeslot pattern: assign to a fixed-length timeslot + +If all planning entities have *the same duration* (or can be inflated to the same duration), the Timeslot pattern is useful. +The planning entities are assigned to a timeslot rather than time. +For example, in school timetabling, all lectures take one hour. + +The timeslots can start at any time. +For example, the timeslots start at 8:00, 9:00, 10:15 (after a 15-minute break), 11:15, ... They can even overlap, but that is unusual. + +It is also usable if all planning entities can be inflated to the same duration. +For example, in Examination Timetabling, some exams take 90 minutes and others 120 minutes, but all timeslots are 120 minutes. +When an exam of 90 minutes is assigned to a timeslot, for the remaining 30 minutes, its seats are occupied too and cannot be used by another exam. + +Usually there is a second planning variable, for example the room. +In course timetabling, two lectures are in conflict if they share the same room at the same timeslot. +However, in exam timetabling, that is allowed, if there is enough seating capacity in the room (although mixed exam durations in the same room do inflict a soft score penalty). + + +[#timeGrainPattern] +== TimeGrain pattern: assign to a starting TimeGrain + +Assigning humans to start a meeting at four seconds after 9 o'clock is pointless because most human activities have a time granularity of five minutes or 15 minutes. +Therefore it is not necessary to allow a planning entity to be assigned subsecond, second or even one minute accuracy. +A granularity of 15 minutes, 1 hour or 1 day accuracy suffices for most use cases. +The TimeGrain pattern models such *time accuracy* by partitioning time as time grains. +For example, in Meeting Scheduling, all meetings start/end in hour, half hour, or 15-minute intervals before or after each hour, +therefore the optimal settings for time grains is 15 minutes. + +Each planning entity is assigned to a start time grain. +The end time grain is calculated by adding the duration in grains to the starting time grain. +Overlap of two entities is determined by comparing their start and end time grains. + +*The TimeGrain pattern doesn't scale well*. +Especially with a finer time granularity (such as 1 minute) and a long planning window, +the value range (and therefore xref:optimization-algorithms/overview.adoc#searchSpaceSize[the search space]) is too big to scale well. +It's recommended to use a coarse time granularity (such as 1 week, 1 day, 1 half day, ...) or shorten the planning window size to scale. +To resolve scaling issues, the <> is often a good alternative. + +[#chainedThroughTimePattern] +== Chained through time pattern: assign in a chain that determines starting time + +If a person or a machine continuously works on **one task at a time in sequence**, +which means starting a task when the previous is finished (or with a deterministic delay), the Chained Through Time pattern is useful. +For example, in xref:quickstart/quarkus-vehicle-routing/quarkus-vehicle-routing-quickstart.adoc[Vehicle routing with time windows], +a vehicle drives from customer to customer (thus it handles one customer at a time). + +The focus in this pattern is on deciding the order of a set of elements instead of assigning them to a specific date and time. +However, the time coordinate of each element can be deduced from its position in the sequence. +If the elements' position on time axis affects the score, +use a xref:running-timefold-solver/modeling-planning-problems.adoc#shadowVariable[shadow variable] to calculate the time. + +This pattern is implemented using the xref:running-timefold-solver/modeling-planning-problems.adoc#planningListVariable[planning list variable]. +The planning entity determines the starting time of the first element in its planning list variable. +The second element's starting time is calculated based on the starting time and duration of the first element. +For example, in task assignment, Beth (the entity) starts working at 8:00, thus her first task starts at 8:00. +It lasts 52 minutes, therefore her second task starts at 8:52. +The starting time of an element is usually a xref:running-timefold-solver/modeling-planning-problems.adoc#shadowVariable[shadow variable]. + +[#chainedThroughTimePatternGaps] +=== Chained through time pattern: creating gaps + +Between planning entities, there are three ways to create gaps: + +* No gaps: This is common when the anchor is a machine. For example, a build server always starts the next job when the previous finishes, without a break. +* Only deterministic gaps: This is common for humans. For example, any task that crosses the 10:00 barrier gets an extra 15 minutes duration so the human can take a break. +** A deterministic gap can be subjected to complex business logic. +For example, a cross-continent truck driver needs to rest 15 minutes after two hours of driving +(which may also occur during loading or unloading time at a customer location) +and also needs to rest 10 hours after 14 hours of work. +* Planning variable gaps: This is uncommon, because that extra planning variable reduces efficiency and scalability, +(besides impacting the xref:optimization-algorithms/overview.adoc#searchSpaceSize[search space] too). + + +[#chainedThroughTimeAutomaticCollapse] +=== Chained through time: automatic collapse + +In some use case there is an overhead time for certain tasks, +which can be shared by multiple tasks, if those are consecutively scheduled. +Basically, the solver receives a _discount_ if it combines those tasks. + +For example when delivering pizza to two different customers, +a food delivery service combines both deliveries into a single trip, +if those two customers ordered from the same restaurant around the same time and live in the same part of the city. + +image::design-patterns/chainedThroughTimeAutomaticCollapse.png[align="center"] + +Implement the automatic collapse in the xref:running-timefold-solver/modeling-planning-problems.adoc#shadowVariable[shadow variables] +that calculate the start and end times of each task. + + +[#chainedThroughTimeAutomaticDelayUntilLast] + +=== Chained through time: automatic delay until last + +Some tasks require more than one person to execute. +In such cases, both employees need to be there at the same time, +before the work can start. + +For example when assembling furniture, assembling a bed is a two-person job. + +image::design-patterns/chainedThroughTimeAutomaticDelayUntilLast.png[align="center"] + +Implement the automatic delay in the xref:running-timefold-solver/modeling-planning-problems.adoc#shadowVariable[shadow variables] +that calculates the arrival, start and end times of each task. +*Separate the arrival time from the start time.* +Additionally, add loop detection to avoid an infinite loop: + +image::design-patterns/chainedThroughTimeAutomaticDelayUntilLastLoop.png[align="center"] + + +[#timeBucketPattern] +== Time bucket pattern: assign to a capacitated bucket per time period + +In this pattern, the time of each employee is divided into _buckets_. +For example 1 bucket per week. +Each bucket has a capacity, depending on the FTE (Full Time Equivalent), holidays and the approved vacation of the employee. +For example, a bucket usually has 40 hours for a full time employee and 20 hours for a half time employee +but only 8 hours on a specific week if the employee takes vacation the rest of that week. + +Each task is assigned to a bucket, which determines the employee and the coarse-grained time period for working on it. +_The tasks within one bucket are not ordered_: it's up to the employee to decide the order. +This gives the employee more autonomy, but makes it harder to do certain optimization, +such as minimize travel time between task locations. diff --git a/docs/src/modules/ROOT/pages/running-timefold-solver/modeling-planning-problems.adoc b/docs/src/modules/ROOT/pages/running-timefold-solver/modeling-planning-problems.adoc index 8040b44847d..142f8a38ca5 100644 --- a/docs/src/modules/ROOT/pages/running-timefold-solver/modeling-planning-problems.adoc +++ b/docs/src/modules/ROOT/pages/running-timefold-solver/modeling-planning-problems.adoc @@ -22,7 +22,7 @@ Explore our documentation and available models https://docs.timefold.ai/[here]. Timefold Solver gives you access to a range of building blocks to model the domain of your planning problem. This page describes those building blocks in more detail. -If instead you are looking for guidance on how to create a good domain model, read the xref:design-patterns/design-patterns.adoc#domainModelingGuide[domain modeling guide]. +If instead you are looking for guidance on how to create a good domain model, read the xref:design-patterns/domain-modeling.adoc[domain modeling guide]. [NOTE] From db328194fe29bb65cfd5ad5da52663c328b92530 Mon Sep 17 00:00:00 2001 From: Tom Cools Date: Fri, 26 Jun 2026 19:05:45 +0200 Subject: [PATCH 11/17] docs: split deployment --- docs/src/modules/ROOT/nav.adoc | 5 +- .../cloud-architecture.adoc | 0 .../infrastructure-requirements.adoc | 64 +++++++++++++++++++ .../ROOT/pages/integration/integration.adoc | 62 +----------------- .../multithreaded-solving.adoc | 4 +- 5 files changed, 71 insertions(+), 64 deletions(-) rename docs/src/modules/ROOT/pages/{design-patterns => deployment}/cloud-architecture.adoc (100%) create mode 100644 docs/src/modules/ROOT/pages/deployment/infrastructure-requirements.adoc diff --git a/docs/src/modules/ROOT/nav.adoc b/docs/src/modules/ROOT/nav.adoc index 7e8ed2ab736..3e5cd7f47cc 100644 --- a/docs/src/modules/ROOT/nav.adoc +++ b/docs/src/modules/ROOT/nav.adoc @@ -24,7 +24,6 @@ ** xref:constraints-and-score/performance.adoc[leveloffset=+1] * xref:running-timefold-solver/overview.adoc[Running the Solver] -** xref:design-patterns/cloud-architecture.adoc[leveloffset=+1] ** xref:service/overview.adoc[Service Reference (Preview)] *** xref:service/rest-api.adoc[leveloffset=+1] *** xref:service/modeling-changes.adoc[leveloffset=+1] @@ -35,6 +34,10 @@ *** xref:running-timefold-solver/configuration.adoc[leveloffset=+1] *** xref:constraints-and-score/constraint-configuration.adoc#libraryWeightOverrides[Constraint weights] +* Deployment +** xref:deployment/cloud-architecture.adoc[leveloffset=+1] +** xref:deployment/infrastructure-requirements.adoc[leveloffset=+1] + * Tuning the Solver ** xref:running-timefold-solver/benchmarking-and-tweaking.adoc[leveloffset=+1] ** xref:running-timefold-solver/solver-diagnostics.adoc[leveloffset=+1] diff --git a/docs/src/modules/ROOT/pages/design-patterns/cloud-architecture.adoc b/docs/src/modules/ROOT/pages/deployment/cloud-architecture.adoc similarity index 100% rename from docs/src/modules/ROOT/pages/design-patterns/cloud-architecture.adoc rename to docs/src/modules/ROOT/pages/deployment/cloud-architecture.adoc diff --git a/docs/src/modules/ROOT/pages/deployment/infrastructure-requirements.adoc b/docs/src/modules/ROOT/pages/deployment/infrastructure-requirements.adoc new file mode 100644 index 00000000000..15290f125c0 --- /dev/null +++ b/docs/src/modules/ROOT/pages/deployment/infrastructure-requirements.adoc @@ -0,0 +1,64 @@ +[#sizingHardwareAndSoftware] += Infrastructure requirements +:doctype: book +:sectnums: +:icons: font + +Before sizing a Timefold Solver service, first understand the typical behaviour of a `Solver.solve()` call: + +image::integration/sizingHardware.png[align="center"] + +Understand these guidelines to decide the hardware for a Timefold Solver service: + +* **RAM memory**: Provision plenty, but no need to provide more. +** The problem dataset, loaded before Timefold Solver is called, often consumes the most memory. It depends on the problem scale. +*** If this is a problem, review the domain class structure: remove classes or fields that Timefold Solver doesn't need during solving. +*** Timefold Solver usually has up to three solution instances: the internal working solution, the best solution and the old best solution (when it's being replaced). However, these are all a xref:running-timefold-solver/modeling-planning-problems.adoc#cloningASolution[planning clone] of each other, so many problem fact instances are shared between those solution instances. +** During solving, the memory is very volatile, because solving creates many short-lived objects. The Garbage Collector deletes these in bulk and therefore needs some heap space as a buffer. +** The maximum size of the JVM heap space can be in three states: +*** **Insufficient**: An `OutOfMemoryException` is thrown (often because the Garbage Collector is using more than 98% of the CPU time). +*** **Narrow**: The heap buffer for those short-lived instances is too small, therefore the Garbage Collector needs to run more than it would like to, which causes a performance loss. +**** Profiling shows that in the heap chart, the used heap space frequently touches the max heap space during solving. It also shows that the Garbage Collector has a significant CPU usage impact. +**** Adding more heap space increases the xref:constraints-and-score/performance.adoc#moveEvaluationSpeed[move evaluation speed]. +*** **Plenty**: There is enough heap space. The Garbage Collector is active, but its CPU usage is low. +**** Adding more heap space does _not_ increase performance. +**** Usually, this is around 300 to 500MB above the dataset size, _regardless of the problem scale_, +except with xref:optimization-algorithms/move-selector-reference.adoc#nearbySelection[nearby selection] and caching move selector, +neither of which are used by default. +* **CPU power**: More is better. +** Improving CPU speed directly increases the xref:constraints-and-score/performance.adoc#moveEvaluationSpeed[move evaluation speed]. +*** If the CPU power is twice as fast, it takes half the time to find the same result. However, this does not guarantee that it finds a better result in the same time, nor that it finds a similar result for a problem twice as big in the same time. +*** Increasing CPU power usually does not resolve scaling issues, because planning problems scale exponentially. Power tweaking the solver configuration has far better results for scaling issues than throwing hardware at it. +** During the `solve()` method, the CPU power will max out until it returns +(except in xref:responding-to-change/responding-to-change.adoc#daemon[daemon mode] +or if your xref:optimization-algorithms/overview.adoc#SolverEventListener[SolverEventListener] writes the best solution to disk or the network). +* **Number of CPU cores**: one CPU core per active Solver, plus at least one for the operating system. +** So in a multitenant application, which has one Solver per tenant, this means one CPU core per tenant, unless the number of solver threads is limited, as that limits the number of tenants being solved in parallel. +** With Partitioned Search, presume one CPU core per partition (per active tenant), unless the number of partition threads is limited. +*** To reduce the number of used cores, it can be better to reduce the partition threads (so solve some partitions sequentially) than to reduce the number of partitions. +** In use cases with many tenants (such as scheduling Software as a Service) or many partitions, it might not be affordable to provision that many CPUs. +*** Reduce the number of active Solvers at a time. For example: give each tenant only one minute of machine time and use a `ExecutorService` with a fixed thread pool to queue requests. +*** Distribute the Solver runs across the day (or night). This is especially an opportunity in SaaS that's used across the globe, due to timezones: UK and India can use the same CPU core when scheduling at night. +** The SolverManager will take care of the orchestration, especially in those underfunded environments in which solvers (and partitions) are forced to share CPU cores or wait in line. +* **I/O (network, disk, ...)**: Not used during solving. +** Timefold Solver is not a web server: a solver thread does not block (unlike a servlet thread), each one fully drains a CPU. +*** A web server can handle 24 active servlets threads with eight cores without performance loss, because most servlets threads are blocking on I/O. +*** However, 24 active solver threads with eight cores will cause each solver's xref:constraints-and-score/performance.adoc#moveEvaluationSpeed[move evaluation speed] to be three times slower, causing a big performance loss. +** Note that calling any I/O during solving, for example a remote service in your score calculation, causes a huge performance loss because it's called thousands of times per second, so it should complete in microseconds. So no good implementation does that. + +Keep these guidelines in mind when selecting and configuring the software. +See https://timefold.ai/blog/[our blog archive] for the details of our experiments, which use our diverse set of examples. +Your mileage may vary. + +* Operating System +** No experimentally proven advice yet (but prefer Linux anyway). +* JDK +** Version: Our benchmarks have consistently shown improvements in performance when comparing new JDK releases with their predecessors. It is therefore recommended using the latest available JDK. If you're interested in the performance comparisons of Timefold Solver running of different JDK releases, you can find them in the form of blog posts in https://timefold.ai/blog/[our blog archive]. +** Garbage Collector: ParallelGC can be potentially between 5% and 35% faster than G1GC (the default). Unlike web servers, Timefold Solver needs a GC focused on throughput, not latency. Use `-XX:+UseParallelGC` to turn on ParallelGC. +* Logging can have a severe impact on performance. +** Debug logging `ai.timefold.solver` can be between 0% and 15% slower than info logging. Trace logging can be between 5% and 70% slower than info logging. +** Synchronous logging to a file has an additional significant impact for debug and trace logging (but not for info logging). +* Avoid a cloud environment in which you share your CPU core(s) with other virtual machines or containers. Performance (and therefore solution quality) can be unreliable when the available CPU power varies greatly. + +Keep in mind that the perfect hardware/software environment will probably _not_ solve scaling issues (even Moore's law is too slow). +There is no need to follow these guidelines to the letter. diff --git a/docs/src/modules/ROOT/pages/integration/integration.adoc b/docs/src/modules/ROOT/pages/integration/integration.adoc index e5fcda49d09..b91e5145b27 100644 --- a/docs/src/modules/ROOT/pages/integration/integration.adoc +++ b/docs/src/modules/ROOT/pages/integration/integration.adoc @@ -1116,64 +1116,4 @@ For this reason, it is recommended that the human planner is actively involved i image::integration/keepTheUserInControl.png[align="center"] -[#sizingHardwareAndSoftware] -== Sizing hardware and software - -Before sizing a Timefold Solver service, first understand the typical behaviour of a `Solver.solve()` call: - -image::integration/sizingHardware.png[align="center"] - -Understand these guidelines to decide the hardware for a Timefold Solver service: - -* **RAM memory**: Provision plenty, but no need to provide more. -** The problem dataset, loaded before Timefold Solver is called, often consumes the most memory. It depends on the problem scale. -*** If this is a problem, review the domain class structure: remove classes or fields that Timefold Solver doesn't need during solving. -*** Timefold Solver usually has up to three solution instances: the internal working solution, the best solution and the old best solution (when it's being replaced). However, these are all a xref:running-timefold-solver/modeling-planning-problems.adoc#cloningASolution[planning clone] of each other, so many problem fact instances are shared between those solution instances. -** During solving, the memory is very volatile, because solving creates many short-lived objects. The Garbage Collector deletes these in bulk and therefore needs some heap space as a buffer. -** The maximum size of the JVM heap space can be in three states: -*** **Insufficient**: An `OutOfMemoryException` is thrown (often because the Garbage Collector is using more than 98% of the CPU time). -*** **Narrow**: The heap buffer for those short-lived instances is too small, therefore the Garbage Collector needs to run more than it would like to, which causes a performance loss. -**** Profiling shows that in the heap chart, the used heap space frequently touches the max heap space during solving. It also shows that the Garbage Collector has a significant CPU usage impact. -**** Adding more heap space increases the xref:constraints-and-score/performance.adoc#moveEvaluationSpeed[move evaluation speed]. -*** **Plenty**: There is enough heap space. The Garbage Collector is active, but its CPU usage is low. -**** Adding more heap space does _not_ increase performance. -**** Usually, this is around 300 to 500MB above the dataset size, _regardless of the problem scale_, -except with xref:optimization-algorithms/move-selector-reference.adoc#nearbySelection[nearby selection] and caching move selector, -neither of which are used by default. -* **CPU power**: More is better. -** Improving CPU speed directly increases the xref:constraints-and-score/performance.adoc#moveEvaluationSpeed[move evaluation speed]. -*** If the CPU power is twice as fast, it takes half the time to find the same result. However, this does not guarantee that it finds a better result in the same time, nor that it finds a similar result for a problem twice as big in the same time. -*** Increasing CPU power usually does not resolve scaling issues, because planning problems scale exponentially. Power tweaking the solver configuration has far better results for scaling issues than throwing hardware at it. -** During the `solve()` method, the CPU power will max out until it returns -(except in xref:responding-to-change/responding-to-change.adoc#daemon[daemon mode] -or if your xref:optimization-algorithms/overview.adoc#SolverEventListener[SolverEventListener] writes the best solution to disk or the network). -* **Number of CPU cores**: one CPU core per active Solver, plus at least one for the operating system. -** So in a multitenant application, which has one Solver per tenant, this means one CPU core per tenant, unless the number of solver threads is limited, as that limits the number of tenants being solved in parallel. -** With Partitioned Search, presume one CPU core per partition (per active tenant), unless the number of partition threads is limited. -*** To reduce the number of used cores, it can be better to reduce the partition threads (so solve some partitions sequentially) than to reduce the number of partitions. -** In use cases with many tenants (such as scheduling Software as a Service) or many partitions, it might not be affordable to provision that many CPUs. -*** Reduce the number of active Solvers at a time. For example: give each tenant only one minute of machine time and use a `ExecutorService` with a fixed thread pool to queue requests. -*** Distribute the Solver runs across the day (or night). This is especially an opportunity in SaaS that's used across the globe, due to timezones: UK and India can use the same CPU core when scheduling at night. -** The SolverManager will take care of the orchestration, especially in those underfunded environments in which solvers (and partitions) are forced to share CPU cores or wait in line. -* **I/O (network, disk, ...)**: Not used during solving. -** Timefold Solver is not a web server: a solver thread does not block (unlike a servlet thread), each one fully drains a CPU. -*** A web server can handle 24 active servlets threads with eight cores without performance loss, because most servlets threads are blocking on I/O. -*** However, 24 active solver threads with eight cores will cause each solver's xref:constraints-and-score/performance.adoc#moveEvaluationSpeed[move evaluation speed] to be three times slower, causing a big performance loss. -** Note that calling any I/O during solving, for example a remote service in your score calculation, causes a huge performance loss because it's called thousands of times per second, so it should complete in microseconds. So no good implementation does that. - -Keep these guidelines in mind when selecting and configuring the software. -See https://timefold.ai/blog/[our blog archive] for the details of our experiments, which use our diverse set of examples. -Your mileage may vary. - -* Operating System -** No experimentally proven advice yet (but prefer Linux anyway). -* JDK -** Version: Our benchmarks have consistently shown improvements in performance when comparing new JDK releases with their predecessors. It is therefore recommended using the latest available JDK. If you're interested in the performance comparisons of Timefold Solver running of different JDK releases, you can find them in the form of blog posts in https://timefold.ai/blog/[our blog archive]. -** Garbage Collector: ParallelGC can be potentially between 5% and 35% faster than G1GC (the default). Unlike web servers, Timefold Solver needs a GC focused on throughput, not latency. Use `-XX:+UseParallelGC` to turn on ParallelGC. -* Logging can have a severe impact on performance. -** Debug logging `ai.timefold.solver` can be between 0% and 15% slower than info logging. Trace logging can be between 5% and 70% slower than info logging. -** Synchronous logging to a file has an additional significant impact for debug and trace logging (but not for info logging). -* Avoid a cloud environment in which you share your CPU core(s) with other virtual machines or containers. Performance (and therefore solution quality) can be unreliable when the available CPU power varies greatly. - -Keep in mind that the perfect hardware/software environment will probably _not_ solve scaling issues (even Moore's law is too slow). -There is no need to follow these guidelines to the letter. +For information on sizing hardware and software, see xref:deployment/infrastructure-requirements.adoc[Infrastructure requirements]. diff --git a/docs/src/modules/ROOT/pages/running-timefold-solver/multithreaded-solving.adoc b/docs/src/modules/ROOT/pages/running-timefold-solver/multithreaded-solving.adoc index 4a8e8f00878..b2e5014e257 100644 --- a/docs/src/modules/ROOT/pages/running-timefold-solver/multithreaded-solving.adoc +++ b/docs/src/modules/ROOT/pages/running-timefold-solver/multithreaded-solving.adoc @@ -121,7 +121,7 @@ xsi:schemaLocation="https://timefold.ai/xsd/solver https://timefold.ai/xsd/solve Setting `moveThreadCount` to `AUTO` allows Timefold Solver to decide how many move threads to run in parallel. This formula is based on experience and does not hog all CPU cores on a multi-core machine. -A `moveThreadCount` of `4` xref:integration/integration.adoc#sizingHardwareAndSoftware[saturates almost 5 CPU cores]. +A `moveThreadCount` of `4` xref:deployment/infrastructure-requirements.adoc#sizingHardwareAndSoftware[saturates almost 5 CPU cores]. The 4 move threads fill up 4 CPU cores completely and the solver thread uses most of another CPU core. @@ -320,7 +320,7 @@ However, Timefold Solver has a system to prevent CPU starving of other processes (such as an SSH connection in production or your IDE in development) or other threads (such as the servlet threads that handle REST requests). -As explained in xref:integration/integration.adoc#sizingHardwareAndSoftware[sizing hardware and software], +As explained in xref:deployment/infrastructure-requirements.adoc#sizingHardwareAndSoftware[sizing hardware and software], each solver (including each child solver) does no IO during `solve()` and therefore saturates one CPU core completely. In Partitioned Search, every partition always has its own thread, called a part thread. It is impossible for two partitions to share a thread, From 9a1fd7627f90c05cd811fb0f86296ce269db5adf Mon Sep 17 00:00:00 2001 From: Tom Cools Date: Fri, 26 Jun 2026 19:32:43 +0200 Subject: [PATCH 12/17] docs: split integration docs --- docs/src/modules/ROOT/nav.adoc | 7 +- .../constraint-configuration.adoc | 4 +- .../understanding-the-score.adoc | 4 +- .../pages/frequently-asked-questions.adoc | 50 + .../ROOT/pages/integration/integration.adoc | 1092 +---------------- .../pages/integration/persistent-storage.adoc | 249 ++++ .../ROOT/pages/integration/quarkus.adoc | 371 ++++++ .../ROOT/pages/integration/spring-boot.adoc | 367 ++++++ .../quarkus/quarkus-quickstart.adoc | 2 +- .../spring-boot/spring-boot-quickstart.adoc | 2 +- .../library-integration.adoc | 52 +- .../framework-version-warning.adoc | 2 +- 12 files changed, 1088 insertions(+), 1114 deletions(-) create mode 100644 docs/src/modules/ROOT/pages/integration/persistent-storage.adoc create mode 100644 docs/src/modules/ROOT/pages/integration/quarkus.adoc create mode 100644 docs/src/modules/ROOT/pages/integration/spring-boot.adoc diff --git a/docs/src/modules/ROOT/nav.adoc b/docs/src/modules/ROOT/nav.adoc index 3e5cd7f47cc..eeee1208a7f 100644 --- a/docs/src/modules/ROOT/nav.adoc +++ b/docs/src/modules/ROOT/nav.adoc @@ -30,9 +30,12 @@ *** xref:constraints-and-score/constraint-configuration.adoc#serviceWeightOverrides[Constraint weights] *** xref:service/demo-data.adoc[leveloffset=+1] *** xref:service/exposing-metrics.adoc[leveloffset=+1] -** xref:running-timefold-solver/library-integration.adoc[Use as a Library (Advanced)] +** xref:running-timefold-solver/library-integration.adoc[Use as a Library] *** xref:running-timefold-solver/configuration.adoc[leveloffset=+1] *** xref:constraints-and-score/constraint-configuration.adoc#libraryWeightOverrides[Constraint weights] +*** xref:integration/quarkus.adoc[leveloffset=+1] +*** xref:integration/spring-boot.adoc[leveloffset=+1] +*** xref:integration/persistent-storage.adoc[leveloffset=+1] * Deployment ** xref:deployment/cloud-architecture.adoc[leveloffset=+1] @@ -41,7 +44,6 @@ * Tuning the Solver ** xref:running-timefold-solver/benchmarking-and-tweaking.adoc[leveloffset=+1] ** xref:running-timefold-solver/solver-diagnostics.adoc[leveloffset=+1] -** xref:running-timefold-solver/multithreaded-solving.adoc#multithreadedIncrementalSolving[Multithreaded solving] ** xref:optimization-algorithms/overview.adoc[Optimization algorithms] *** xref:optimization-algorithms/construction-heuristics.adoc[leveloffset=+1] *** xref:optimization-algorithms/local-search.adoc[leveloffset=+1] @@ -50,7 +52,6 @@ *** xref:optimization-algorithms/neighborhoods.adoc[Neighborhoods API] *** xref:optimization-algorithms/move-selector-reference.adoc[leveloffset=+1] * xref:responding-to-change/responding-to-change.adoc[leveloffset=+1] -* xref:integration/integration.adoc[leveloffset=+1] * xref:frequently-asked-questions.adoc[leveloffset=+1] * https://github.com/TimefoldAI/timefold-solver/releases[New and noteworthy][leveloffset=+1] * Upgrading Timefold Solver diff --git a/docs/src/modules/ROOT/pages/constraints-and-score/constraint-configuration.adoc b/docs/src/modules/ROOT/pages/constraints-and-score/constraint-configuration.adoc index b0add211659..e7466787b02 100644 --- a/docs/src/modules/ROOT/pages/constraints-and-score/constraint-configuration.adoc +++ b/docs/src/modules/ROOT/pages/constraints-and-score/constraint-configuration.adoc @@ -172,8 +172,8 @@ Overrides are part of the planning solution, and as such they are automatically serialized into JSON using Jackson, assuming either of the following conditions are met: -* You use Timefold Solver's xref:integration/integration.adoc#integrationWithQuarkus[Quarkus integration], -* you use Timefold Solver's xref:integration/integration.adoc#integrationWithSpringBoot[Spring Boot integration], +* You use Timefold Solver's xref:integration/quarkus.adoc#integrationWithQuarkus[Quarkus integration], +* you use Timefold Solver's xref:integration/spring-boot.adoc#integrationWithSpringBoot[Spring Boot integration], * or you directly included the `timefold-solver-jackson` module in your project. Overrides doesn't natively deserialize from JSON back to Java objects. diff --git a/docs/src/modules/ROOT/pages/constraints-and-score/understanding-the-score.adoc b/docs/src/modules/ROOT/pages/constraints-and-score/understanding-the-score.adoc index 37ee39e8110..25bb99aea67 100644 --- a/docs/src/modules/ROOT/pages/constraints-and-score/understanding-the-score.adoc +++ b/docs/src/modules/ROOT/pages/constraints-and-score/understanding-the-score.adoc @@ -208,8 +208,8 @@ To succeed at this, `ScoreAnalysis` is JSON-friendly and can be easily sent over `ScoreAnalysis` instances will serialize into JSON automatically (using Jackson): -* If you use Timefold Solver's xref:integration/integration.adoc#integrationWithQuarkus[Quarkus integration], -* or if you use Timefold Solver's xref:integration/integration.adoc#integrationWithSpringBoot[Spring Boot integration], +* If you use Timefold Solver's xref:integration/quarkus.adoc#integrationWithQuarkus[Quarkus integration], +* or if you use Timefold Solver's xref:integration/spring-boot.adoc#integrationWithSpringBoot[Spring Boot integration], * or if you directly included the `timefold-solver-jackson` module in your project. If you implemented `ConstraintJustication` to provide custom justification objects, you are responsible for making them JSON-friendly yourself. diff --git a/docs/src/modules/ROOT/pages/frequently-asked-questions.adoc b/docs/src/modules/ROOT/pages/frequently-asked-questions.adoc index 6b02a3f9e08..e552b04dcd6 100644 --- a/docs/src/modules/ROOT/pages/frequently-asked-questions.adoc +++ b/docs/src/modules/ROOT/pages/frequently-asked-questions.adoc @@ -22,6 +22,56 @@ These models are built upon _Timefold Solver Enterprise Edition_ technology and See all available models on https://app.timefold.ai/[our platform]. +== Which versions of Java, Quarkus, and Spring Boot are supported? + +[#compatibilityMatrix] +Timefold Solver integrates with xref:integration/quarkus.adoc[Quarkus] and xref:integration/spring-boot.adoc[Spring Boot] out of the box. +Get started quickly with the xref:quickstart/quarkus/quarkus-quickstart.adoc#quarkusQuickStart[Quarkus quick start] +or the xref:quickstart/spring-boot/spring-boot-quickstart.adoc#springBootQuickStart[Spring Boot quick start]. + +The following table lists the supported versions of Java, Quarkus, and Spring Boot for each Timefold Solver major version: + +[cols="1,2,2,2",options="header"] +|=== +|Timefold Solver |Java Baseline |Quarkus |Spring Boot + +|1.x +|17 +|3.x +|3.x + +|2.x +|21 +|3.x +|4.x +|=== + +== Will Timefold Solver replace our human planners? + +A good Timefold Solver implementation beats any good human planner for non-trivial datasets. +Many human planners fail to accept this, often because they feel threatened by an automated system. + +But despite that, both can benefit if the human planner becomes the supervisor of Timefold Solver: + +* *The human planner defines, validates, and tweaks the score function.* +** The human planner tweaks the constraint weights of the xref:constraints-and-score/constraint-configuration.adoc[constraint configuration] in a UI, +as the business priorities change over time. +** When the business changes, the score function often needs to change too. +The human planner can notify the developers to add, change or remove score constraints. +* *The human planner is always in control of Timefold Solver.* +** The human planner can pin down one or more planning variables to a specific planning value. +Because they are xref:responding-to-change/responding-to-change.adoc#pinnedPlanningEntities[pinned], +Timefold Solver does not change them: it optimizes the planning around the enforcements made by the human. +If the human planner pins down all planning variables, he/she sidelines Timefold Solver completely. +** In a prototype implementation, the human planner occasionally uses pinning to intervene, +but as the implementation matures, this should become obsolete. +The feature should be kept available as a reassurance for the humans, +and in the event that the business changes dramatically before the score constraints are adjusted accordingly. + +For this reason, it is recommended that the human planner is actively involved in your project. + +image::integration/keepTheUserInControl.png[align="center"] + == Can Timefold Solver be included in a (GraalVM) native application? Yes. Timefold Solver has been tested with Quarkus and Spring Boot plugins to create native executables. diff --git a/docs/src/modules/ROOT/pages/integration/integration.adoc b/docs/src/modules/ROOT/pages/integration/integration.adoc index b91e5145b27..7dfe87aba4d 100644 --- a/docs/src/modules/ROOT/pages/integration/integration.adoc +++ b/docs/src/modules/ROOT/pages/integration/integration.adoc @@ -1,11 +1,8 @@ [#integration] -= Integration += Framework integrations :doctype: book :sectnums: :icons: font -:relevance: library-and-service -:notes: Relevant, needs some rewrites. Also should include the "Getting Started" for Quarkus (raw) and Spring - @@ -14,8 +11,8 @@ Timefold Solver's input and output data (the planning problem and the best solution) are plain old JavaBeans (POJOs), so integration with other Java technologies is straightforward. The most common way to use Timefold Solver is through its first-class framework integrations: -xref:#integrationWithQuarkus[Quarkus] -and xref:#integrationWithSpringBoot[Spring Boot]. +xref:integration/quarkus.adoc[Quarkus] +and xref:integration/spring-boot.adoc[Spring Boot]. Beyond that, Timefold Solver also integrates with many other Java technologies. For example: @@ -34,1086 +31,3 @@ so that changes to one do not force changes to the other. image::integration/integrationOverview.png[align="center"] - -[#compatibilityMatrix] -== Compatibility matrix - -Timefold Solver integrates with xref:#integrationWithQuarkus[Quarkus] and xref:#integrationWithSpringBoot[Spring Boot] out of the box. -Get started quickly with the xref:quickstart/quarkus/quarkus-quickstart.adoc#quarkusQuickStart[Quarkus quick start] -or the xref:quickstart/spring-boot/spring-boot-quickstart.adoc#springBootQuickStart[Spring Boot quick start]. - -The following table lists the supported versions of Java, Quarkus, and Spring Boot for each Timefold Solver major version: - -[cols="1,2,2,2",options="header"] -|=== -|Timefold Solver |Java Baseline |Quarkus |Spring Boot - -|1.x -|17 -|3.x -|3.x - -|2.x -|21 -|3.x -|4.x -|=== - - -[#integrationWithQuarkus] -== Quarkus - -To use Timefold Solver with Quarkus, read the xref:quickstart/quarkus/quarkus-quickstart.adoc#quarkusQuickStart[Quarkus Java quick start]. -If you are starting a new project, visit the https://code.quarkus.io/[code.quarkus.io] and select -the _Timefold AI constraint solver_ extension before generating your application. - -[#integrationWithQuarkusVersionPolicy] -=== Supported Quarkus versions - -The following version policy applies: - -- **On the solver `main` line:** -Timefold Solver targets the **latest Quarkus release** and the **latest Quarkus LTS release**. -Support for the latest LTS is provided on a **best-effort basis**. -When Quarkus releases a new major version (v4), Timefold Solver will follow. - -- **On the solver `1.x` line:** -Timefold Solver `1.x` targets the **latest Quarkus LTS release**. -When a new LTS is released for Quarkus v3, Timefold Solver 1.x will upgrade to it. -**Quarkus v4 will not be supported on the 1.x line.** - -[#integrationWithQuarkusProperties] -=== Available configuration properties - -Following properties are supported in the Quarkus `application.properties`: - -:property_prefix: quarkus. -:solver_name_prefix: -include::_config-properties.adoc[] - -[#integrationWithQuarkusManagedResources] -=== Injecting managed resources - -The Quarkus integration allows the injection of several managed resources, including `SolverConfig`, `SolverFactory`, -`SolverManager`, `SolutionManager`, `ConstraintVerifier` and `ConstraintMetaModel`. - -The `SolverConfig` resource is constructed by reading the `application.properties` file and classpath. -Therefore, Domain entities (solution, entities, and constraint classes) and customized properties (`spent-limit`, etc.) for the -planning problem are identified and loaded into the solver configuration. - -The available resources can be injected as follows: - -[tabs] -==== -Java:: -+ --- -[source,java,options="nowrap"] ----- -@Path("/path") -public class Resource { - - @Inject - SolverConfig solverConfig; - - @Inject - SolverFactory solverFactory; - - @Inject - SolverManager solverManager; - - @Inject - SolutionManager simpleSolutionManager; // <1> - - @Inject - ConstraintMetaModel constraintMetaModel; - - @Inject - ConstraintVerifier constraintVerifier; - - ... -} ----- -<1> You can find all the available score types in the xref:constraints-and-score/overview.adoc#scoreType[Constraints and Score] page. --- -Kotlin:: -+ --- -[source,kotlin,options="nowrap"] ----- -@Path("path") -class Resource { - - @Inject - var solverConfig:SolverConfig? - - @Inject - var solverFactory:SolverFactory? - - @Inject - var solverManager:SolverManager? - - @Inject - var simpleSolutionManager:SolutionManager? // <1> - - @Inject - var constraintMetaModel: ConstraintMetaModel? - - @Inject - var constraintVerifier:ConstraintVerifier? = null - - ... -} ----- -<1> You can find all the available score types in the xref:constraints-and-score/overview.adoc#scoreType[Constraints and Score] page. --- -==== - -Timefold provides all the necessary resources for problem-solving and analysis. However, it is still possible to manually create and override the default managed resources. To create a custom `SolverManager`, use the `SolverFactory` resource. - -[tabs] -==== -Java:: -+ --- -[source,java,options="nowrap"] ----- -package org.acme.employeescheduling.rest; - -import jakarta.enterprise.context.ApplicationScoped; -import jakarta.enterprise.inject.Default; -import jakarta.ws.rs.Produces; - -import ai.timefold.solver.core.api.solver.SolverFactory; -import ai.timefold.solver.core.api.solver.SolverManager; -import ai.timefold.solver.core.config.solver.SolverManagerConfig; - -import org.acme.employeescheduling.domain.EmployeeSchedule; - -@ApplicationScoped -public class BeanProducer { - - @Produces - @Default - public SolverManager overrideSolverManager(SolverFactory solverFactory) { - SolverManagerConfig solverManagerConfig = new SolverManagerConfig(); - return SolverManager.create(solverFactory, solverManagerConfig); - } -} ----- --- -Kotlin:: -+ --- -[source,kotlin,options="nowrap"] ----- -package org.acme.employeescheduling.rest - -import ai.timefold.solver.core.api.solver.SolverFactory -import ai.timefold.solver.core.api.solver.SolverManager -import ai.timefold.solver.core.config.solver.SolverManagerConfig -import jakarta.enterprise.context.ApplicationScoped -import jakarta.enterprise.inject.Default -import jakarta.ws.rs.Produces -import org.acme.kotlin.schooltimetabling.domain.Timetable - -@ApplicationScoped -class BeanProducer { - - @Produces - @Default - fun overrideSolverManager(solverFactory: SolverFactory?): SolverManager { - val solverManagerConfig = SolverManagerConfig() - return SolverManager.create(solverFactory, solverManagerConfig) - } -} ----- --- -==== - -[NOTE] -==== -Consider using xref:#integrationWithQuarkusMultipleResources[multiple solver configurations] instead of manually creating resources. -==== - -[#integrationWithQuarkusMultipleResources] -=== Injecting multiple instances of `SolverManager` - -Quarkus extension allows for defining different solver settings or even wholly distinct planning problems in the same -application. Timefold identifies each setting and provides a specific managed resource, `SolverManager`. - -==== Solver configuration properties - -The configuration properties for multiple solvers are defined using the namespace `quarkus.timefold.solver.`, -where `` is the name of the related solver. The `` property is only necessary when using multiple -solvers. For defining a single solver, refer to the xref:#integrationWithQuarkusProperties[Timefold configuration properties section]. -Following properties are supported: - -:property_prefix: quarkus. -:solver_name_prefix: . -include::_config-properties.adoc[] - -==== Working with multiple Solvers - -The different solver settings are configured in the `application.properties` file. For example, two different time -settings for `spent-limit` are defined as follows: - -[source,properties,options="nowrap"] ----- -# The solver "fastSolver" runs only for 5 seconds, and "regularSolver" runs for 10s -quarkus.timefold.solver."fastSolver".termination.spent-limit=5s // <1> -quarkus.timefold.solver."regularSolver".termination.spent-limit=10s // <2> ----- -<1> Define a solver config named *fastSolver*. -<2> Define a solver config named *regularSolver*. - -To inject a specific `SolverManager`, use the `@Named` annotation along with the solver configuration name, e.g., fastSolver. - -[tabs] -==== -Java:: -+ --- -[source,java,options="nowrap"] ----- -@Path("/path") -public class Resource { - - @Named("fastSolver") - SolverManager<...> fastSolver; - - @Named("regularSolver") - SolverManager<...> regularSolver; - - ... -} ----- --- -Kotlin:: -+ --- -[source,kotlin,options="nowrap"] ----- -@Path("path") -class Resource { - - private lateinit var fastSolver: SolverManager<...>? - private lateinit var regularSolver: SolverManager<...>? - - @Inject - constructor( - @Named("fastSolver") fastSolver: SolverManager, - @Named("regularSolver") regularSolver: SolverManager - ) { - this.fastSolver = fastSolver - this.regularSolver = regularSolver - } - - ... -} ----- --- -==== - -For a more advanced example, let's imagine two different steps for optimizing the school timetabling problem: - -1. Initially, a specific group of teachers is designated to teach the available lessons. -2. The next step involves assigning lessons to the rooms. - -To configure two different problems, add two XML files containing related planning configurations. - -[tabs] -==== -teachersSolverConfig.xml:: -+ -[source,xml,options="nowrap"] ----- - - org.acme.schooltimetabling.domain.TeacherToLessonSchedule - org.acme.schooltimetabling.domain.Teacher - ----- -roomsSolverConfig.xml:: -+ -[source,xml,options="nowrap"] ----- - - org.acme.schooltimetabling.domain.Timetable - org.acme.schooltimetabling.domain.Lesson - ----- -==== - -Set the solvers with the specific XML files in the `application.properties` file: - -[source,properties,options="nowrap"] ----- -quarkus.timefold.solver."teacherSolver".solver-config-xml=teachersSolverConfig.xml // <1> -quarkus.timefold.solver."roomSolver".solver-config-xml=roomsSolverConfig.xml // <2> ----- -<1> Define a solver config named *teacherSolver*. -<2> Define a solver config named *roomSolver*. - -Now let's inject both solvers. - -[tabs] -==== -Java:: -+ --- -[source,java,options="nowrap"] ----- -@PlanningSolution -public class TeacherToLessonSchedule { - ... -} - -@PlanningEntity -public class Teacher { - ... -} - -@Path("/path") -public class Resource { - - @Named("teacherSolver") - SolverManager teacherToLessonScheduleSolverManager; - - @Named("roomSolver") - SolverManager lessonToRoomTimeslotSolverManager; - - ... -} - - ----- --- -Kotlin:: -+ --- -[source,kotlin,options="nowrap"] ----- - -@PlanningSolution -class TeacherToLessonSchedule { - ... -} - -@PlanningEntity -class Teacher { - ... -} - -@Path("path") -class Resource { - - private lateinit var teacherToLessonScheduleSolverManager: SolverManager? - private lateinit var lessonToRoomTimeslotSolverManager: SolverManager? - - @Inject - constructor( - @Named("teacherSolver") teacherToLessonScheduleSolverManager: SolverManager, - @Named("roomSolver") lessonToRoomTimeslotSolverManager: SolverManager - ) { - this.teacherToLessonScheduleSolverManager = teacherToLessonScheduleSolverManager - this.lessonToRoomTimeslotSolverManager = lessonToRoomTimeslotSolverManager - } - - ... -} ----- --- -==== - -**Multi-stage** planning can also be accomplished by using a separate solver configuration for each optimization stage. - -[#integrationWithSpringBoot] -== Spring Boot - -To use Timefold Solver on Spring Boot, add the `timefold-solver-spring-boot-starter` dependency -and read the xref:quickstart/spring-boot/spring-boot-quickstart.adoc#springBootQuickStart[Spring Boot Java quick start]. - -[NOTE] -==== -Timefold Solver Spring Boot Starter only supports Spring Boot version 4.x. -==== - -[#integrationWithSpringBootProperties] -=== Available configuration properties - -These properties are supported in Spring's `application.properties`: - -:property_prefix: -:solver_name_prefix: -include::_config-properties.adoc[] - -[#integrationWithSpringBootManagedResources] -=== Injecting managed resources - -The Spring Boot integration allows the injection of several managed resources, including `SolverConfig`, `SolverFactory`, -`SolverManager`, `SolutionManager`, `ConstraintMetaModel` and `ConstraintVerifier`. - -The `SolverConfig` resource is constructed by reading the `application.properties` file and classpath. Therefore, Domain entities (solution, entities, and constraint classes) and customized properties (`spent-limit`, etc.) for the -planning problem are identified and loaded into the solver configuration. - -The available resouses can be injected as follows: - -[tabs] -==== -Java:: -+ --- -[source,java,options="nowrap"] ----- -@RestController -@RequestMapping("/path") -public class Resource { - - @Autowired - SolverConfig solverConfig; - - @Autowired - SolverFactory solverFactory; - - @Autowired - SolverManager solverManager; - - @Autowired - SolutionManager simpleSolutionManager; // <1> - - @Autowired - ConstraintMetaModel constraintMetaModel; - - @Autowired - ConstraintVerifier constraintVerifier; - - ... -} ----- -<1> You can find all the available score types in the xref:constraints-and-score/overview.adoc#scoreType[Constraints and Score] page. --- -Kotlin:: -+ --- -[source,kotlin,options="nowrap"] ----- -@RestController -@RequestMapping("/path") -class Resource { - - @Autowired - var solverConfig:SolverConfig? - - @Autowired - var solverFactory:SolverFactory? - - @Autowired - var solverManager:SolverManager? - - @Autowired - var simpleSolutionManager:SolutionManager? // <1> - - @Autowired - var constraintMetaModel:ConstraintMetaModel? = null - - @Autowired - var constraintVerifier:ConstraintVerifier? = null - - ... -} ----- -<1> You can find all the available score types in the xref:constraints-and-score/overview.adoc#scoreType[Constraints and Score] page. --- -==== - -Timefold provides all the necessary resources for problem-solving and analysis. However, it is still possible to manually create and override the default managed resources. To create a custom `SolverManager`, use the `SolverFactory` resource. - -[tabs] -==== -Java:: -+ --- -[source,java,options="nowrap"] ----- -package org.acme.schooltimetabling.rest; - -import ai.timefold.solver.core.api.solver.SolverFactory; -import ai.timefold.solver.core.api.solver.SolverManager; -import ai.timefold.solver.core.config.solver.SolverManagerConfig; -import ai.timefold.solver.spring.boot.autoconfigure.TimefoldAutoConfiguration; - -import org.acme.schooltimetabling.domain.Timetable; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.context.annotation.Import; -import org.springframework.context.annotation.Primary; - -@Configuration -@Import(TimefoldAutoConfiguration.class) -public class BeanProducer { - - @Bean - @Primary - public SolverManager overrideSolverManager(SolverFactory solverFactory) { - SolverManagerConfig solverManagerConfig = new SolverManagerConfig(); - return SolverManager.create(solverFactory, solverManagerConfig); - } -} ----- --- -Kotlin:: -+ --- -[source,kotlin,options="nowrap"] ----- -package org.acme.schooltimetabling.rest - -import ai.timefold.solver.core.api.solver.SolverFactory -import ai.timefold.solver.core.api.solver.SolverManager -import ai.timefold.solver.core.config.solver.SolverManagerConfig -import ai.timefold.solver.spring.boot.autoconfigure.TimefoldAutoConfiguration -import org.acme.schooltimetabling.domain.Timetable -import org.springframework.context.annotation.Bean -import org.springframework.context.annotation.Configuration -import org.springframework.context.annotation.Import -import org.springframework.context.annotation.Primary - -@Configuration -@Import( - TimefoldAutoConfiguration::class -) -class BeanProducer { - @Bean - @Primary - fun overrideSolverManager(solverFactory: SolverFactory?): SolverManager { - val solverManagerConfig = SolverManagerConfig() - return SolverManager.create(solverFactory, solverManagerConfig) - } -} ----- --- -==== - -[NOTE] -==== -Consider using xref:#integrationWithSpringBootMultipleResources[multiple solver configurations] instead of manually creating resources. -==== - -[#integrationWithSpringBootMultipleResources] -=== Injecting multiple instances of `SolverManager` - -Spring Boot auto-configuration module allows for defining different solver settings or even wholly distinct planning problems in the same -application. Timefold identifies each setting and provides a specific managed resource, `SolverManager`. - -==== Solver configuration properties - -The configuration properties for multiple solvers are defined using the namespace `timefold.solver.`, -where `` is the name of the related solver. The `` property is only necessary when using multiple -solvers. For defining a single solver, refer to the xref:#integrationWithSpringBootProperties[Timefold configuration properties section]. -Following properties are supported: - -:property_prefix: -:solver_name_prefix: . -include::_config-properties.adoc[] - -==== Working with multiple Solvers - -The different solver settings are configured in the `application.properties` file. For example, two different time -settings for `spent-limit` are defined as follows: - -[source,properties,options="nowrap"] ----- -# The solver "fastSolver" runs only for 5 seconds, and "regularSolver" runs for 10s -timefold.solver.fastSolver.termination.spent-limit=5s // <1> -timefold.solver.regularSolver.termination.spent-limit=10s // <2> ----- -<1> Define a solver config named *fastSolver*. -<2> Define a solver config named *regularSolver*. - -To inject a specific `SolverManager`, use the `@Qualifier` annotation along with the solver configuration name, e.g., fastSolver. - -[tabs] -==== -Java:: -+ --- -[source,java,options="nowrap"] ----- -@RestController -@RequestMapping("/path") -public class Resource { - - @Autowired - @Qualifier("fastSolver") - SolverManager<...> fastSolver; - - @Autowired - @Qualifier("regularSolver") - SolverManager<...> regularSolver; - - ... -} ----- --- -Kotlin:: -+ --- -[source,kotlin,options="nowrap"] ----- -@RestController -@RequestMapping("/path") -class Resource { - - @Autowired - @Qualifier("fastSolver") - var fastSolver: SolverManager<...>? - - @Autowired - @Qualifier("regularSolver") - var regularSolver: SolverManager<...>? - - ... -} ----- --- -==== - -For a more advanced example, let's imagine two different steps for optimizing the school timetabling problem: - -1. Initially, a specific group of teachers is designated to teach the available lessons. -2. The next step involves assigning lessons to the rooms. - -To configure two different problems, add two XML files containing related planning configurations. - -[tabs] -==== -teachersSolverConfig.xml:: -+ -[source,xml,options="nowrap"] ----- - - org.acme.schooltimetabling.domain.TeacherToLessonSchedule - org.acme.schooltimetabling.domain.Teacher - ----- -roomsSolverConfig.xml:: -+ -[source,xml,options="nowrap"] ----- - - org.acme.schooltimetabling.domain.Timetable - org.acme.schooltimetabling.domain.Lesson - ----- -==== - -Set the solvers with the specific XML files in the `application.properties` file: - -[source,properties,options="nowrap"] ----- -timefold.solver.teacherSolver.solver-config-xml=teachersSolverConfig.xml // <1> -timefold.solver.roomSolver.solver-config-xml=roomsSolverConfig.xml // <2> ----- -<1> Define a solver config named *teacherSolver*. -<2> Define a solver config named *roomSolver*. - -Now let's inject both solvers. - -[tabs] -==== -Java:: -+ --- -[source,java,options="nowrap"] ----- -@PlanningSolution -public class TeacherToLessonSchedule { - ... -} - -@PlanningEntity -public class Teacher { - ... -} - -@RestController -@RequestMapping("/path") -public class Resource { - - @Autowired - @Qualifier("teacherSolver") - SolverManager teacherToLessonScheduleSolverManager; - - @Autowired - @Qualifier("roomSolver") - SolverManager lessonToRoomTimeslotSolverManager; - - ... -} - - ----- --- -Kotlin:: -+ --- -[source,kotlin,options="nowrap"] ----- - -@PlanningSolution -class TeacherToLessonSchedule { - ... -} - -@PlanningEntity -class Teacher { - ... -} - -@RestController -@RequestMapping("/path") -class Resource { - - @Autowired - @Qualifier("teacherSolver") - var teacherToLessonScheduleSolverManager: SolverManager? - - @Autowired - @Qualifier("roomSolver") - var lessonToRoomTimeslotSolverManager: SolverManager? - - ... -} ----- --- -==== -**Multi-stage** planning can also be accomplished by using a separate solver configuration for each optimization stage. - -[#integrationWithPersistentStorage] -== Persistent storage - - -[#integrationWithJpaAndHibernate] -=== Database: JPA and Hibernate - -Enrich domain POJOs (solution, entities and problem facts) with JPA annotations -to store them in a database by calling `EntityManager.persist()`. - -[NOTE] -==== -Do not confuse JPA's `@Entity` annotation with Timefold Solver's `@PlanningEntity` annotation. -They can appear both on the same class: - -[source,java,options="nowrap"] ----- -@PlanningEntity // Timefold Solver annotation -@Entity // JPA annotation -public class Talk {...} ----- -==== - -[#jpaAndHibernatePersistingAScore] -==== JPA and Hibernate: persisting a `Score` - -The `timefold-solver-jpa` jar provides a JPA score converter for every built-in score type. - -[source,java,options="nowrap"] ----- -@PlanningSolution -@Entity -public class VehicleRoutePlan { - - @PlanningScore - @Convert(converter = HardSoftScoreConverter.class) - protected HardSoftScore score; - - ... -} ----- - -Please note that the converters make JPA and Hibernate serialize the score in a single `VARCHAR` column. -This has the disadvantage that the score cannot be used in a SQL or JPA-QL query to efficiently filter the results, for example to query all infeasible schedules. - -To avoid this limitation, https://docs.jboss.org/hibernate/orm/current/userguide/html_single/Hibernate_User_Guide.html#embeddable-mapping-custom[implement the `CompositeUserType`] to persist each score level into a separate database table column. - -[#jpaAndHibernatePlanningCloning] -==== JPA and Hibernate: planning cloning - -In JPA and Hibernate, there is usually a `@ManyToOne` relationship from most problem fact classes to the planning solution class. -Therefore, the problem fact classes reference the planning solution class, -which implies that when the solution is xref:running-timefold-solver/modeling-planning-problems.adoc#cloningASolution[planning cloned], they need to be cloned too. -Use an `@DeepPlanningClone` on each such problem fact class to enforce that: - -[source,java,options="nowrap"] ----- -@PlanningSolution // Timefold Solver annotation -@Entity // JPA annotation -public class Conference { - - @OneToMany(mappedBy="conference") - private List roomList; - - ... -} ----- - -[source,java,options="nowrap"] ----- -@DeepPlanningClone // Timefold Solver annotation: Force the default planning cloner to planning clone this class too -@Entity // JPA annotation -public class Room { - - @ManyToOne - private Conference conference; // Because of this reference, this problem fact needs to be planning cloned too - -} ----- - -Neglecting to do this can lead to persisting duplicate solutions, JPA exceptions or other side effects. - - -[#integrationWithJaxb] -=== XML or JSON: JAXB - -Enrich domain POJOs (solution, entities and problem facts) with JAXB annotations to serialize them to/from XML or JSON. - -Add a dependency to the `timefold-solver-jaxb` jar to take advantage of these extra integration features: - -[#jaxbMarshallingAScore] -==== JAXB: marshalling a `Score` - -When a `Score` is marshalled to XML or JSON by the default JAXB configuration, it's corrupted. -To fix that, configure the appropriate ``ScoreJaxbAdapter``: - -[source,java,options="nowrap"] ----- -@PlanningSolution -@XmlRootElement @XmlAccessorType(XmlAccessType.FIELD) -public class VehicleRoutePlan { - - @PlanningScore - @XmlJavaTypeAdapter(HardSoftScoreJaxbAdapter.class) - private HardSoftScore score; - - ... -} ----- - -For example, this generates pretty XML: - -[source,xml,options="nowrap"] ----- - - ... - 0hard/-200soft - ----- - -The same applies for a bendable score: - -[source,java,options="nowrap"] ----- -@PlanningSolution -@XmlRootElement @XmlAccessorType(XmlAccessType.FIELD) -public class Schedule { - - @PlanningScore - @XmlJavaTypeAdapter(BendableScoreJaxbAdapter.class) - private BendableScore score; - - ... -} ----- - -For example, with a `hardLevelsSize` of `2` and a `softLevelsSize` of `3`, that will generate: - -[source,xml,options="nowrap"] ----- - - ... - [0/0]hard/[-100/-20/-3]soft - ----- - -The `hardLevelsSize` and `softLevelsSize` implied, when reading a bendable score from an XML element, must always be in sync with those in the solver. - - -[#integrationWithJackson] -=== JSON: Jackson - -Enrich domain POJOs (solution, entities and problem facts) with Jackson annotations to serialize them to/from JSON. - -Add a dependency to the `timefold-solver-jackson` jar and register `TimefoldJacksonModule`: - -[source,java,options="nowrap"] ----- -var objectMapper = JsonMapper.builder() - .addModule(TimefoldJacksonModule.createModule()) - .build(); ----- - - -[#jacksonMarshallingAScore] -==== Jackson: marshalling a `Score` - -When a `Score` is marshalled to/from JSON by the default Jackson configuration, it fails. -The `TimefoldJacksonModule` fixes that, by using `HardSoftScoreJacksonSerializer`, -`HardSoftScoreJacksonDeserializer`, etc. - -[source,java,options="nowrap"] ----- -@PlanningSolution -public class VehicleRoutePlan { - - @PlanningScore - private HardSoftScore score; - - ... -} ----- - -For example, this generates: - -[source,json,options="nowrap"] ----- -{ - "score":"0hard/-200soft" - ... -} ----- - -[NOTE] -==== -When reading a `BendableScore`, the `hardLevelsSize` and `softLevelsSize` implied in the JSON element, -must always be in sync with those defined in the `@PlanningScore` annotation in the solution class. For example: - -[source,json,options="nowrap"] ----- -{ - "score":"[0/0]hard/[-100/-20/-3]soft" - ... -} ----- - -This JSON implies the `hardLevelsSize` is 2 and the `softLevelsSize` is 3, -which must be in sync with the `@PlanningScore` annotation: - -[source,java,options="nowrap"] ----- -@PlanningSolution -public class Schedule { - - @PlanningScore(bendableHardLevelsSize = 2, bendableSoftLevelsSize = 3) - private BendableScore score; - - ... -} ----- -==== - -When a field is the `Score` supertype (instead of a specific type such as `HardSoftScore`), -it uses `PolymorphicScoreJacksonSerializer` and `PolymorphicScoreJacksonDeserializer` to record the score type in JSON too, -otherwise it would be impossible to deserialize it: - -[source,java,options="nowrap"] ----- -@PlanningSolution -public class VehicleRoutePlan { - - @PlanningScore - private Score score; - - ... -} ----- - -For example, this generates: - -[source,json,options="nowrap"] ----- -{ - "score":{"HardSoftScore":"0hard/-200soft"} - ... -} ----- - - -[#integrationWithOtherEnvironments] -== Other environments - - -[#integrationWithJPMS] -=== Java platform module system (Jigsaw) - -Starting in version 2.0, Timefold Solver has official support for the https://openjdk.org/projects/jigsaw/spec/[Java Platform Module System (JPMS)]. - -When using Timefold Solver from code on the modulepath, -_export_ the packages that contain your domain objects, constraints and solver configuration, and _open_ them to the Timefold Solver module(s) -in your `module-info.java` file: - -[source,java,options="nowrap"] ----- -module org.acme.vehiclerouting { - requires ai.timefold.solver.core; - ... - - exports org.acme.vehiclerouting.domain; // Domain classes - exports org.acme.vehiclerouting.solver; // Constraints - - opens org.acme.vehiclerouting.domain to ai.timefold.solver.core; // Domain classes - opens org.acme.vehiclerouting.solver to ai.timefold.solver.core; // Constraints - ... -} ----- - -If this is not set up correctly, you will get errors. Usually, these mention the `unnamed module` and give detailed information of what needs to be changed. - -[source,options="nowrap"] ----- -class org.acme.schooltimetabling.domain.Timetable$Timefold$MemberAccessor$Field$lessons (in unnamed module @0x273444fe) -cannot access class org.acme.schooltimetabling.domain.Timetable (in module hello.world.school.timetabling) -because module hello.world.school.timetabling does not export org.acme.schooltimetabling.domain to unnamed module @0x273444fe ----- - -[WARNING] -==== -Only JPMS exported packages are part of the supported public API. -==== -If you access non-exported classes (for example by running on the classpath), and we later change or remove them, we will not treat that as a breaking change. -Those classes are not covered by compatibility guarantees. - -You can opt-out of JPMS by running all JAR files on the classpath instead of using the module path (for example, by not configuring a module path in your build or runtime setup). - - -[#integrationWithHumanPlanners] -== Integration with human planners (politics) - -A good Timefold Solver implementation beats any good human planner for non-trivial datasets. -Many human planners fail to accept this, often because they feel threatened by an automated system. - -But despite that, both can benefit if the human planner becomes the supervisor of Timefold Solver: - -* *The human planner defines, validates, and tweaks the score function.* -** The human planner tweaks the constraint weights of the xref:constraints-and-score/constraint-configuration.adoc[constraint configuration] in a UI, -as the business priorities change over time. -** When the business changes, the score function often needs to change too. -The human planner can notify the developers to add, change or remove score constraints. -* *The human planner is always in control of Timefold Solver.* -** The human planner can pin down one or more planning variables to a specific planning value. -Because they are xref:responding-to-change/responding-to-change.adoc#pinnedPlanningEntities[pinned], -Timefold Solver does not change them: it optimizes the planning around the enforcements made by the human. -If the human planner pins down all planning variables, he/she sidelines Timefold Solver completely. -** In a prototype implementation, the human planner occasionally uses pinning to intervene, -but as the implementation matures, this should become obsolete. -The feature should be kept available as a reassurance for the humans, -and in the event that the business changes dramatically before the score constraints are adjusted accordingly. - -For this reason, it is recommended that the human planner is actively involved in your project. - -image::integration/keepTheUserInControl.png[align="center"] - -For information on sizing hardware and software, see xref:deployment/infrastructure-requirements.adoc[Infrastructure requirements]. diff --git a/docs/src/modules/ROOT/pages/integration/persistent-storage.adoc b/docs/src/modules/ROOT/pages/integration/persistent-storage.adoc new file mode 100644 index 00000000000..8ade01866d5 --- /dev/null +++ b/docs/src/modules/ROOT/pages/integration/persistent-storage.adoc @@ -0,0 +1,249 @@ +[#integrationWithPersistentStorage] += Persistent storage +:doctype: book +:sectnums: +:icons: font + +[#integrationWithJpaAndHibernate] +== Database: JPA and Hibernate + +Enrich domain POJOs (solution, entities and problem facts) with JPA annotations +to store them in a database by calling `EntityManager.persist()`. + +[NOTE] +==== +Do not confuse JPA's `@Entity` annotation with Timefold Solver's `@PlanningEntity` annotation. +They can appear both on the same class: + +[source,java,options="nowrap"] +---- +@PlanningEntity // Timefold Solver annotation +@Entity // JPA annotation +public class Talk {...} +---- +==== + +[#jpaAndHibernatePersistingAScore] +=== JPA and Hibernate: persisting a `Score` + +The `timefold-solver-jpa` jar provides a JPA score converter for every built-in score type. + +[source,java,options="nowrap"] +---- +@PlanningSolution +@Entity +public class VehicleRoutePlan { + + @PlanningScore + @Convert(converter = HardSoftScoreConverter.class) + protected HardSoftScore score; + + ... +} +---- + +Please note that the converters make JPA and Hibernate serialize the score in a single `VARCHAR` column. +This has the disadvantage that the score cannot be used in a SQL or JPA-QL query to efficiently filter the results, for example to query all infeasible schedules. + +To avoid this limitation, https://docs.jboss.org/hibernate/orm/current/userguide/html_single/Hibernate_User_Guide.html#embeddable-mapping-custom[implement the `CompositeUserType`] to persist each score level into a separate database table column. + +[#jpaAndHibernatePlanningCloning] +=== JPA and Hibernate: planning cloning + +In JPA and Hibernate, there is usually a `@ManyToOne` relationship from most problem fact classes to the planning solution class. +Therefore, the problem fact classes reference the planning solution class, +which implies that when the solution is xref:running-timefold-solver/modeling-planning-problems.adoc#cloningASolution[planning cloned], they need to be cloned too. +Use an `@DeepPlanningClone` on each such problem fact class to enforce that: + +[source,java,options="nowrap"] +---- +@PlanningSolution // Timefold Solver annotation +@Entity // JPA annotation +public class Conference { + + @OneToMany(mappedBy="conference") + private List roomList; + + ... +} +---- + +[source,java,options="nowrap"] +---- +@DeepPlanningClone // Timefold Solver annotation: Force the default planning cloner to planning clone this class too +@Entity // JPA annotation +public class Room { + + @ManyToOne + private Conference conference; // Because of this reference, this problem fact needs to be planning cloned too + +} +---- + +Neglecting to do this can lead to persisting duplicate solutions, JPA exceptions or other side effects. + + +[#integrationWithJaxb] +== XML or JSON: JAXB + +Enrich domain POJOs (solution, entities and problem facts) with JAXB annotations to serialize them to/from XML or JSON. + +Add a dependency to the `timefold-solver-jaxb` jar to take advantage of these extra integration features: + +[#jaxbMarshallingAScore] +=== JAXB: marshalling a `Score` + +When a `Score` is marshalled to XML or JSON by the default JAXB configuration, it's corrupted. +To fix that, configure the appropriate ``ScoreJaxbAdapter``: + +[source,java,options="nowrap"] +---- +@PlanningSolution +@XmlRootElement @XmlAccessorType(XmlAccessType.FIELD) +public class VehicleRoutePlan { + + @PlanningScore + @XmlJavaTypeAdapter(HardSoftScoreJaxbAdapter.class) + private HardSoftScore score; + + ... +} +---- + +For example, this generates pretty XML: + +[source,xml,options="nowrap"] +---- + + ... + 0hard/-200soft + +---- + +The same applies for a bendable score: + +[source,java,options="nowrap"] +---- +@PlanningSolution +@XmlRootElement @XmlAccessorType(XmlAccessType.FIELD) +public class Schedule { + + @PlanningScore + @XmlJavaTypeAdapter(BendableScoreJaxbAdapter.class) + private BendableScore score; + + ... +} +---- + +For example, with a `hardLevelsSize` of `2` and a `softLevelsSize` of `3`, that will generate: + +[source,xml,options="nowrap"] +---- + + ... + [0/0]hard/[-100/-20/-3]soft + +---- + +The `hardLevelsSize` and `softLevelsSize` implied, when reading a bendable score from an XML element, must always be in sync with those in the solver. + + +[#integrationWithJackson] +== JSON: Jackson + +Enrich domain POJOs (solution, entities and problem facts) with Jackson annotations to serialize them to/from JSON. + +Add a dependency to the `timefold-solver-jackson` jar and register `TimefoldJacksonModule`: + +[source,java,options="nowrap"] +---- +var objectMapper = JsonMapper.builder() + .addModule(TimefoldJacksonModule.createModule()) + .build(); +---- + + +[#jacksonMarshallingAScore] +=== Jackson: marshalling a `Score` + +When a `Score` is marshalled to/from JSON by the default Jackson configuration, it fails. +The `TimefoldJacksonModule` fixes that, by using `HardSoftScoreJacksonSerializer`, +`HardSoftScoreJacksonDeserializer`, etc. + +[source,java,options="nowrap"] +---- +@PlanningSolution +public class VehicleRoutePlan { + + @PlanningScore + private HardSoftScore score; + + ... +} +---- + +For example, this generates: + +[source,json,options="nowrap"] +---- +{ + "score":"0hard/-200soft" + ... +} +---- + +[NOTE] +==== +When reading a `BendableScore`, the `hardLevelsSize` and `softLevelsSize` implied in the JSON element, +must always be in sync with those defined in the `@PlanningScore` annotation in the solution class. For example: + +[source,json,options="nowrap"] +---- +{ + "score":"[0/0]hard/[-100/-20/-3]soft" + ... +} +---- + +This JSON implies the `hardLevelsSize` is 2 and the `softLevelsSize` is 3, +which must be in sync with the `@PlanningScore` annotation: + +[source,java,options="nowrap"] +---- +@PlanningSolution +public class Schedule { + + @PlanningScore(bendableHardLevelsSize = 2, bendableSoftLevelsSize = 3) + private BendableScore score; + + ... +} +---- +==== + +When a field is the `Score` supertype (instead of a specific type such as `HardSoftScore`), +it uses `PolymorphicScoreJacksonSerializer` and `PolymorphicScoreJacksonDeserializer` to record the score type in JSON too, +otherwise it would be impossible to deserialize it: + +[source,java,options="nowrap"] +---- +@PlanningSolution +public class VehicleRoutePlan { + + @PlanningScore + private Score score; + + ... +} +---- + +For example, this generates: + +[source,json,options="nowrap"] +---- +{ + "score":{"HardSoftScore":"0hard/-200soft"} + ... +} +---- diff --git a/docs/src/modules/ROOT/pages/integration/quarkus.adoc b/docs/src/modules/ROOT/pages/integration/quarkus.adoc new file mode 100644 index 00000000000..02d47012aa7 --- /dev/null +++ b/docs/src/modules/ROOT/pages/integration/quarkus.adoc @@ -0,0 +1,371 @@ +[#integrationWithQuarkus] += Quarkus integration +:doctype: book +:sectnums: +:icons: font + +To use Timefold Solver with Quarkus, read the xref:quickstart/quarkus/quarkus-quickstart.adoc#quarkusQuickStart[Quarkus Java quick start]. +If you are starting a new project, visit the https://code.quarkus.io/[code.quarkus.io] and select +the _Timefold AI constraint solver_ extension before generating your application. + +[#integrationWithQuarkusVersionPolicy] +== Supported Quarkus versions + +The following version policy applies: + +- **On the solver `main` line:** +Timefold Solver targets the **latest Quarkus release** and the **latest Quarkus LTS release**. +Support for the latest LTS is provided on a **best-effort basis**. +When Quarkus releases a new major version (v4), Timefold Solver will follow. + +- **On the solver `1.x` line:** +Timefold Solver `1.x` targets the **latest Quarkus LTS release**. +When a new LTS is released for Quarkus v3, Timefold Solver 1.x will upgrade to it. +**Quarkus v4 will not be supported on the 1.x line.** + +[#integrationWithQuarkusProperties] +== Available configuration properties + +Following properties are supported in the Quarkus `application.properties`: + +:property_prefix: quarkus. +:solver_name_prefix: +include::_config-properties.adoc[] + +[#integrationWithQuarkusManagedResources] +== Injecting managed resources + +The Quarkus integration allows the injection of several managed resources, including `SolverConfig`, `SolverFactory`, +`SolverManager`, `SolutionManager`, `ConstraintVerifier` and `ConstraintMetaModel`. + +The `SolverConfig` resource is constructed by reading the `application.properties` file and classpath. +Therefore, Domain entities (solution, entities, and constraint classes) and customized properties (`spent-limit`, etc.) for the +planning problem are identified and loaded into the solver configuration. + +The available resources can be injected as follows: + +[tabs] +==== +Java:: ++ +-- +[source,java,options="nowrap"] +---- +@Path("/path") +public class Resource { + + @Inject + SolverConfig solverConfig; + + @Inject + SolverFactory solverFactory; + + @Inject + SolverManager solverManager; + + @Inject + SolutionManager simpleSolutionManager; // <1> + + @Inject + ConstraintMetaModel constraintMetaModel; + + @Inject + ConstraintVerifier constraintVerifier; + + ... +} +---- +<1> You can find all the available score types in the xref:constraints-and-score/overview.adoc#scoreType[Constraints and Score] page. +-- +Kotlin:: ++ +-- +[source,kotlin,options="nowrap"] +---- +@Path("path") +class Resource { + + @Inject + var solverConfig:SolverConfig? + + @Inject + var solverFactory:SolverFactory? + + @Inject + var solverManager:SolverManager? + + @Inject + var simpleSolutionManager:SolutionManager? // <1> + + @Inject + var constraintMetaModel: ConstraintMetaModel? + + @Inject + var constraintVerifier:ConstraintVerifier? = null + + ... +} +---- +<1> You can find all the available score types in the xref:constraints-and-score/overview.adoc#scoreType[Constraints and Score] page. +-- +==== + +Timefold provides all the necessary resources for problem-solving and analysis. However, it is still possible to manually create and override the default managed resources. To create a custom `SolverManager`, use the `SolverFactory` resource. + +[tabs] +==== +Java:: ++ +-- +[source,java,options="nowrap"] +---- +package org.acme.employeescheduling.rest; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.enterprise.inject.Default; +import jakarta.ws.rs.Produces; + +import ai.timefold.solver.core.api.solver.SolverFactory; +import ai.timefold.solver.core.api.solver.SolverManager; +import ai.timefold.solver.core.config.solver.SolverManagerConfig; + +import org.acme.employeescheduling.domain.EmployeeSchedule; + +@ApplicationScoped +public class BeanProducer { + + @Produces + @Default + public SolverManager overrideSolverManager(SolverFactory solverFactory) { + SolverManagerConfig solverManagerConfig = new SolverManagerConfig(); + return SolverManager.create(solverFactory, solverManagerConfig); + } +} +---- +-- +Kotlin:: ++ +-- +[source,kotlin,options="nowrap"] +---- +package org.acme.employeescheduling.rest + +import ai.timefold.solver.core.api.solver.SolverFactory +import ai.timefold.solver.core.api.solver.SolverManager +import ai.timefold.solver.core.config.solver.SolverManagerConfig +import jakarta.enterprise.context.ApplicationScoped +import jakarta.enterprise.inject.Default +import jakarta.ws.rs.Produces +import org.acme.kotlin.schooltimetabling.domain.Timetable + +@ApplicationScoped +class BeanProducer { + + @Produces + @Default + fun overrideSolverManager(solverFactory: SolverFactory?): SolverManager { + val solverManagerConfig = SolverManagerConfig() + return SolverManager.create(solverFactory, solverManagerConfig) + } +} +---- +-- +==== + +[NOTE] +==== +Consider using xref:#integrationWithQuarkusMultipleResources[multiple solver configurations] instead of manually creating resources. +==== + +[#integrationWithQuarkusMultipleResources] +== Injecting multiple instances of `SolverManager` + +Quarkus extension allows for defining different solver settings or even wholly distinct planning problems in the same +application. Timefold identifies each setting and provides a specific managed resource, `SolverManager`. + +=== Solver configuration properties + +The configuration properties for multiple solvers are defined using the namespace `quarkus.timefold.solver.`, +where `` is the name of the related solver. The `` property is only necessary when using multiple +solvers. For defining a single solver, refer to the xref:#integrationWithQuarkusProperties[Timefold configuration properties section]. +Following properties are supported: + +:property_prefix: quarkus. +:solver_name_prefix: . +include::_config-properties.adoc[] + +=== Working with multiple Solvers + +The different solver settings are configured in the `application.properties` file. For example, two different time +settings for `spent-limit` are defined as follows: + +[source,properties,options="nowrap"] +---- +# The solver "fastSolver" runs only for 5 seconds, and "regularSolver" runs for 10s +quarkus.timefold.solver."fastSolver".termination.spent-limit=5s // <1> +quarkus.timefold.solver."regularSolver".termination.spent-limit=10s // <2> +---- +<1> Define a solver config named *fastSolver*. +<2> Define a solver config named *regularSolver*. + +To inject a specific `SolverManager`, use the `@Named` annotation along with the solver configuration name, e.g., fastSolver. + +[tabs] +==== +Java:: ++ +-- +[source,java,options="nowrap"] +---- +@Path("/path") +public class Resource { + + @Named("fastSolver") + SolverManager<...> fastSolver; + + @Named("regularSolver") + SolverManager<...> regularSolver; + + ... +} +---- +-- +Kotlin:: ++ +-- +[source,kotlin,options="nowrap"] +---- +@Path("path") +class Resource { + + private lateinit var fastSolver: SolverManager<...>? + private lateinit var regularSolver: SolverManager<...>? + + @Inject + constructor( + @Named("fastSolver") fastSolver: SolverManager, + @Named("regularSolver") regularSolver: SolverManager + ) { + this.fastSolver = fastSolver + this.regularSolver = regularSolver + } + + ... +} +---- +-- +==== + +For a more advanced example, let's imagine two different steps for optimizing the school timetabling problem: + +1. Initially, a specific group of teachers is designated to teach the available lessons. +2. The next step involves assigning lessons to the rooms. + +To configure two different problems, add two XML files containing related planning configurations. + +[tabs] +==== +teachersSolverConfig.xml:: ++ +[source,xml,options="nowrap"] +---- + + org.acme.schooltimetabling.domain.TeacherToLessonSchedule + org.acme.schooltimetabling.domain.Teacher + +---- +roomsSolverConfig.xml:: ++ +[source,xml,options="nowrap"] +---- + + org.acme.schooltimetabling.domain.Timetable + org.acme.schooltimetabling.domain.Lesson + +---- +==== + +Set the solvers with the specific XML files in the `application.properties` file: + +[source,properties,options="nowrap"] +---- +quarkus.timefold.solver."teacherSolver".solver-config-xml=teachersSolverConfig.xml // <1> +quarkus.timefold.solver."roomSolver".solver-config-xml=roomsSolverConfig.xml // <2> +---- +<1> Define a solver config named *teacherSolver*. +<2> Define a solver config named *roomSolver*. + +Now let's inject both solvers. + +[tabs] +==== +Java:: ++ +-- +[source,java,options="nowrap"] +---- +@PlanningSolution +public class TeacherToLessonSchedule { + ... +} + +@PlanningEntity +public class Teacher { + ... +} + +@Path("/path") +public class Resource { + + @Named("teacherSolver") + SolverManager teacherToLessonScheduleSolverManager; + + @Named("roomSolver") + SolverManager lessonToRoomTimeslotSolverManager; + + ... +} + + +---- +-- +Kotlin:: ++ +-- +[source,kotlin,options="nowrap"] +---- + +@PlanningSolution +class TeacherToLessonSchedule { + ... +} + +@PlanningEntity +class Teacher { + ... +} + +@Path("path") +class Resource { + + private lateinit var teacherToLessonScheduleSolverManager: SolverManager? + private lateinit var lessonToRoomTimeslotSolverManager: SolverManager? + + @Inject + constructor( + @Named("teacherSolver") teacherToLessonScheduleSolverManager: SolverManager, + @Named("roomSolver") lessonToRoomTimeslotSolverManager: SolverManager + ) { + this.teacherToLessonScheduleSolverManager = teacherToLessonScheduleSolverManager + this.lessonToRoomTimeslotSolverManager = lessonToRoomTimeslotSolverManager + } + + ... +} +---- +-- +==== + +**Multi-stage** planning can also be accomplished by using a separate solver configuration for each optimization stage. diff --git a/docs/src/modules/ROOT/pages/integration/spring-boot.adoc b/docs/src/modules/ROOT/pages/integration/spring-boot.adoc new file mode 100644 index 00000000000..4d70c75d45a --- /dev/null +++ b/docs/src/modules/ROOT/pages/integration/spring-boot.adoc @@ -0,0 +1,367 @@ +[#integrationWithSpringBoot] += Spring Boot integration +:doctype: book +:sectnums: +:icons: font + +To use Timefold Solver on Spring Boot, add the `timefold-solver-spring-boot-starter` dependency +and read the xref:quickstart/spring-boot/spring-boot-quickstart.adoc#springBootQuickStart[Spring Boot Java quick start]. + +[NOTE] +==== +Timefold Solver Spring Boot Starter only supports Spring Boot version 4.x. +==== + +[#integrationWithSpringBootProperties] +== Available configuration properties + +These properties are supported in Spring's `application.properties`: + +:property_prefix: +:solver_name_prefix: +include::_config-properties.adoc[] + +[#integrationWithSpringBootManagedResources] +== Injecting managed resources + +The Spring Boot integration allows the injection of several managed resources, including `SolverConfig`, `SolverFactory`, +`SolverManager`, `SolutionManager`, `ConstraintMetaModel` and `ConstraintVerifier`. + +The `SolverConfig` resource is constructed by reading the `application.properties` file and classpath. Therefore, Domain entities (solution, entities, and constraint classes) and customized properties (`spent-limit`, etc.) for the +planning problem are identified and loaded into the solver configuration. + +The available resources can be injected as follows: + +[tabs] +==== +Java:: ++ +-- +[source,java,options="nowrap"] +---- +@RestController +@RequestMapping("/path") +public class Resource { + + @Autowired + SolverConfig solverConfig; + + @Autowired + SolverFactory solverFactory; + + @Autowired + SolverManager solverManager; + + @Autowired + SolutionManager simpleSolutionManager; // <1> + + @Autowired + ConstraintMetaModel constraintMetaModel; + + @Autowired + ConstraintVerifier constraintVerifier; + + ... +} +---- +<1> You can find all the available score types in the xref:constraints-and-score/overview.adoc#scoreType[Constraints and Score] page. +-- +Kotlin:: ++ +-- +[source,kotlin,options="nowrap"] +---- +@RestController +@RequestMapping("/path") +class Resource { + + @Autowired + var solverConfig:SolverConfig? + + @Autowired + var solverFactory:SolverFactory? + + @Autowired + var solverManager:SolverManager? + + @Autowired + var simpleSolutionManager:SolutionManager? // <1> + + @Autowired + var constraintMetaModel:ConstraintMetaModel? = null + + @Autowired + var constraintVerifier:ConstraintVerifier? = null + + ... +} +---- +<1> You can find all the available score types in the xref:constraints-and-score/overview.adoc#scoreType[Constraints and Score] page. +-- +==== + +Timefold provides all the necessary resources for problem-solving and analysis. However, it is still possible to manually create and override the default managed resources. To create a custom `SolverManager`, use the `SolverFactory` resource. + +[tabs] +==== +Java:: ++ +-- +[source,java,options="nowrap"] +---- +package org.acme.schooltimetabling.rest; + +import ai.timefold.solver.core.api.solver.SolverFactory; +import ai.timefold.solver.core.api.solver.SolverManager; +import ai.timefold.solver.core.config.solver.SolverManagerConfig; +import ai.timefold.solver.spring.boot.autoconfigure.TimefoldAutoConfiguration; + +import org.acme.schooltimetabling.domain.Timetable; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; +import org.springframework.context.annotation.Primary; + +@Configuration +@Import(TimefoldAutoConfiguration.class) +public class BeanProducer { + + @Bean + @Primary + public SolverManager overrideSolverManager(SolverFactory solverFactory) { + SolverManagerConfig solverManagerConfig = new SolverManagerConfig(); + return SolverManager.create(solverFactory, solverManagerConfig); + } +} +---- +-- +Kotlin:: ++ +-- +[source,kotlin,options="nowrap"] +---- +package org.acme.schooltimetabling.rest + +import ai.timefold.solver.core.api.solver.SolverFactory +import ai.timefold.solver.core.api.solver.SolverManager +import ai.timefold.solver.core.config.solver.SolverManagerConfig +import ai.timefold.solver.spring.boot.autoconfigure.TimefoldAutoConfiguration +import org.acme.schooltimetabling.domain.Timetable +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration +import org.springframework.context.annotation.Import +import org.springframework.context.annotation.Primary + +@Configuration +@Import( + TimefoldAutoConfiguration::class +) +class BeanProducer { + @Bean + @Primary + fun overrideSolverManager(solverFactory: SolverFactory?): SolverManager { + val solverManagerConfig = SolverManagerConfig() + return SolverManager.create(solverFactory, solverManagerConfig) + } +} +---- +-- +==== + +[NOTE] +==== +Consider using xref:#integrationWithSpringBootMultipleResources[multiple solver configurations] instead of manually creating resources. +==== + +[#integrationWithSpringBootMultipleResources] +== Injecting multiple instances of `SolverManager` + +Spring Boot auto-configuration module allows for defining different solver settings or even wholly distinct planning problems in the same +application. Timefold identifies each setting and provides a specific managed resource, `SolverManager`. + +=== Solver configuration properties + +The configuration properties for multiple solvers are defined using the namespace `timefold.solver.`, +where `` is the name of the related solver. The `` property is only necessary when using multiple +solvers. For defining a single solver, refer to the xref:#integrationWithSpringBootProperties[Timefold configuration properties section]. +Following properties are supported: + +:property_prefix: +:solver_name_prefix: . +include::_config-properties.adoc[] + +=== Working with multiple Solvers + +The different solver settings are configured in the `application.properties` file. For example, two different time +settings for `spent-limit` are defined as follows: + +[source,properties,options="nowrap"] +---- +# The solver "fastSolver" runs only for 5 seconds, and "regularSolver" runs for 10s +timefold.solver.fastSolver.termination.spent-limit=5s // <1> +timefold.solver.regularSolver.termination.spent-limit=10s // <2> +---- +<1> Define a solver config named *fastSolver*. +<2> Define a solver config named *regularSolver*. + +To inject a specific `SolverManager`, use the `@Qualifier` annotation along with the solver configuration name, e.g., fastSolver. + +[tabs] +==== +Java:: ++ +-- +[source,java,options="nowrap"] +---- +@RestController +@RequestMapping("/path") +public class Resource { + + @Autowired + @Qualifier("fastSolver") + SolverManager<...> fastSolver; + + @Autowired + @Qualifier("regularSolver") + SolverManager<...> regularSolver; + + ... +} +---- +-- +Kotlin:: ++ +-- +[source,kotlin,options="nowrap"] +---- +@RestController +@RequestMapping("/path") +class Resource { + + @Autowired + @Qualifier("fastSolver") + var fastSolver: SolverManager<...>? + + @Autowired + @Qualifier("regularSolver") + var regularSolver: SolverManager<...>? + + ... +} +---- +-- +==== + +For a more advanced example, let's imagine two different steps for optimizing the school timetabling problem: + +1. Initially, a specific group of teachers is designated to teach the available lessons. +2. The next step involves assigning lessons to the rooms. + +To configure two different problems, add two XML files containing related planning configurations. + +[tabs] +==== +teachersSolverConfig.xml:: ++ +[source,xml,options="nowrap"] +---- + + org.acme.schooltimetabling.domain.TeacherToLessonSchedule + org.acme.schooltimetabling.domain.Teacher + +---- +roomsSolverConfig.xml:: ++ +[source,xml,options="nowrap"] +---- + + org.acme.schooltimetabling.domain.Timetable + org.acme.schooltimetabling.domain.Lesson + +---- +==== + +Set the solvers with the specific XML files in the `application.properties` file: + +[source,properties,options="nowrap"] +---- +timefold.solver.teacherSolver.solver-config-xml=teachersSolverConfig.xml // <1> +timefold.solver.roomSolver.solver-config-xml=roomsSolverConfig.xml // <2> +---- +<1> Define a solver config named *teacherSolver*. +<2> Define a solver config named *roomSolver*. + +Now let's inject both solvers. + +[tabs] +==== +Java:: ++ +-- +[source,java,options="nowrap"] +---- +@PlanningSolution +public class TeacherToLessonSchedule { + ... +} + +@PlanningEntity +public class Teacher { + ... +} + +@RestController +@RequestMapping("/path") +public class Resource { + + @Autowired + @Qualifier("teacherSolver") + SolverManager teacherToLessonScheduleSolverManager; + + @Autowired + @Qualifier("roomSolver") + SolverManager lessonToRoomTimeslotSolverManager; + + ... +} + + +---- +-- +Kotlin:: ++ +-- +[source,kotlin,options="nowrap"] +---- + +@PlanningSolution +class TeacherToLessonSchedule { + ... +} + +@PlanningEntity +class Teacher { + ... +} + +@RestController +@RequestMapping("/path") +class Resource { + + @Autowired + @Qualifier("teacherSolver") + var teacherToLessonScheduleSolverManager: SolverManager? + + @Autowired + @Qualifier("roomSolver") + var lessonToRoomTimeslotSolverManager: SolverManager? + + ... +} +---- +-- +==== + +**Multi-stage** planning can also be accomplished by using a separate solver configuration for each optimization stage. diff --git a/docs/src/modules/ROOT/pages/quickstart/quarkus/quarkus-quickstart.adoc b/docs/src/modules/ROOT/pages/quickstart/quarkus/quarkus-quickstart.adoc index 7f3f9df8dea..20685fe6f42 100644 --- a/docs/src/modules/ROOT/pages/quickstart/quarkus/quarkus-quickstart.adoc +++ b/docs/src/modules/ROOT/pages/quickstart/quarkus/quarkus-quickstart.adoc @@ -445,4 +445,4 @@ You have just developed a Quarkus application with https://timefold.ai[Timefold] Next Steps: - For a full implementation with a web UI and in-memory storage, check out {quarkus-quickstart-url}[the Quarkus quickstart source code]. -- Check out more information about Timefold's integration with Quarkus in our xref:../integration/integration.adoc#integrationWithQuarkus[Integration documentation]. +- Check out more information about Timefold's integration with Quarkus in our xref:../integration/quarkus.adoc#integrationWithQuarkus[Integration documentation]. diff --git a/docs/src/modules/ROOT/pages/quickstart/spring-boot/spring-boot-quickstart.adoc b/docs/src/modules/ROOT/pages/quickstart/spring-boot/spring-boot-quickstart.adoc index 0eb1c81a21b..1774559918c 100644 --- a/docs/src/modules/ROOT/pages/quickstart/spring-boot/spring-boot-quickstart.adoc +++ b/docs/src/modules/ROOT/pages/quickstart/spring-boot/spring-boot-quickstart.adoc @@ -493,4 +493,4 @@ You have just developed a Spring application with https://timefold.ai[Timefold]! Next Steps: - For a full implementation with a web UI, check out {spring-boot-quickstart-url}[the Spring-boot quickstart source code]. -- Check out more information about Timefold's integration with Spring in our xref:../integration/integration.adoc#integrationWithSpringBoot[Integration documentation]. +- Check out more information about Timefold's integration with Spring in our xref:../integration/spring-boot.adoc#integrationWithSpringBoot[Integration documentation]. diff --git a/docs/src/modules/ROOT/pages/running-timefold-solver/library-integration.adoc b/docs/src/modules/ROOT/pages/running-timefold-solver/library-integration.adoc index b00ee21b4a4..d3c183c4a86 100644 --- a/docs/src/modules/ROOT/pages/running-timefold-solver/library-integration.adoc +++ b/docs/src/modules/ROOT/pages/running-timefold-solver/library-integration.adoc @@ -56,7 +56,7 @@ This avoids timeout issues of HTTP and other technologies. Internally a `SolverManager` manages a thread pool of solver threads, which call `Solver.solve(...)`, and a thread pool of consumer threads, which handle best solution changed events. -In xref:integration/integration.adoc#integrationWithQuarkus[Quarkus] and xref:integration/integration.adoc#integrationWithSpringBoot[Spring Boot], +In xref:integration/quarkus.adoc#integrationWithQuarkus[Quarkus] and xref:integration/spring-boot.adoc#integrationWithSpringBoot[Spring Boot], the `SolverManager` instance is automatically injected in your code. Otherwise, build a `SolverManager` instance with the `create(...)` method: @@ -263,7 +263,7 @@ such as the termination configuration, you can use the `withConfigOverride(...)` [NOTE] ==== -The solver also permits the configuration of multiple solver managers with distinct settings in xref:integration/integration.adoc#integrationWithQuarkusMultipleResources[Quarkus] or xref:integration/integration.adoc#integrationWithSpringBootMultipleResources[Spring Boot]. +The solver also permits the configuration of multiple solver managers with distinct settings in xref:integration/quarkus.adoc#integrationWithQuarkusMultipleResources[Quarkus] or xref:integration/spring-boot.adoc#integrationWithSpringBootMultipleResources[Spring Boot]. ==== [#solverManagerSolveBatch] @@ -368,22 +368,44 @@ to limit the number of best solution events fired over any period of time. Open-source users may implement their own throttling mechanism within the `Consumer` itself. ==== -[#manualEnrichment] -== Manual enrichment +[#integrationWithJPMS] +== Java Platform Module System (JPMS) -The service module's xref:service/modeling-changes.adoc#solverModelEnrichment[`SolverModelEnricher`] is not available in library mode. -Any pre-processing or enrichment of the planning problem — such as fetching external data, -computing derived fields, or pinning entities from a previous solution — must be performed -before calling `Solver.solve()`: +Starting in version 2.0, Timefold Solver has official support for the https://openjdk.org/projects/jigsaw/spec/[Java Platform Module System (JPMS)]. + +When using Timefold Solver from code on the modulepath, +_export_ the packages that contain your domain objects, constraints and solver configuration, and _open_ them to the Timefold Solver module(s) +in your `module-info.java` file: -[tabs] -==== -Java:: -+ [source,java,options="nowrap"] ---- -Timetable problem = loadFromDatabase(); -enrichWithExternalData(problem); // your own enrichment logic -Timetable solution = solver.solve(problem); +module org.acme.vehiclerouting { + requires ai.timefold.solver.core; + ... + + exports org.acme.vehiclerouting.domain; // Domain classes + exports org.acme.vehiclerouting.solver; // Constraints + + opens org.acme.vehiclerouting.domain to ai.timefold.solver.core; // Domain classes + opens org.acme.vehiclerouting.solver to ai.timefold.solver.core; // Constraints + ... +} +---- + +If this is not set up correctly, you will get errors. Usually, these mention the `unnamed module` and give detailed information of what needs to be changed. + +[source,options="nowrap"] +---- +class org.acme.schooltimetabling.domain.Timetable$Timefold$MemberAccessor$Field$lessons (in unnamed module @0x273444fe) +cannot access class org.acme.schooltimetabling.domain.Timetable (in module hello.world.school.timetabling) +because module hello.world.school.timetabling does not export org.acme.schooltimetabling.domain to unnamed module @0x273444fe ---- + +[WARNING] ==== +Only JPMS exported packages are part of the supported public API. +==== +If you access non-exported classes (for example by running on the classpath), and we later change or remove them, we will not treat that as a breaking change. +Those classes are not covered by compatibility guarantees. + +You can opt-out of JPMS by running all JAR files on the classpath instead of using the module path (for example, by not configuring a module path in your build or runtime setup). diff --git a/docs/src/modules/ROOT/pages/upgrading-timefold-solver/framework-version-warning.adoc b/docs/src/modules/ROOT/pages/upgrading-timefold-solver/framework-version-warning.adoc index 78a7a9763b8..eb0ce7a389e 100644 --- a/docs/src/modules/ROOT/pages/upgrading-timefold-solver/framework-version-warning.adoc +++ b/docs/src/modules/ROOT/pages/upgrading-timefold-solver/framework-version-warning.adoc @@ -1,5 +1,5 @@ [NOTE] ==== Our automatic migrations will not change the version of your frameworks. -If you run into compatibility issues, please consult the integration guides for xref:../integration/integration.adoc#integrationWithSpringBoot[Spring] or xref:../integration/integration.adoc#integrationWithQuarkus[Quarkus]. +If you run into compatibility issues, please consult the integration guides for xref:../integration/spring-boot.adoc#integrationWithSpringBoot[Spring] or xref:../integration/quarkus.adoc#integrationWithQuarkus[Quarkus]. ==== \ No newline at end of file From a142560c594be4928bd9d8b6ccc2011b1d3aed60 Mon Sep 17 00:00:00 2001 From: Tom Cools Date: Fri, 26 Jun 2026 21:03:31 +0200 Subject: [PATCH 13/17] docs: restore doc references --- .../modules/ROOT/pages/constraints-and-score/overview.adoc | 4 ++-- .../ROOT/pages/constraints-and-score/performance.adoc | 4 ++-- .../modules/ROOT/pages/design-patterns/design-patterns.adoc | 2 +- .../modules/ROOT/pages/integration/_config-properties.adoc | 2 +- .../ROOT/pages/optimization-algorithms/local-search.adoc | 6 +++--- .../ROOT/pages/optimization-algorithms/overview.adoc | 6 +++--- .../quickstart/hello-world/hello-world-quickstart.adoc | 2 +- .../quarkus-vehicle-routing/vehicle-routing-model.adoc | 2 +- .../school-timetabling/_school-timetabling-model.adoc | 2 +- .../pages/responding-to-change/responding-to-change.adoc | 2 +- .../running-timefold-solver/benchmarking-and-tweaking.adoc | 6 +++--- .../running-timefold-solver/modeling-planning-problems.adoc | 2 +- 12 files changed, 20 insertions(+), 20 deletions(-) diff --git a/docs/src/modules/ROOT/pages/constraints-and-score/overview.adoc b/docs/src/modules/ROOT/pages/constraints-and-score/overview.adoc index 03cacea86af..a33c782461b 100644 --- a/docs/src/modules/ROOT/pages/constraints-and-score/overview.adoc +++ b/docs/src/modules/ROOT/pages/constraints-and-score/overview.adoc @@ -434,7 +434,7 @@ The score calculation must be deterministic and free of side-effects. It must not change the planning entities or the problem facts in any way. For example, it must not call a setter method on a planning entity in the score calculation. -Timefold Solver does not recalculate the score of a solution if it can predict it (unless an xref:running-timefold-solver/multithreaded-solving.adoc#environmentMode[environmentMode assertion] is enabled). +Timefold Solver does not recalculate the score of a solution if it can predict it (unless an xref:running-timefold-solver/solver-diagnostics.adoc#environmentMode[environmentMode assertion] is enabled). For example, after a winning step is done, there is no need to calculate the score because that move was done and undone earlier. As a result, there is no guarantee that changes applied during score calculation actually happen. @@ -481,7 +481,7 @@ Alternatively, you can also specify the trend for each score level separately: [#invalidScoreDetection] === Invalid score detection -When you put the xref:running-timefold-solver/multithreaded-solving.adoc#environmentMode[`environmentMode`] in `FULL_ASSERT` (or ``STEP_ASSERT``), +When you put the xref:running-timefold-solver/solver-diagnostics.adoc#environmentMode[`environmentMode`] in `FULL_ASSERT` (or ``STEP_ASSERT``), it will detect score corruption in the xref:constraints-and-score/performance.adoc#incrementalScoreCalculationPerformance[incremental score calculation]. However, that will not verify that your score calculator actually implements your score constraints as your business desires. For example, one constraint might consistently match the wrong pattern. diff --git a/docs/src/modules/ROOT/pages/constraints-and-score/performance.adoc b/docs/src/modules/ROOT/pages/constraints-and-score/performance.adoc index 5d4b91c2962..9640304d8dd 100644 --- a/docs/src/modules/ROOT/pages/constraints-and-score/performance.adoc +++ b/docs/src/modules/ROOT/pages/constraints-and-score/performance.adoc @@ -372,7 +372,7 @@ quarkus.timefold.solver.constraint-stream-profiling-enabled=true ==== Constraint profiling will log its results to the `ai.timefold.solver.enterprise.core.api.ConstraintProfiler` class at the end of solving. -For the report to be generated, xref:running-timefold-solver/multithreaded-solving.adoc#logging[the particular logging levels must be configured and enabled]. +For the report to be generated, xref:running-timefold-solver/solver-diagnostics.adoc#logging[the particular logging levels must be configured and enabled]. IMPORTANT: Constraint profiling is only supported for xref:constraints-and-score/score-calculation.adoc#constraintStreams[constraint stream score calculation] when xref:running-timefold-solver/multithreaded-solving.adoc#multithreadedSolving[multi-threaded solving] is disabled. @@ -498,7 +498,7 @@ NOTE: Traditional profiling tools such as Java Mission Control will report the i [#fullAssert] == Validate the implementation using FULL_ASSERT -When you are done optimizing your score calculation, make sure to validate it using xref:running-timefold-solver/multithreaded-solving.adoc#environmentModeFullAssert[`FULL_ASSERT`] mode. Make sure not to run with this mode in production. +When you are done optimizing your score calculation, make sure to validate it using xref:running-timefold-solver/solver-diagnostics.adoc#environmentModeFullAssert[`FULL_ASSERT`] mode. Make sure not to run with this mode in production. [#otherScoreCalculationPerformanceTricks] == Other score calculation performance tricks diff --git a/docs/src/modules/ROOT/pages/design-patterns/design-patterns.adoc b/docs/src/modules/ROOT/pages/design-patterns/design-patterns.adoc index 3cbefdf3e25..18b6f924fe2 100644 --- a/docs/src/modules/ROOT/pages/design-patterns/design-patterns.adoc +++ b/docs/src/modules/ROOT/pages/design-patterns/design-patterns.adoc @@ -8,4 +8,4 @@ This page has been split into three separate guides: * xref:design-patterns/domain-modeling.adoc[Domain modeling guide] * xref:design-patterns/time-patterns.adoc[Time patterns] -* xref:design-patterns/cloud-architecture.adoc[Cloud architecture patterns] +* xref:deployment/cloud-architecture.adoc[Cloud architecture patterns] diff --git a/docs/src/modules/ROOT/pages/integration/_config-properties.adoc b/docs/src/modules/ROOT/pages/integration/_config-properties.adoc index f495985e2cf..ce19beb00ce 100644 --- a/docs/src/modules/ROOT/pages/integration/_config-properties.adoc +++ b/docs/src/modules/ROOT/pages/integration/_config-properties.adoc @@ -25,7 +25,7 @@ located in the classpath. The random seed to be used in the solving process. {property_prefix}timefold.solver.{solver_name_prefix}environment-mode:: -Enable xref:running-timefold-solver/multithreaded-solving.adoc#environmentMode[runtime assertions] to detect common bugs in your +Enable xref:running-timefold-solver/solver-diagnostics.adoc#environmentMode[runtime assertions] to detect common bugs in your implementation during development. {property_prefix}timefold.solver.{solver_name_prefix}constraint-stream-profiling-enabled:: diff --git a/docs/src/modules/ROOT/pages/optimization-algorithms/local-search.adoc b/docs/src/modules/ROOT/pages/optimization-algorithms/local-search.adoc index 50e0f4e8ec4..d627e889e2c 100644 --- a/docs/src/modules/ROOT/pages/optimization-algorithms/local-search.adoc +++ b/docs/src/modules/ROOT/pages/optimization-algorithms/local-search.adoc @@ -95,7 +95,7 @@ the `Acceptor` accepted all of them and the `Forager` picked the move __B0 to B3 image::optimization-algorithms/local-search/decideNextStepNQueens04.png[align="center"] -xref:running-timefold-solver/multithreaded-solving.adoc#logging[Turn on `trace` logging] to show the decision making in the log. +xref:running-timefold-solver/solver-diagnostics.adoc#logging[Turn on `trace` logging] to show the decision making in the log. Because the last solution can degrade (such as in Tabu Search), the `Solver` remembers the best solution it has encountered through the entire search path. @@ -133,7 +133,7 @@ It is possible to disable breaking ties randomly by explicitly setting `breakTie the score calculator should add an extra softer xref:constraints-and-score/overview.adoc#scoreLevel[score level] to score the first move as slightly better. Don't rely on move selection order to enforce that. -* Random tie breaking does not affect xref:running-timefold-solver/multithreaded-solving.adoc#environmentMode[reproducibility]. +* Random tie breaking does not affect xref:running-timefold-solver/solver-diagnostics.adoc#environmentMode[reproducibility]. ==== @@ -154,7 +154,7 @@ To evaluate only a random subset of all the moves, use: Unlike the N-queens problem, real world problems require the use of ``acceptedCountLimit``. Start from an `acceptedCountLimit` that takes a step in less than two seconds. -xref:running-timefold-solver/multithreaded-solving.adoc#logging[Turn on INFO logging] to see the step times. +xref:running-timefold-solver/solver-diagnostics.adoc#logging[Turn on INFO logging] to see the step times. Use the xref:running-timefold-solver/benchmarking-and-tweaking.adoc#benchmarker[Benchmarker] to tweak the value. [IMPORTANT] diff --git a/docs/src/modules/ROOT/pages/optimization-algorithms/overview.adoc b/docs/src/modules/ROOT/pages/optimization-algorithms/overview.adoc index f0c1bb16af7..057f63478f7 100644 --- a/docs/src/modules/ROOT/pages/optimization-algorithms/overview.adoc +++ b/docs/src/modules/ROOT/pages/optimization-algorithms/overview.adoc @@ -273,7 +273,7 @@ These form four nested scopes: image::optimization-algorithms/overview/scopeOverview.png[align="center"] -Configure xref:running-timefold-solver/multithreaded-solving.adoc#logging[logging] to display the log messages of each scope. +Configure xref:running-timefold-solver/solver-diagnostics.adoc#logging[logging] to display the log messages of each scope. [#termination] @@ -716,7 +716,7 @@ This is useful for benchmarking. ---- -Switching xref:running-timefold-solver/multithreaded-solving.adoc#environmentMode[EnvironmentMode] can heavily impact when this termination ends. +Switching xref:running-timefold-solver/solver-diagnostics.adoc#environmentMode[EnvironmentMode] can heavily impact when this termination ends. [#moveCountTermination] ===== `MoveCountTermination` @@ -731,7 +731,7 @@ This is useful for benchmarking. ---- -Switching xref:running-timefold-solver/multithreaded-solving.adoc#environmentMode[EnvironmentMode] can heavily impact when this termination ends. +Switching xref:running-timefold-solver/solver-diagnostics.adoc#environmentMode[EnvironmentMode] can heavily impact when this termination ends. [#diminishedReturnsTermination] ===== `DiminishedReturnsTermination` diff --git a/docs/src/modules/ROOT/pages/quickstart/hello-world/hello-world-quickstart.adoc b/docs/src/modules/ROOT/pages/quickstart/hello-world/hello-world-quickstart.adoc index 74159394664..44d5d92a838 100644 --- a/docs/src/modules/ROOT/pages/quickstart/hello-world/hello-world-quickstart.adoc +++ b/docs/src/modules/ROOT/pages/quickstart/hello-world/hello-world-quickstart.adoc @@ -96,7 +96,7 @@ include::../shared/_java-prerequisites.adoc[] Create a Maven or Gradle build file and add these dependencies: * `timefold-solver-core` (compile scope) to solve the school timetable problem. -* A xref:running-timefold-solver/multithreaded-solving.adoc#logging[logging] implementation, such as `logback-classic` (runtime scope), to see what Timefold Solver is doing. +* A xref:running-timefold-solver/solver-diagnostics.adoc#logging[logging] implementation, such as `logback-classic` (runtime scope), to see what Timefold Solver is doing. [tabs] ==== diff --git a/docs/src/modules/ROOT/pages/quickstart/quarkus-vehicle-routing/vehicle-routing-model.adoc b/docs/src/modules/ROOT/pages/quickstart/quarkus-vehicle-routing/vehicle-routing-model.adoc index 38f11d5dd31..1159ae07eb4 100644 --- a/docs/src/modules/ROOT/pages/quickstart/quarkus-vehicle-routing/vehicle-routing-model.adoc +++ b/docs/src/modules/ROOT/pages/quickstart/quarkus-vehicle-routing/vehicle-routing-model.adoc @@ -265,7 +265,7 @@ so it is easier to read Timefold Solver's `DEBUG` or `TRACE` log, as shown later ==== Determining the `@PlanningListVariable` fields for an arbitrary constraint solving use case is often challenging the first time. -Read xref:design-patterns/design-patterns.adoc#domainModelingGuide[the domain modeling guidelines] to avoid common pitfalls. +Read xref:design-patterns/domain-modeling.adoc#domainModelingGuide[the domain modeling guidelines] to avoid common pitfalls. ==== == Visit diff --git a/docs/src/modules/ROOT/pages/quickstart/shared/school-timetabling/_school-timetabling-model.adoc b/docs/src/modules/ROOT/pages/quickstart/shared/school-timetabling/_school-timetabling-model.adoc index 2bb4ba9cc18..aa281799605 100644 --- a/docs/src/modules/ROOT/pages/quickstart/shared/school-timetabling/_school-timetabling-model.adoc +++ b/docs/src/modules/ROOT/pages/quickstart/shared/school-timetabling/_school-timetabling-model.adoc @@ -305,5 +305,5 @@ The `room` field also has an `@PlanningVariable` annotation, for the same reason ==== Determining the `@PlanningVariable` fields for an arbitrary constraint solving use case is often challenging the first time. -Read xref:design-patterns/design-patterns.adoc#domainModelingGuide[the domain modeling guidelines] to avoid common pitfalls. +Read xref:design-patterns/domain-modeling.adoc#domainModelingGuide[the domain modeling guidelines] to avoid common pitfalls. ==== diff --git a/docs/src/modules/ROOT/pages/responding-to-change/responding-to-change.adoc b/docs/src/modules/ROOT/pages/responding-to-change/responding-to-change.adoc index 5d53a917d24..5d28e9b61da 100644 --- a/docs/src/modules/ROOT/pages/responding-to-change/responding-to-change.adoc +++ b/docs/src/modules/ROOT/pages/responding-to-change/responding-to-change.adoc @@ -202,7 +202,7 @@ However, it is stored in the database and used as a starting point for the next Planning entities that are not in the current planning window. + ** If the planning window is too small to plan all entities, you're dealing with <>. -** If xref:design-patterns/design-patterns.adoc#assigningTimeToPlanningEntities[time is a planning variable], the size of the planning window is determined dynamically, +** If xref:design-patterns/time-patterns.adoc#assigningTimeToPlanningEntities[time is a planning variable], the size of the planning window is determined dynamically, in which case the _unplanned_ stage is not applicable. image::responding-to-change/continuousPublishingWithRotation.png[align="center"] diff --git a/docs/src/modules/ROOT/pages/running-timefold-solver/benchmarking-and-tweaking.adoc b/docs/src/modules/ROOT/pages/running-timefold-solver/benchmarking-and-tweaking.adoc index d91872e7109..da8beabc040 100644 --- a/docs/src/modules/ROOT/pages/running-timefold-solver/benchmarking-and-tweaking.adoc +++ b/docs/src/modules/ROOT/pages/running-timefold-solver/benchmarking-and-tweaking.adoc @@ -419,7 +419,7 @@ To write those solutions in the ``benchmarkDirectory``, enable ``writeOutputSolu [#benchmarkLogging] === Benchmark logging -Benchmark logging is configured like xref:running-timefold-solver/multithreaded-solving.adoc#logging[solver logging]. +Benchmark logging is configured like xref:running-timefold-solver/solver-diagnostics.adoc#logging[solver logging]. To separate the log messages of each single benchmark run into a separate file, use the http://logback.qos.ch/manual/mdc.html[MDC] with key `subSingleBenchmark.name` in a sifting appender. For example with Logback in ``logback.xml``: @@ -1104,8 +1104,8 @@ The `subSingleCount` defaults to `1` (so no statistical benchmarking). [NOTE] ==== If `subSingleCount` is higher than ``1``, -the benchmarker will automatically use a _different_ xref:running-timefold-solver/multithreaded-solving.adoc#randomNumberGenerator[`Random` seed] for every sub single run, -without losing reproducibility (for each sub single index) in xref:running-timefold-solver/multithreaded-solving.adoc#environmentMode[EnvironmentMode] ``NO_ASSERT`` and lower. +the benchmarker will automatically use a _different_ xref:running-timefold-solver/solver-diagnostics.adoc#randomNumberGenerator[`Random` seed] for every sub single run, +without losing reproducibility (for each sub single index) in xref:running-timefold-solver/solver-diagnostics.adoc#environmentMode[EnvironmentMode] ``NO_ASSERT`` and lower. ==== diff --git a/docs/src/modules/ROOT/pages/running-timefold-solver/modeling-planning-problems.adoc b/docs/src/modules/ROOT/pages/running-timefold-solver/modeling-planning-problems.adoc index 142f8a38ca5..09db6293a1a 100644 --- a/docs/src/modules/ROOT/pages/running-timefold-solver/modeling-planning-problems.adoc +++ b/docs/src/modules/ROOT/pages/running-timefold-solver/modeling-planning-problems.adoc @@ -110,7 +110,7 @@ There are 2 different types of planning variables we can distinguish: *genuine* *Genuine* planning variables are variables to which the solver will assign *planning values*. There are 3 variants of genuine planning variables. The variants you use depends on the problem you are solving. -For more detailed guidance on when to use which variant, read the xref:design-patterns/design-patterns.adoc#domainModelingGuide[domain modeling guide]. +For more detailed guidance on when to use which variant, read the xref:design-patterns/domain-modeling.adoc#domainModelingGuide[domain modeling guide]. - xref:planningVariable[@PlanningVariable]: contains a single *planning value*. *Planning values* can be assigned to multiple *planning entities*. - xref:planningListVariable[@PlanningListVariable]: contains multiple *planning values* in a specific order. *Planning values* can only be assigned to a single *planning entity*. From ce393f1a9f415a15b64121adae4c115c8c54d1af Mon Sep 17 00:00:00 2001 From: Tom Cools Date: Fri, 26 Jun 2026 21:32:46 +0200 Subject: [PATCH 14/17] docs: model enrichment --- docs/src/modules/ROOT/nav.adoc | 13 ++-- .../quickstart/service/getting-started.adoc | 2 +- .../ROOT/pages/service/modeling-changes.adoc | 70 +------------------ .../modules/ROOT/pages/service/rest-api.adoc | 2 +- 4 files changed, 11 insertions(+), 76 deletions(-) diff --git a/docs/src/modules/ROOT/nav.adoc b/docs/src/modules/ROOT/nav.adoc index eeee1208a7f..52127151296 100644 --- a/docs/src/modules/ROOT/nav.adoc +++ b/docs/src/modules/ROOT/nav.adoc @@ -2,7 +2,7 @@ * xref:planning-ai-concepts.adoc[leveloffset=+1] * Getting started ** xref:quickstart/overview.adoc[leveloffset=+1] -** xref:quickstart/service/getting-started.adoc[Service Quickstart (Preview)] +** xref:quickstart/service/getting-started.adoc[Building as a service] ** Embed as a library *** xref:quickstart/hello-world/hello-world-quickstart.adoc[Hello World Guide] *** xref:quickstart/quarkus/quarkus-quickstart.adoc[Quarkus Guide] @@ -41,13 +41,14 @@ ** xref:deployment/cloud-architecture.adoc[leveloffset=+1] ** xref:deployment/infrastructure-requirements.adoc[leveloffset=+1] -* Tuning the Solver +* Diagnosing the Solver ** xref:running-timefold-solver/benchmarking-and-tweaking.adoc[leveloffset=+1] ** xref:running-timefold-solver/solver-diagnostics.adoc[leveloffset=+1] -** xref:optimization-algorithms/overview.adoc[Optimization algorithms] -*** xref:optimization-algorithms/construction-heuristics.adoc[leveloffset=+1] -*** xref:optimization-algorithms/local-search.adoc[leveloffset=+1] -*** xref:optimization-algorithms/exhaustive-search.adoc[leveloffset=+1] + +* xref:optimization-algorithms/overview.adoc[Optimization algorithms] +** xref:optimization-algorithms/construction-heuristics.adoc[leveloffset=+1] +** xref:optimization-algorithms/local-search.adoc[leveloffset=+1] +** xref:optimization-algorithms/exhaustive-search.adoc[leveloffset=+1] ** Custom moves *** xref:optimization-algorithms/neighborhoods.adoc[Neighborhoods API] *** xref:optimization-algorithms/move-selector-reference.adoc[leveloffset=+1] diff --git a/docs/src/modules/ROOT/pages/quickstart/service/getting-started.adoc b/docs/src/modules/ROOT/pages/quickstart/service/getting-started.adoc index 8cada57c72f..a3bbf2a1bf7 100644 --- a/docs/src/modules/ROOT/pages/quickstart/service/getting-started.adoc +++ b/docs/src/modules/ROOT/pages/quickstart/service/getting-started.adoc @@ -459,5 +459,5 @@ Important elements to observe in the JSON above: Since this is a "Getting Started" guide, not everything is covered yet. * Learn about improvements you can make to your model: -** How to set up the underlying xref:service/modeling-changes.adoc[Timefold model and constraints]. +** How to enrich your model with xref:service/modeling-changes.adoc[Model Enrichment]. ** How to configure your xref:service/rest-api.adoc[REST API] with validations, custom endpoints, etc. \ No newline at end of file diff --git a/docs/src/modules/ROOT/pages/service/modeling-changes.adoc b/docs/src/modules/ROOT/pages/service/modeling-changes.adoc index 82e7c9421f5..671e4861ada 100644 --- a/docs/src/modules/ROOT/pages/service/modeling-changes.adoc +++ b/docs/src/modules/ROOT/pages/service/modeling-changes.adoc @@ -1,77 +1,11 @@ -= Service Model and Enricher -:description: The core of any model is the solver += Model Enrichment +:description: Enrich your SolverModel with additional information before solving starts. :doctype: book :sectnums: :icons: font -Building Timefold Solver as a service introduces a few concepts which are not relevant when using it as a library when it comes to modeling your problem domain. - include::_preview-note.adoc[] -[#solverModel] -== SolverModel interface - -Your class which is annotated by `@PlanningSolution` should implement the `SolverModel` interface. - -.Example for School Timetabling -[tabs] -==== -Java:: -+ --- -[source,java,options="nowrap"] ----- -@PlanningSolution -public class Timetable implements SolverModel { - - @ProblemFactCollectionProperty - @ValueRangeProvider - private List timeslots; - - @PlanningEntityCollectionProperty - private List lessons; - - @PlanningScore - private HardSoftScore score; - - @Override - public HardSoftScore getScore() { - return score; - } - - // other Getters/Setters/Constructors excluded -} ----- --- - -Kotlin:: -+ --- -[source,kotlin,options="nowrap"] ----- -@PlanningSolution -class Timetable : SolverModel { - - @ProblemFactCollectionProperty - @ValueRangeProvider - val timeslots: List = emptyList() - - @PlanningEntityCollectionProperty - val lessons: List = emptyList() - - private var _score: HardSoftScore? = null - - @PlanningScore - override fun getScore(): HardSoftScore? = _score - - // other Getters/Setters/Constructors excluded -} ----- --- -==== - -In case you don't want control over the `score` class used, you can also extend the `AbstractSimpleModel` as used in the xref:../quickstart/service/getting-started.adoc[getting started guide]. - [#solverModelEnrichment] == SolverModel enrichment diff --git a/docs/src/modules/ROOT/pages/service/rest-api.adoc b/docs/src/modules/ROOT/pages/service/rest-api.adoc index 9bece72bb2d..3917cf2634f 100644 --- a/docs/src/modules/ROOT/pages/service/rest-api.adoc +++ b/docs/src/modules/ROOT/pages/service/rest-api.adoc @@ -286,7 +286,7 @@ class TimetableConvertor : ModelConvertor SolverModel -> ModelOutput mapping. -For enhancing the SolverModel, use the xref:./modeling-changes.adoc#solverModelEnrichment[SolverModel Enrichment] mechanism instead. +For enhancing the SolverModel, use the xref:./modeling-changes.adoc#solverModelEnrichment[Model Enrichment] mechanism instead. ==== [#validatingRestInput] From 6380d212a212887ca1ee9c0bc22781b6bde15d7d Mon Sep 17 00:00:00 2001 From: Tom Cools Date: Fri, 26 Jun 2026 21:43:00 +0200 Subject: [PATCH 15/17] docs: further nav structure fixes --- docs/src/modules/ROOT/nav.adoc | 5 ++--- .../modules/ROOT/pages/constraints-and-score/overview.adoc | 1 - docs/src/modules/ROOT/pages/frequently-asked-questions.adoc | 1 - .../modules/ROOT/pages/optimization-algorithms/overview.adoc | 3 --- docs/src/modules/ROOT/pages/quickstart/overview.adoc | 3 +-- .../pages/responding-to-change/responding-to-change.adoc | 3 +-- .../running-timefold-solver/benchmarking-and-tweaking.adoc | 2 -- .../ROOT/pages/running-timefold-solver/configuration.adoc | 3 --- .../running-timefold-solver/modeling-planning-problems.adoc | 2 -- 9 files changed, 4 insertions(+), 19 deletions(-) diff --git a/docs/src/modules/ROOT/nav.adoc b/docs/src/modules/ROOT/nav.adoc index 52127151296..4918cab263c 100644 --- a/docs/src/modules/ROOT/nav.adoc +++ b/docs/src/modules/ROOT/nav.adoc @@ -1,8 +1,7 @@ * xref:introduction.adoc[leveloffset=+1] * xref:planning-ai-concepts.adoc[leveloffset=+1] -* Getting started -** xref:quickstart/overview.adoc[leveloffset=+1] -** xref:quickstart/service/getting-started.adoc[Building as a service] +* xref:quickstart/overview.adoc[Getting started] +** xref:quickstart/service/getting-started.adoc[Build as a service] ** Embed as a library *** xref:quickstart/hello-world/hello-world-quickstart.adoc[Hello World Guide] *** xref:quickstart/quarkus/quarkus-quickstart.adoc[Quarkus Guide] diff --git a/docs/src/modules/ROOT/pages/constraints-and-score/overview.adoc b/docs/src/modules/ROOT/pages/constraints-and-score/overview.adoc index a33c782461b..f2f30a4d4ec 100644 --- a/docs/src/modules/ROOT/pages/constraints-and-score/overview.adoc +++ b/docs/src/modules/ROOT/pages/constraints-and-score/overview.adoc @@ -5,7 +5,6 @@ :doctype: book :sectnums: :icons: font -:relevance: library-and-service [#scoreTerminology] == Score terminology diff --git a/docs/src/modules/ROOT/pages/frequently-asked-questions.adoc b/docs/src/modules/ROOT/pages/frequently-asked-questions.adoc index e552b04dcd6..34f20a72e14 100644 --- a/docs/src/modules/ROOT/pages/frequently-asked-questions.adoc +++ b/docs/src/modules/ROOT/pages/frequently-asked-questions.adoc @@ -1,7 +1,6 @@ = FAQ :doctype: book :icons: font -:relevance: library-and-service == How is Timefold Solver Licensed? diff --git a/docs/src/modules/ROOT/pages/optimization-algorithms/overview.adoc b/docs/src/modules/ROOT/pages/optimization-algorithms/overview.adoc index 057f63478f7..d91dac272e4 100644 --- a/docs/src/modules/ROOT/pages/optimization-algorithms/overview.adoc +++ b/docs/src/modules/ROOT/pages/optimization-algorithms/overview.adoc @@ -7,9 +7,6 @@ :doctype: book :sectnums: :icons: font -:relevance: library-and-service -:notes: Relevant, but some of this can move to a "shared" section instead. - == The basics diff --git a/docs/src/modules/ROOT/pages/quickstart/overview.adoc b/docs/src/modules/ROOT/pages/quickstart/overview.adoc index 056c9ccf184..0ef137b60bd 100644 --- a/docs/src/modules/ROOT/pages/quickstart/overview.adoc +++ b/docs/src/modules/ROOT/pages/quickstart/overview.adoc @@ -1,11 +1,10 @@ [#quickStartOverview] -= Overview += Getting started :page-aliases: quickstart/quickstart.adoc, \ development/development.adoc, \ use-cases-and-examples/use-cases-and-examples.adoc, \ overview-quickstarts.adoc :imagesdir: ../.. -:relevance: library-and-service :icons: font [TIP] diff --git a/docs/src/modules/ROOT/pages/responding-to-change/responding-to-change.adoc b/docs/src/modules/ROOT/pages/responding-to-change/responding-to-change.adoc index 5d28e9b61da..f424e300016 100644 --- a/docs/src/modules/ROOT/pages/responding-to-change/responding-to-change.adoc +++ b/docs/src/modules/ROOT/pages/responding-to-change/responding-to-change.adoc @@ -4,8 +4,7 @@ :doctype: book :sectnums: :icons: font -:relevance: library-and-service -:notes: Wildly different in service module, so potentially split. +:todo: https://github.com/TimefoldAI/timefold-solver/issues/2257 The problem facts used to create a solution may change before or during the execution of that solution. diff --git a/docs/src/modules/ROOT/pages/running-timefold-solver/benchmarking-and-tweaking.adoc b/docs/src/modules/ROOT/pages/running-timefold-solver/benchmarking-and-tweaking.adoc index da8beabc040..675cb767b35 100644 --- a/docs/src/modules/ROOT/pages/running-timefold-solver/benchmarking-and-tweaking.adoc +++ b/docs/src/modules/ROOT/pages/running-timefold-solver/benchmarking-and-tweaking.adoc @@ -4,8 +4,6 @@ :doctype: book :sectnums: :icons: font -:relevance: library-and-service -:notes: Mostly irrelevant in service module. [#findTheBestSolverConfiguration] diff --git a/docs/src/modules/ROOT/pages/running-timefold-solver/configuration.adoc b/docs/src/modules/ROOT/pages/running-timefold-solver/configuration.adoc index 762c28db2a9..1723509a3da 100644 --- a/docs/src/modules/ROOT/pages/running-timefold-solver/configuration.adoc +++ b/docs/src/modules/ROOT/pages/running-timefold-solver/configuration.adoc @@ -3,9 +3,6 @@ :doctype: book :sectnums: :icons: font -:relevance: core-only -:notes: Mostly irrelevant in service module. Also, "configuring" is too broad - [#solverConfigurationByXML] == Solver configuration by XML diff --git a/docs/src/modules/ROOT/pages/running-timefold-solver/modeling-planning-problems.adoc b/docs/src/modules/ROOT/pages/running-timefold-solver/modeling-planning-problems.adoc index 09db6293a1a..23cf23aaa59 100644 --- a/docs/src/modules/ROOT/pages/running-timefold-solver/modeling-planning-problems.adoc +++ b/docs/src/modules/ROOT/pages/running-timefold-solver/modeling-planning-problems.adoc @@ -4,8 +4,6 @@ :doctype: book :sectnums: :icons: font -:relevance: library-and-service -:notes: Lots of shared things here, but might need to split up some things. [TIP] ==== From 218e7e77231a38d65860c7215ea5e06fabb46cfd Mon Sep 17 00:00:00 2001 From: Tom Cools Date: Fri, 26 Jun 2026 22:15:04 +0200 Subject: [PATCH 16/17] docs: further nav structure fixes --- docs/src/modules/ROOT/nav.adoc | 21 ++++++++++++------- .../quickstart/service/getting-started.adoc | 2 +- .../pages/service/constraint-weights.adoc | 12 ----------- .../modules/ROOT/pages/service/demo-data.adoc | 2 +- .../ROOT/pages/service/exposing-metrics.adoc | 2 +- .../modules/ROOT/pages/service/overview.adoc | 6 ++---- .../modules/ROOT/pages/service/rest-api.adoc | 6 ++---- 7 files changed, 20 insertions(+), 31 deletions(-) delete mode 100644 docs/src/modules/ROOT/pages/service/constraint-weights.adoc diff --git a/docs/src/modules/ROOT/nav.adoc b/docs/src/modules/ROOT/nav.adoc index 4918cab263c..3b6008d99f9 100644 --- a/docs/src/modules/ROOT/nav.adoc +++ b/docs/src/modules/ROOT/nav.adoc @@ -6,11 +6,8 @@ *** xref:quickstart/hello-world/hello-world-quickstart.adoc[Hello World Guide] *** xref:quickstart/quarkus/quarkus-quickstart.adoc[Quarkus Guide] *** xref:quickstart/spring-boot/spring-boot-quickstart.adoc[Spring Boot Guide] -* Example use cases -** xref:quickstart/quarkus-vehicle-routing/quarkus-vehicle-routing-quickstart.adoc[Vehicle Routing (Guide)] -** https://github.com/TimefoldAI/timefold-quickstarts[More examples on GitHub^] -* Building your model +* Domain modeling ** xref:running-timefold-solver/modeling-planning-problems.adoc[leveloffset=+1] ** xref:design-patterns/domain-modeling.adoc[leveloffset=+1] ** xref:design-patterns/time-patterns.adoc[leveloffset=+1] @@ -36,14 +33,14 @@ *** xref:integration/spring-boot.adoc[leveloffset=+1] *** xref:integration/persistent-storage.adoc[leveloffset=+1] -* Deployment -** xref:deployment/cloud-architecture.adoc[leveloffset=+1] -** xref:deployment/infrastructure-requirements.adoc[leveloffset=+1] - * Diagnosing the Solver ** xref:running-timefold-solver/benchmarking-and-tweaking.adoc[leveloffset=+1] ** xref:running-timefold-solver/solver-diagnostics.adoc[leveloffset=+1] +* Deployment +** xref:deployment/cloud-architecture.adoc[leveloffset=+1] +** xref:deployment/infrastructure-requirements.adoc[leveloffset=+1] + * xref:optimization-algorithms/overview.adoc[Optimization algorithms] ** xref:optimization-algorithms/construction-heuristics.adoc[leveloffset=+1] ** xref:optimization-algorithms/local-search.adoc[leveloffset=+1] @@ -51,9 +48,17 @@ ** Custom moves *** xref:optimization-algorithms/neighborhoods.adoc[Neighborhoods API] *** xref:optimization-algorithms/move-selector-reference.adoc[leveloffset=+1] + * xref:responding-to-change/responding-to-change.adoc[leveloffset=+1] + +* Example use cases +** xref:quickstart/quarkus-vehicle-routing/quarkus-vehicle-routing-quickstart.adoc[Vehicle Routing (Guide)] +** https://github.com/TimefoldAI/timefold-quickstarts[More examples on GitHub^] + + * xref:frequently-asked-questions.adoc[leveloffset=+1] * https://github.com/TimefoldAI/timefold-solver/releases[New and noteworthy][leveloffset=+1] + * Upgrading Timefold Solver ** xref:upgrading-timefold-solver/overview.adoc[leveloffset=+1] ** xref:upgrading-timefold-solver/upgrade-from-v1.adoc[leveloffset=+1] diff --git a/docs/src/modules/ROOT/pages/quickstart/service/getting-started.adoc b/docs/src/modules/ROOT/pages/quickstart/service/getting-started.adoc index a3bbf2a1bf7..c6c0672fd50 100644 --- a/docs/src/modules/ROOT/pages/quickstart/service/getting-started.adoc +++ b/docs/src/modules/ROOT/pages/quickstart/service/getting-started.adoc @@ -230,7 +230,7 @@ However, this class is also the output of the solution: NOTE: The `getConstraintWeightOverrides()` method is required by the `SolverModel` interface (implemented by `AbstractSimpleModel`), and `setConstraintWeightOverrides()` is optional. For now `getConstraintWeightOverrides()` is left as a stub — `none()` means no constraint weights can be overridden per request. -Configuring this properly is covered in the xref:service/constraint-weights.adoc[Constraint weights] section and is not part of this guide. +Configuring this properly is covered in the xref:constraints-and-score/constraint-configuration.adoc#serviceWeightOverrides[constraint weights] section and is not part of this guide. include::../shared/school-timetabling/_school-timetabling-solution-value-range-providers.adoc[leveloffset=+1] diff --git a/docs/src/modules/ROOT/pages/service/constraint-weights.adoc b/docs/src/modules/ROOT/pages/service/constraint-weights.adoc deleted file mode 100644 index 924703ed3d4..00000000000 --- a/docs/src/modules/ROOT/pages/service/constraint-weights.adoc +++ /dev/null @@ -1,12 +0,0 @@ -= Constraint weights (optional) -:description: How to make constraint weights and other parameters user configurable. -:doctype: book -:sectnums: -:icons: font - -include::_preview-note.adoc[] - -For information on adjusting constraint weights at runtime when using the service, -see xref:../constraints-and-score/constraint-configuration.adoc#serviceWeightOverrides[Applying overrides — Using the service]. - -//TODO === Adjusting Model Parameters https://github.com/TimefoldAI/timefold-solver/issues/2347 diff --git a/docs/src/modules/ROOT/pages/service/demo-data.adoc b/docs/src/modules/ROOT/pages/service/demo-data.adoc index e32e6ee85e5..25cf4c33344 100644 --- a/docs/src/modules/ROOT/pages/service/demo-data.adoc +++ b/docs/src/modules/ROOT/pages/service/demo-data.adoc @@ -1,4 +1,4 @@ -= Demo data (optional) += Demo data :description: How to configure the model to generate Demo Data :doctype: book :sectnums: diff --git a/docs/src/modules/ROOT/pages/service/exposing-metrics.adoc b/docs/src/modules/ROOT/pages/service/exposing-metrics.adoc index 0c7388d0e28..b4257c12734 100644 --- a/docs/src/modules/ROOT/pages/service/exposing-metrics.adoc +++ b/docs/src/modules/ROOT/pages/service/exposing-metrics.adoc @@ -1,4 +1,4 @@ -= Exposing metrics (optional) += Exposing metrics :description: How to expose input and output metrics. :doctype: book :sectnums: diff --git a/docs/src/modules/ROOT/pages/service/overview.adoc b/docs/src/modules/ROOT/pages/service/overview.adoc index 8c855e11267..1c671a4f3f7 100644 --- a/docs/src/modules/ROOT/pages/service/overview.adoc +++ b/docs/src/modules/ROOT/pages/service/overview.adoc @@ -1,14 +1,12 @@ [#serviceOverview] -= Building a service (Preview) += Building a service :description: Overview of running Timefold Solver as a fully isolated optimization service. :doctype: book :sectnums: :icons: font -include::_preview-note.adoc[] - Run dataset optimization as a fully isolated service. -This opinionated approach builds on https://quarkus.io[Quarkus] and eliminates the boilerplate typically needed to expose a solver over a REST API. +This opinionated approach eliminates the boilerplate typically needed to expose a solver over a REST API. If you haven't done so yet, start with the xref:../quickstart/service/getting-started.adoc[Getting started: building a service] guide. diff --git a/docs/src/modules/ROOT/pages/service/rest-api.adoc b/docs/src/modules/ROOT/pages/service/rest-api.adoc index 3917cf2634f..aa359cf307a 100644 --- a/docs/src/modules/ROOT/pages/service/rest-api.adoc +++ b/docs/src/modules/ROOT/pages/service/rest-api.adoc @@ -6,8 +6,6 @@ The REST API module will automatically generate REST API endpoints so you can interact with the optimization service. -include::_preview-note.adoc[] - == Setup In order for the API to be automatically generated, Timefold Solver will look for implementations of a couple of interfaces. @@ -151,8 +149,8 @@ The diminished returns termination is the recommended default setting. This termination is desirable since it terminates based on the relative rate of improvement, and behaves similarly on different hardware and different problem instances. `unimprovedSpentLimit` should be set only when necessary. -The `model` configuration is a model-specific field, that contains additional global model configuration attributes. -See xref:./constraint-weights.adoc[Model Configuration Overrides] for more information. +The `model` configuration is a model-specific field that contains additional global model configuration attributes. +See xref:constraints-and-score/constraint-configuration.adoc#serviceWeightOverrides[constraint weights] for more information. [#solvingResponse] === Structured solving response From cfd4200714114032725963e3b029029da3f377cb Mon Sep 17 00:00:00 2001 From: Tom Cools Date: Fri, 26 Jun 2026 22:19:20 +0200 Subject: [PATCH 17/17] docs: further nav structure fixes --- docs/src/modules/ROOT/nav.adoc | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/docs/src/modules/ROOT/nav.adoc b/docs/src/modules/ROOT/nav.adoc index 3b6008d99f9..dca1b4f5be8 100644 --- a/docs/src/modules/ROOT/nav.adoc +++ b/docs/src/modules/ROOT/nav.adoc @@ -3,9 +3,9 @@ * xref:quickstart/overview.adoc[Getting started] ** xref:quickstart/service/getting-started.adoc[Build as a service] ** Embed as a library -*** xref:quickstart/hello-world/hello-world-quickstart.adoc[Hello World Guide] -*** xref:quickstart/quarkus/quarkus-quickstart.adoc[Quarkus Guide] -*** xref:quickstart/spring-boot/spring-boot-quickstart.adoc[Spring Boot Guide] +*** xref:quickstart/hello-world/hello-world-quickstart.adoc[Hello World guide] +*** xref:quickstart/quarkus/quarkus-quickstart.adoc[Quarkus guide] +*** xref:quickstart/spring-boot/spring-boot-quickstart.adoc[Spring Boot guide] * Domain modeling ** xref:running-timefold-solver/modeling-planning-problems.adoc[leveloffset=+1] @@ -20,13 +20,13 @@ ** xref:constraints-and-score/performance.adoc[leveloffset=+1] * xref:running-timefold-solver/overview.adoc[Running the Solver] -** xref:service/overview.adoc[Service Reference (Preview)] +** xref:service/overview.adoc[As a service] *** xref:service/rest-api.adoc[leveloffset=+1] *** xref:service/modeling-changes.adoc[leveloffset=+1] *** xref:constraints-and-score/constraint-configuration.adoc#serviceWeightOverrides[Constraint weights] *** xref:service/demo-data.adoc[leveloffset=+1] *** xref:service/exposing-metrics.adoc[leveloffset=+1] -** xref:running-timefold-solver/library-integration.adoc[Use as a Library] +** xref:running-timefold-solver/library-integration.adoc[As a library] *** xref:running-timefold-solver/configuration.adoc[leveloffset=+1] *** xref:constraints-and-score/constraint-configuration.adoc#libraryWeightOverrides[Constraint weights] *** xref:integration/quarkus.adoc[leveloffset=+1] @@ -37,10 +37,6 @@ ** xref:running-timefold-solver/benchmarking-and-tweaking.adoc[leveloffset=+1] ** xref:running-timefold-solver/solver-diagnostics.adoc[leveloffset=+1] -* Deployment -** xref:deployment/cloud-architecture.adoc[leveloffset=+1] -** xref:deployment/infrastructure-requirements.adoc[leveloffset=+1] - * xref:optimization-algorithms/overview.adoc[Optimization algorithms] ** xref:optimization-algorithms/construction-heuristics.adoc[leveloffset=+1] ** xref:optimization-algorithms/local-search.adoc[leveloffset=+1] @@ -49,10 +45,14 @@ *** xref:optimization-algorithms/neighborhoods.adoc[Neighborhoods API] *** xref:optimization-algorithms/move-selector-reference.adoc[leveloffset=+1] +* Deployment +** xref:deployment/cloud-architecture.adoc[leveloffset=+1] +** xref:deployment/infrastructure-requirements.adoc[leveloffset=+1] + * xref:responding-to-change/responding-to-change.adoc[leveloffset=+1] * Example use cases -** xref:quickstart/quarkus-vehicle-routing/quarkus-vehicle-routing-quickstart.adoc[Vehicle Routing (Guide)] +** xref:quickstart/quarkus-vehicle-routing/quarkus-vehicle-routing-quickstart.adoc[Vehicle routing (guide)] ** https://github.com/TimefoldAI/timefold-quickstarts[More examples on GitHub^] @@ -64,7 +64,7 @@ ** xref:upgrading-timefold-solver/upgrade-from-v1.adoc[leveloffset=+1] ** https://docs.timefold.ai/timefold-solver/1.x/upgrading-timefold-solver/upgrade-from-optaplanner[Upgrading from OptaPlanner][leveloffset=+1] ** xref:upgrading-timefold-solver/backwards-compatibility.adoc[leveloffset=+1] -** Migration Guides +** Migration guides *** xref:upgrading-timefold-solver/migration-guides/variable-listeners-to-custom-shadow-variables.adoc[leveloffset=+1] *** xref:upgrading-timefold-solver/migration-guides/chained-variables-to-planning-list-variable.adoc[leveloffset=+1] * xref:commercial-editions/commercial-editions.adoc[leveloffset=+1]