From cb54e1e811c2c08d3ac4f65dac45f2c152f6d477 Mon Sep 17 00:00:00 2001 From: Tom Cools Date: Tue, 7 Jul 2026 09:42:28 +0200 Subject: [PATCH 1/5] docs: improve model enrichment docs: https://github.com/TimefoldAI/timefold-solver/issues/2415 --- docs/src/modules/ROOT/nav.adoc | 2 +- .../quickstart/service/getting-started.adoc | 2 +- .../service/model-enrichment.adoc | 201 ++++++++++++++++++ .../service/modeling-changes.adoc | 168 --------------- .../service/rest-api.adoc | 2 +- 5 files changed, 204 insertions(+), 171 deletions(-) create mode 100644 docs/src/modules/ROOT/pages/running-timefold-solver/service/model-enrichment.adoc delete mode 100644 docs/src/modules/ROOT/pages/running-timefold-solver/service/modeling-changes.adoc diff --git a/docs/src/modules/ROOT/nav.adoc b/docs/src/modules/ROOT/nav.adoc index 4357f24700..b6b746a86e 100644 --- a/docs/src/modules/ROOT/nav.adoc +++ b/docs/src/modules/ROOT/nav.adoc @@ -23,7 +23,7 @@ ** xref:running-timefold-solver/overview.adoc[Overview] ** xref:running-timefold-solver/service/overview.adoc[As a service] *** xref:running-timefold-solver/service/rest-api.adoc[leveloffset=+1] -*** xref:running-timefold-solver/service/modeling-changes.adoc[leveloffset=+1] +*** xref:running-timefold-solver/service/model-enrichment.adoc[leveloffset=+1] *** xref:running-timefold-solver/service/constraint-overrides.adoc[Constraint weights] *** xref:running-timefold-solver/service/demo-data.adoc[leveloffset=+1] *** xref:running-timefold-solver/service/exposing-metrics.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 da43c65083..3d6ccbeeed 100644 --- a/docs/src/modules/ROOT/pages/quickstart/service/getting-started.adoc +++ b/docs/src/modules/ROOT/pages/quickstart/service/getting-started.adoc @@ -476,5 +476,5 @@ See xref:running-timefold-solver/service/rest-api.adoc#deliberateAPIChanges[Deli Since this is a "Getting Started" guide, not everything is covered yet. * Learn about improvements you can make to your model: -** How to enrich your model with xref:running-timefold-solver/service/modeling-changes.adoc[Model Enrichment]. +** How to enrich your model with xref:running-timefold-solver/service/model-enrichment.adoc[model enrichment]. ** How to configure your xref:running-timefold-solver/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/running-timefold-solver/service/model-enrichment.adoc b/docs/src/modules/ROOT/pages/running-timefold-solver/service/model-enrichment.adoc new file mode 100644 index 0000000000..c437487a21 --- /dev/null +++ b/docs/src/modules/ROOT/pages/running-timefold-solver/service/model-enrichment.adoc @@ -0,0 +1,201 @@ += Model enrichment +:page-aliases: service/modeling-changes.adoc, running-timefold-solver/service/modeling-changes.adoc +:description: Enrich your SolverModel with additional information before solving starts. +:doctype: book +:sectnums: +:icons: font + +[#solverModelEnrichment] +== What is model enrichment? + +Model enrichment is the process of adding extra information to the `SolverModel` before solving starts. +The service receives the planning problem as submitted through the REST API, +but sometimes that raw model is incomplete: it may be missing derived fields, external lookups, or pre-computed values +that are too expensive or impractical to calculate during solving. + +Enrichers run once, just before the solver begins, and produce an augmented model that constraints can use directly. + +Common use cases include: + +* Looking up data from an external service or database (e.g. travel times, holiday calendars, price lists). +* Pinning entities that represent work already completed or committed. +* Pre-computing expensive derived values (e.g. distance matrices, compatibility scores) so constraints stay fast. + +[#whyEnrich] +== Why enrich the model instead of computing inside constraints? + +Constraint streams execute many times per second during solving. +Any computation that happens inside a constraint is repeated on every score calculation. +If that computation involves a remote call, a database query, or a complex algorithm, +it will dominate solving time and make the solver unusably slow. + +[#bestPractices] +== Best practices + +Do:: +* Make enrichers idempotent: calling `enrich()` twice should produce the same result. +* Fail fast: if a required external resource is unavailable, throw an exception rather than silently returning an incomplete model. +* Use the `SolverModelEnrichmentDirector` to control ordering when one enricher depends on another. + +Don't:: +* Do not perform heavy computation inside constraint streams. Move it to an enricher instead. +* Do not modify planning entities' planning variables inside an enricher; only non-planning fields should be set. +* Do not share mutable state between enricher instances; enrichers may be called from multiple threads. +* Do not use enrichment for validation that should happen at input time, reject bad input at the API boundary, not here. + +[#implementingAnEnricher] +== Implementing an enricher + +To enrich the model, implement `SolverModelEnricher` and annotate it so it is picked up by your DI container. + +In this example, the `Timetable` planning solution contains `Timeslot` objects. +Each `Timeslot` has an `isHoliday` field that must be populated by consulting an external calendar service. + +.Timeslot class with the field to be enriched +[tabs] +==== +Java:: ++ +-- +[source,java,options="nowrap"] +---- +public class Timeslot { + + private LocalDateTime startTime; + private LocalDateTime endTime; + + private boolean isHoliday; // <1> + + public void setHoliday(boolean isHoliday) { + this.isHoliday = isHoliday; + } + + // other Getters/Setters/Constructors excluded +} +---- +<1> This field is not provided by the caller — it is populated by the enricher. +-- + +Kotlin:: ++ +-- +[source,kotlin,options="nowrap"] +---- +data class Timeslot( + val startTime: LocalDateTime? = null, + val endTime: LocalDateTime? = null +) { + var isHoliday: Boolean = false // <1> +} +---- +<1> This field is not provided by the caller — it is populated by the enricher. +-- +==== + +.Enricher implementation +[tabs] +==== +Java:: ++ +-- +[source,java,options="nowrap"] +---- +@ApplicationScoped +public class TimeslotHolidayEnricher implements SolverModelEnricher { + + @Override + public Timetable enrich(Timetable solverModel) { + for (Timeslot timeslot : solverModel.getTimeslots()) { + boolean isHoliday = overlapsWithKnownHoliday(timeslot.getStartTime(), timeslot.getEndTime()); + timeslot.setHoliday(isHoliday); + } + return solverModel; + } + + private boolean overlapsWithKnownHoliday(LocalDateTime start, LocalDateTime end) { + // Call an external service or database here. + return false; + } +} +---- +-- + +Kotlin:: ++ +-- +[source,kotlin,options="nowrap"] +---- +@ApplicationScoped +class TimeslotHolidayEnricher : SolverModelEnricher { + + override fun enrich(solverModel: Timetable): Timetable { + for (timeslot in solverModel.timeslots) { + timeslot.isHoliday = overlapsWithKnownHoliday(timeslot.startTime, timeslot.endTime) + } + return solverModel + } + + private fun overlapsWithKnownHoliday(start: LocalDateTime?, end: LocalDateTime?): Boolean { + // Call an external service or database here. + return false + } +} +---- +-- +==== + +[#enrichmentDirector] +== Controlling enrichment order + +When you have multiple enrichers, register a `SolverModelEnrichmentDirector` to control the order in which they run. +This matters when one enricher builds on the output of another. + +//TODO add link to maps component docs https://github.com/TimefoldAI/timefold-solver/issues/2348 + +.Enrichment director that sequences enrichers explicitly +[tabs] +==== +Java:: ++ +-- +[source,java,options="nowrap"] +---- +@ApplicationScoped +public class TimetableEnrichmentDirector implements SolverModelEnrichmentDirector { + + private final TimeslotHolidayEnricher timeslotEnricher; + + @Inject + public TimetableEnrichmentDirector(TimeslotHolidayEnricher timeslotEnricher) { + this.timeslotEnricher = timeslotEnricher; + } + + @Override + public Timetable enrich(Timetable solverModel) { + var enrichedModel = timeslotEnricher.enrich(solverModel); + // Chain additional enrichers here, in dependency order. + return enrichedModel; + } +} +---- +-- + +Kotlin:: ++ +-- +[source,kotlin,options="nowrap"] +---- +@ApplicationScoped +class TimetableEnrichmentDirector @Inject constructor( + private val timeslotEnricher: TimeslotHolidayEnricher +) : SolverModelEnrichmentDirector { + + override fun enrich(solverModel: Timetable): Timetable { + val enrichedModel = timeslotEnricher.enrich(solverModel) + // Chain additional enrichers here, in dependency order. + return enrichedModel + } +} +---- +-- +==== diff --git a/docs/src/modules/ROOT/pages/running-timefold-solver/service/modeling-changes.adoc b/docs/src/modules/ROOT/pages/running-timefold-solver/service/modeling-changes.adoc deleted file mode 100644 index a447f3f67a..0000000000 --- a/docs/src/modules/ROOT/pages/running-timefold-solver/service/modeling-changes.adoc +++ /dev/null @@ -1,168 +0,0 @@ -= Model Enrichment -:page-aliases: service/modeling-changes.adoc -:description: Enrich your SolverModel with additional information before solving starts. -:doctype: book -:sectnums: -:icons: font - -include::_preview-note.adoc[] - -[#solverModelEnrichment] -== SolverModel enrichment - -In some situations, the `SolverModel` must be enriched with additional information. -This could be external information such as map data or -smaller enhancements such as pinning entities which occurred in the past. - -Enrichers usually pre-calculate fields which would otherwise be calculated in a _ConstraintStream_. -Especially when the field depends on external information or is difficult to compute, pre-calculating can lead to much faster results. - -In this example, we enrich the Timetable PlanningSolution described above by filling in the "isHoliday" field for all Timeslot objects. - -.Timeslot class for the School Timetabling example -[tabs] -==== -Java:: -+ --- -[source,java,options="nowrap"] ----- -public class Timeslot { - - private LocalDateTime startTime; - private LocalDateTime endTime; - - private boolean isHoliday; - - public void setHoliday(boolean isHoliday) { - this.isHoliday = isHoliday; - } - - // other Getters/Setters/Constructors excluded -} ----- --- - -Kotlin:: -+ --- -[source,kotlin,options="nowrap"] ----- -data class Timeslot( - val startTime: LocalDateTime? = null, - val endTime: LocalDateTime? = null -) { - var isHoliday: Boolean = false -} ----- --- -==== - -Enrichment of the SolverModel is possible by implementing a `SolverModelEnricher`. - -.Timeslot Enricher for the School Timetabling example -[tabs] -==== -Java:: -+ --- -[source,java,options="nowrap"] ----- -@ApplicationScoped -public class TimeslotHolidayEnricher implements SolverModelEnricher { - - @Override - public Timetable enrich(Timetable solverModel) { - for (Timeslot timeslot : solverModel.getTimeslots()) { - boolean isHoliday = overlapsWithKnownHoliday(timeslot.getStartTime(), timeslot.getEndTime()); - timeslot.setHoliday(isHoliday); - } - - return solverModel; - } - - private boolean overlapsWithKnownHoliday(LocalDateTime start, LocalDateTime end) { - // Implementation excluded; potentially call external service / database. - return false; - } -} ----- --- - -Kotlin:: -+ --- -[source,kotlin,options="nowrap"] ----- -@ApplicationScoped -class TimeslotHolidayEnricher : SolverModelEnricher { - - override fun enrich(solverModel: Timetable): Timetable { - for (timeslot in solverModel.timeslots) { - timeslot.isHoliday = overlapsWithKnownHoliday(timeslot.startTime, timeslot.endTime) - } - - return solverModel - } - - private fun overlapsWithKnownHoliday(start: LocalDateTime?, end: LocalDateTime?): Boolean { - // Implementation excluded; potentially call external service / database. - return false - } -} ----- --- -==== - -Next, register a `SolverModelEnrichmentDirector` implementation. -This class allows you to determine the order in which enrichers are executed. -This might be important when 1 of your custom enrichers depends on an enricher provided by Timefold Solver. -//TODO add link to maps component docs https://github.com/TimefoldAI/timefold-solver/issues/2348 - -.Timetable Enrichment Director for the School Timetabling example -[tabs] -==== -Java:: -+ --- -[source,java,options="nowrap"] ----- -@ApplicationScoped -public class TimetableEnrichmentDirector implements SolverModelEnrichmentDirector { - - private final TimeslotHolidayEnricher timeslotEnricher; - - @Inject - public TimetableEnrichmentDirector(TimeslotHolidayEnricher timeslotEnricher) { - this.timeslotEnricher = timeslotEnricher; - } - - @Override - public Timetable enrich(Timetable solverModel) { - var enrichedModel = timeslotEnricher.enrich(solverModel); - // Additional enrichers. - return enrichedModel; - } -} ----- --- - -Kotlin:: -+ --- -[source,kotlin,options="nowrap"] ----- -@ApplicationScoped -class TimetableEnrichmentDirector @Inject constructor( - private val timeslotEnricher: TimeslotHolidayEnricher -) : SolverModelEnrichmentDirector { - - override fun enrich(solverModel: Timetable): Timetable { - val enrichedModel = timeslotEnricher.enrich(solverModel) - // Additional enrichers. - return enrichedModel - } -} ----- --- -==== \ No newline at end of file diff --git a/docs/src/modules/ROOT/pages/running-timefold-solver/service/rest-api.adoc b/docs/src/modules/ROOT/pages/running-timefold-solver/service/rest-api.adoc index 39827767be..c39d24cc07 100644 --- a/docs/src/modules/ROOT/pages/running-timefold-solver/service/rest-api.adoc +++ b/docs/src/modules/ROOT/pages/running-timefold-solver/service/rest-api.adoc @@ -285,7 +285,7 @@ class TimetableConvertor : ModelConvertor SolverModel -> ModelOutput mapping. -For enhancing the SolverModel, use the xref:./modeling-changes.adoc#solverModelEnrichment[Model Enrichment] mechanism instead. +For enhancing the SolverModel, use the xref:./model-enrichment.adoc[Model Enrichment] mechanism instead. ==== [#validatingRestInput] From 2f3eeced29dc20eef29072f9ee358dd0fbe068cf Mon Sep 17 00:00:00 2001 From: Tom Cools Date: Tue, 7 Jul 2026 11:19:31 +0200 Subject: [PATCH 2/5] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- .../pages/running-timefold-solver/service/model-enrichment.adoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/src/modules/ROOT/pages/running-timefold-solver/service/model-enrichment.adoc b/docs/src/modules/ROOT/pages/running-timefold-solver/service/model-enrichment.adoc index c437487a21..87e65e1336 100644 --- a/docs/src/modules/ROOT/pages/running-timefold-solver/service/model-enrichment.adoc +++ b/docs/src/modules/ROOT/pages/running-timefold-solver/service/model-enrichment.adoc @@ -40,7 +40,7 @@ Do:: Don't:: * Do not perform heavy computation inside constraint streams. Move it to an enricher instead. * Do not modify planning entities' planning variables inside an enricher; only non-planning fields should be set. -* Do not share mutable state between enricher instances; enrichers may be called from multiple threads. +* Do not keep mutable state in enrichers; they may be invoked concurrently from multiple threads. * Do not use enrichment for validation that should happen at input time, reject bad input at the API boundary, not here. [#implementingAnEnricher] From 9f2e58c4944a9fbee3ee7c37d0bf570736080428 Mon Sep 17 00:00:00 2001 From: Tom Cools Date: Mon, 13 Jul 2026 08:12:50 +0200 Subject: [PATCH 3/5] Apply suggestions from code review Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- .../running-timefold-solver/service/model-enrichment.adoc | 4 +++- .../ROOT/pages/running-timefold-solver/service/rest-api.adoc | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/docs/src/modules/ROOT/pages/running-timefold-solver/service/model-enrichment.adoc b/docs/src/modules/ROOT/pages/running-timefold-solver/service/model-enrichment.adoc index 87e65e1336..d4c9c7fb34 100644 --- a/docs/src/modules/ROOT/pages/running-timefold-solver/service/model-enrichment.adoc +++ b/docs/src/modules/ROOT/pages/running-timefold-solver/service/model-enrichment.adoc @@ -33,15 +33,17 @@ it will dominate solving time and make the solver unusably slow. == Best practices Do:: ++ * Make enrichers idempotent: calling `enrich()` twice should produce the same result. * Fail fast: if a required external resource is unavailable, throw an exception rather than silently returning an incomplete model. * Use the `SolverModelEnrichmentDirector` to control ordering when one enricher depends on another. Don't:: ++ * Do not perform heavy computation inside constraint streams. Move it to an enricher instead. * Do not modify planning entities' planning variables inside an enricher; only non-planning fields should be set. * Do not keep mutable state in enrichers; they may be invoked concurrently from multiple threads. -* Do not use enrichment for validation that should happen at input time, reject bad input at the API boundary, not here. +* Do not use enrichment for validation that should happen at input time; reject bad input at the API boundary, not here. [#implementingAnEnricher] == Implementing an enricher diff --git a/docs/src/modules/ROOT/pages/running-timefold-solver/service/rest-api.adoc b/docs/src/modules/ROOT/pages/running-timefold-solver/service/rest-api.adoc index c39d24cc07..c5ac799873 100644 --- a/docs/src/modules/ROOT/pages/running-timefold-solver/service/rest-api.adoc +++ b/docs/src/modules/ROOT/pages/running-timefold-solver/service/rest-api.adoc @@ -285,7 +285,7 @@ class TimetableConvertor : ModelConvertor SolverModel -> ModelOutput mapping. -For enhancing the SolverModel, use the xref:./model-enrichment.adoc[Model Enrichment] mechanism instead. +For enhancing the SolverModel, use the xref:./model-enrichment.adoc#solverModelEnrichment[Model Enrichment] mechanism instead. ==== [#validatingRestInput] From 31fba8bc7220c54dd8538455bc36348b5ac0d50f Mon Sep 17 00:00:00 2001 From: Tom Cools Date: Mon, 13 Jul 2026 10:22:51 +0200 Subject: [PATCH 4/5] docs: extend model-enrichtment docs --- .../service/model-enrichment.adoc | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/docs/src/modules/ROOT/pages/running-timefold-solver/service/model-enrichment.adoc b/docs/src/modules/ROOT/pages/running-timefold-solver/service/model-enrichment.adoc index d4c9c7fb34..d2a4227d95 100644 --- a/docs/src/modules/ROOT/pages/running-timefold-solver/service/model-enrichment.adoc +++ b/docs/src/modules/ROOT/pages/running-timefold-solver/service/model-enrichment.adoc @@ -25,9 +25,11 @@ Common use cases include: == Why enrich the model instead of computing inside constraints? Constraint streams execute many times per second during solving. -Any computation that happens inside a constraint is repeated on every score calculation. -If that computation involves a remote call, a database query, or a complex algorithm, -it will dominate solving time and make the solver unusably slow. +Any enrichment which can be done before solving should be done before solving. + +WARNING: Never do computation which involves a remote call, a database query, +or a complex algorithm while the solver is running (e.g. inside the constraint streams or shadow variables). +This is an antipattern and will make the solver unusably slow. [#bestPractices] == Best practices @@ -41,14 +43,13 @@ Do:: Don't:: + * Do not perform heavy computation inside constraint streams. Move it to an enricher instead. -* Do not modify planning entities' planning variables inside an enricher; only non-planning fields should be set. * Do not keep mutable state in enrichers; they may be invoked concurrently from multiple threads. * Do not use enrichment for validation that should happen at input time; reject bad input at the API boundary, not here. [#implementingAnEnricher] == Implementing an enricher -To enrich the model, implement `SolverModelEnricher` and annotate it so it is picked up by your DI container. +To enrich the model, implement `SolverModelEnricher` and annotate it so it is picked up by your Dependency Injection (DI) container. In this example, the `Timetable` planning solution contains `Timeslot` objects. Each `Timeslot` has an `isHoliday` field that must be populated by consulting an external calendar service. From 2f54098a339812d75c17c34e37e6c339ad642c6c Mon Sep 17 00:00:00 2001 From: Tom Cools Date: Mon, 13 Jul 2026 13:57:32 +0200 Subject: [PATCH 5/5] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- .../pages/running-timefold-solver/service/model-enrichment.adoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/src/modules/ROOT/pages/running-timefold-solver/service/model-enrichment.adoc b/docs/src/modules/ROOT/pages/running-timefold-solver/service/model-enrichment.adoc index d2a4227d95..386c273b5f 100644 --- a/docs/src/modules/ROOT/pages/running-timefold-solver/service/model-enrichment.adoc +++ b/docs/src/modules/ROOT/pages/running-timefold-solver/service/model-enrichment.adoc @@ -25,7 +25,7 @@ Common use cases include: == Why enrich the model instead of computing inside constraints? Constraint streams execute many times per second during solving. -Any enrichment which can be done before solving should be done before solving. +Any computation that can be done once before solving starts should be moved into model enrichment. WARNING: Never do computation which involves a remote call, a database query, or a complex algorithm while the solver is running (e.g. inside the constraint streams or shadow variables).