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..386c273b5f --- /dev/null +++ b/docs/src/modules/ROOT/pages/running-timefold-solver/service/model-enrichment.adoc @@ -0,0 +1,204 @@ += 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 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). +This is an antipattern and will 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 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 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. + +.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..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:./modeling-changes.adoc#solverModelEnrichment[Model Enrichment] mechanism instead. +For enhancing the SolverModel, use the xref:./model-enrichment.adoc#solverModelEnrichment[Model Enrichment] mechanism instead. ==== [#validatingRestInput]