Integration

1. Overview

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. For example:

  • To read a planning problem from the database (and store the best solution in it), annotate the domain POJOs with JPA annotations.

  • To read a planning problem from an XML file (and store the best solution in it), annotate the domain POJOs with JAXB annotations.

  • To expose the Solver as a REST Service that reads the planning problem and responds with the best solution, annotate the domain POJOs with JAXB or Jackson annotations and hook the Solver in RESTEasy.

integrationOverview

2. Persistent storage

2.1. 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().

Do not confuse JPA’s @Entity annotation with Timefold Solver’s @PlanningEntity annotation. They can appear both on the same class:

@PlanningEntity // Timefold Solver annotation
@Entity // JPA annotation
public class Talk {...}

2.1.1. JPA and Hibernate: persisting a Score

The timefold-solver-jpa jar provides a JPA score converter for every built-in score type.

@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, implement the CompositeUserType to persist each score level into a separate database table column.

2.1.2. 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 planning cloned, they need to be cloned too. Use an @DeepPlanningClone on each such problem fact class to enforce that:

@PlanningSolution // Timefold Solver annotation
@Entity // JPA annotation
public class Conference {

    @OneToMany(mappedBy="conference")
    private List<Room> roomList;

    ...
}
@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.

2.2. 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:

2.2.1. 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:

@PlanningSolution
@XmlRootElement @XmlAccessorType(XmlAccessType.FIELD)
public class VehicleRoutePlan {

    @PlanningScore
    @XmlJavaTypeAdapter(HardSoftScoreJaxbAdapter.class)
    private HardSoftScore score;

    ...
}

For example, this generates pretty XML:

<vehicleRoutePlan>
   ...
   <score>0hard/-200soft</score>
</vehicleRoutePlan>

The same applies for a bendable score:

@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:

<schedule>
   ...
   <score>[0/0]hard/[-100/-20/-3]soft</score>
</schedule>

The hardLevelsSize and softLevelsSize implied, when reading a bendable score from an XML element, must always be in sync with those in the solver.

2.3. 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:

ObjectMapper objectMapper = new ObjectMapper();
objectMapper.registerModule(TimefoldJacksonModule.createModule());

2.3.1. 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.

@PlanningSolution
public class VehicleRoutePlan {

    @PlanningScore
    private HardSoftScore score;

    ...
}

For example, this generates:

{
   "score":"0hard/-200soft"
   ...
}

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:

{
   "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:

@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:

@PlanningSolution
public class VehicleRoutePlan {

    @PlanningScore
    private Score score;

    ...
}

For example, this generates:

{
   "score":{"HardSoftScore":"0hard/-200soft"}
   ...
}

3. Quarkus

To use Timefold Solver with Quarkus, read the Quarkus Java quick start. If you are starting a new project, visit the code.quarkus.io and select the Timefold AI constraint solver extension before generating your application.

3.1. Available configuration properties

Following properties are supported in the Quarkus application.properties:

quarkus.timefold.solver-manager.parallel-solver-count

The number of solvers that run in parallel. This directly influences CPU consumption. Defaults to AUTO.

quarkus.timefold.solver.solver-config-xml

A classpath resource to read the 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 configuration continues without using any configuration file. The specified resources take precedence over the default file solverConfig.xml, even if both files' resources are located in the classpath.

quarkus.timefold.solver.environment-mode

Enable runtime assertions to detect common bugs in your implementation during development.

quarkus.timefold.solver.daemon

Enable daemon mode. In daemon mode, non-early termination pauses the solver instead of stopping it, until the next problem fact change arrives. This is often useful for real-time planning. Defaults to false.

quarkus.timefold.solver.move-thread-count

Enable multi-threaded solving for a single problem, which increases CPU consumption. Defaults to NONE. Note that this is a feature of the Enterprise edition, which is Timefold’s commercial offering. See multithreaded incremental solving.

quarkus.timefold.solver.domain-access-type

How Timefold Solver should access the domain model. See the domain access section for more details. Defaults to GIZMO. The other possible value is REFLECTION.

quarkus.timefold.solver.nearby-distance-meter-class

Enable the Nearby Selection quick configuration. If the Nearby Selection distance meter class is specified, the solver evaluates the available move selectors and automatically enables Nearby Selection for the compatible move selectors.

quarkus.timefold.solver.termination.spent-limit

How long the solver can run. For example: 30s is 30 seconds. 5m is 5 minutes. 2h is 2 hours. 1d is 1 day. The ISO8601 format is also supported, e.g., PT30S is 30 seconds. PT5M is 5 minutes. PT2H is 2 hours. P1D is 1 day.

quarkus.timefold.solver.termination.unimproved-spent-limit

How long the solver can run without finding a new best solution after finding a new best solution. For example: 30s is 30 seconds. 5m is 5 minutes. 2h is 2 hours. 1d is 1 day. The ISO8601 format is also supported, e.g., PT30S is 30 seconds. PT5M is 5 minutes. PT2H is 2 hours. P1D is 1 day.

quarkus.timefold.solver.termination.best-score-limit

Terminates the solver when a specific score (or better) has been reached. For example: 0hard/-1000soft terminates when the best score changes from 0hard/-1200soft to 0hard/-900soft. Wildcards are supported to replace numbers. For example: 0hard/*soft to terminate when any feasible score is reached.

quarkus.timefold.benchmark.solver-benchmark-config-xml

A classpath resource to read the benchmark configuration XML. Defaults to solverBenchmarkConfig.xml. If this property isn’t specified, that solverBenchmarkConfig.xml is optional.

quarkus.timefold.benchmark.result-directory

Where the benchmark results are written to. Defaults to target/benchmarks.

quarkus.timefold.benchmark.solver.termination.spent-limit

How long solver should be run in a benchmark run. For example: 30s is 30 seconds. 5m is 5 minutes. 2h is 2 hours. 1d is 1 day. Also supports ISO-8601 format, see Duration.

3.2. Injecting managed resources

The Quarkus integration allows the injection of several managed resources, including SolverConfig, SolverFactory, SolverManager, SolutionManager, ScoreManager, 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, domain-access-type, etc.) for the planning problem are identified and loaded into the solver configuration.

The available resouses can be injected as follows:

  • Java

  • Kotlin

@Path("/path")
public class Resource {

    @Inject
    SolverConfig solverConfig;

    @Inject
    SolverFactory<Timetable> solverFactory;

    @Inject
    SolverManager<Timetable, String> solverManager;

    @Inject
    SolutionManager<Timetable, SimpleScore> simpleSolutionManager; (1)

    @Inject
    ScoreManager<Timetable, SimpleScore> simpleScoreManager; (1)

    @Inject
    ConstraintVerifier<TimetableConstraintProvider, Timetable> constraintVerifier;

    ...
}
1 You can find all the available score types in the Constraints and Score page.
@Path("path")
class Resource {

    @Inject
    var solverConfig:SolverConfig?

    @Inject
    var solverFactory:SolverFactory<Timetable>?

    @Inject
    var solverManager:SolverManager<Timetable, String>?

    @Inject
    var simpleSolutionManager:SolutionManager<Timetable, SimpleScore>? (1)

    @Inject
    var simpleScoreManager:ScoreManager<Timetable, SimpleScore>? (1)

    @Inject
    var constraintVerifier:ConstraintVerifier<TimetableConstraintProvider, Timetable>? = null

    ...
}
1 You can find all the available score types in the 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.

  • Java

  • Kotlin

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<Timetable, String> overrideSolverManager(SolverFactory<Timetable> solverFactory) {
        SolverManagerConfig solverManagerConfig = new SolverManagerConfig();
        return SolverManager.create(solverFactory, solverManagerConfig);
    }
}
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<Timetable>?): SolverManager<Timetable, String> {
        val solverManagerConfig = SolverManagerConfig()
        return SolverManager.create(solverFactory, solverManagerConfig)
    }
}

Consider using multiple solver configurations instead of manually creating resources.

3.3. 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.

3.3.1. Solver configuration properties

The configuration properties for multiple solvers are defined using the namespace quarkus.timefold.solver.<solverName>, where <solverName> is the name of the related solver. The <solverName> property is only necessary when using multiple solvers. For defining a single solver, refer to the Timefold configuration properties section. Following properties are supported:

quarkus.timefold.solver.<solverName>.solver-config-xml

A classpath resource to read the 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 configuration continues without using any configuration file. The specified resources take precedence over the default file solverConfig.xml, even if both files' resources are located in the classpath.

quarkus.timefold.solver.<solverName>.environment-mode

Enable runtime assertions to detect common bugs in your implementation during development.

quarkus.timefold.solver.<solverName>.daemon

Enable daemon mode. In daemon mode, non-early termination pauses the solver instead of stopping it, until the next problem fact change arrives. This is often useful for real-time planning. Defaults to false.

quarkus.timefold.solver.<solverName>.move-thread-count

Enable multi-threaded solving for a single problem, which increases CPU consumption. Defaults to NONE. Note that this is a feature of the Enterprise edition, which is Timefold’s commercial offering. See multithreaded incremental solving.

quarkus.timefold.solver.<solverName>.domain-access-type

How Timefold Solver should access the domain model. See the domain access section for more details. Defaults to GIZMO. The other possible value is REFLECTION.

quarkus.timefold.solver.<solverName>.nearby-distance-meter-class

Enable the Nearby Selection quick configuration. If the Nearby Selection distance meter class is specified, the solver evaluates the available move selectors and automatically enables Nearby Selection for the compatible move selectors.

quarkus.timefold.solver.<solverName>.termination.spent-limit

How long the solver can run. For example: 30s is 30 seconds. 5m is 5 minutes. 2h is 2 hours. 1d is 1 day. The ISO8601 format is also supported, e.g., PT30S is 30 seconds. PT5M is 5 minutes. PT2H is 2 hours. P1D is 1 day.

quarkus.timefold.solver.<solverName>.termination.unimproved-spent-limit

How long the solver can run without finding a new best solution after finding a new best solution. For example: 30s is 30 seconds. 5m is 5 minutes. 2h is 2 hours. 1d is 1 day. The ISO8601 format is also supported, e.g., PT30S is 30 seconds. PT5M is 5 minutes. PT2H is 2 hours. P1D is 1 day.

quarkus.timefold.solver.<solverName>.termination.best-score-limit

Terminates the solver when a specific score (or better) has been reached. For example: 0hard/-1000soft terminates when the best score changes from 0hard/-1200soft to 0hard/-900soft. Wildcards are supported to replace numbers. For example: 0hard/*soft to terminate when any feasible score is reached.

3.3.2. 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:

# 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.

  • Java

  • Kotlin

@Path("/path")
public class Resource {

    @Named("fastSolver")
    SolverManager<...> fastSolver;

    @Named("regularSolver")
    SolverManager<...> regularSolver;

    ...
}
@Path("path")
class Resource {

    private final var fastSolver: SolverManager<...>?
    private final var regularSolver: SolverManager<...>?

    @Inject
    constructor(
        @Named("fastSolver") fastSolver: SolverManager<Timetable, String>,
        @Named("regularSolver") fastSolver: SolverManager<Timetable, String>
    ) {
        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.

  • teachersSolverConfig.xml

  • roomsSolverConfig.xml

<solver xmlns="https://timefold.ai/xsd/solver" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="https://timefold.ai/xsd/solver https://timefold.ai/xsd/solver/solver.xsd">
  <solutionClass>org.acme.schooltimetabling.domain.TeacherToLessonSchedule</solutionClass>
  <entityClass>org.acme.schooltimetabling.domain.Teacher</entityClass>
</solver>
<solver xmlns="https://timefold.ai/xsd/solver" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="https://timefold.ai/xsd/solver https://timefold.ai/xsd/solver/solver.xsd">
  <solutionClass>org.acme.schooltimetabling.domain.Timetable</solutionClass>
  <entityClass>org.acme.schooltimetabling.domain.Lesson</entityClass>
</solver>

Set the solvers with the specific XML files in the application.properties file:

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.

  • Java

  • Kotlin

@PlanningSolution
public class TeacherToLessonSchedule {
    ...
}

@PlanningEntity
public class Teacher {
    ...
}

@Path("/path")
public class Resource {

    @Named("teacherSolver")
    SolverManager<TeacherToLessonSchedule, String> teacherToLessonScheduleSolverManager;

    @Named("roomSolver")
    SolverManager<Timetable, String> lessonToRoomTimeslotSolverManager;

    ...
}
@PlanningSolution
class TeacherToLessonSchedule {
    ...
}

@PlanningEntity
class Teacher {
    ...
}

@Path("path")
class Resource {

    private final var teacherToLessonScheduleSolverManager: SolverManager<TeacherToLessonSchedule, String>?
    private final var lessonToRoomTimeslotSolverManager: SolverManager<Timetable, String>?

    @Inject
    constructor(
        @Named("teacherSolver") teacherToLessonScheduleSolverManager: SolverManager<TeacherToLessonSchedule, String>,
        @Named("roomSolver") lessonToRoomTimeslotSolverManager: SolverManager<Timetable, String>
    ) {
        this.teacherToLessonScheduleSolverManager = teacherToLessonScheduleSolverManager
        this.lessonToRoomTimeslotSolverManager = lessonToRoomTimeslotSolverManager
    }

    ...
}

Multi-stage planning can also be accomplished by using a separate solver configuration for each optimization stage.

4. Spring Boot

To use Timefold Solver on Spring Boot, add the timefold-solver-spring-boot-starter dependency and read the Spring Boot Java quick start.

4.1. Available configuration properties

These properties are supported in Spring’s application.properties:

timefold.solver-manager.parallel-solver-count

The number of solvers that run in parallel. This directly influences CPU consumption. Defaults to AUTO.

timefold.solver.solver-config-xml

A classpath resource to read the 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 configuration continues without using any configuration file. The specified resources take precedence over the default file solverConfig.xml, even if both files' resources are located in the classpath.

timefold.solver.environment-mode

Enable runtime assertions to detect common bugs in your implementation during development.

timefold.solver.daemon

Enable daemon mode. In daemon mode, non-early termination pauses the solver instead of stopping it, until the next problem fact change arrives. This is often useful for real-time planning. Defaults to false.

timefold.solver.move-thread-count

Enable multi-threaded solving for a single problem, which increases CPU consumption. Defaults to NONE. Note that this is a feature of the Enterprise edition, which is Timefold’s commercial offering. See multithreaded incremental solving.

timefold.solver.domain-access-type

How Timefold Solver should access the domain model. See the domain access section for more details. Defaults to REFLECTION. The other possible value is GIZMO.

timefold.solver.nearby-distance-meter-class

Enable the Nearby Selection quick configuration. If the Nearby Selection distance meter class is specified, the solver evaluates the available move selectors and automatically enables Nearby Selection for the compatible move selectors.

timefold.solver.termination.spent-limit

How long the solver can run. For example: 30s is 30 seconds. 5m is 5 minutes. 2h is 2 hours. 1d is 1 day. The ISO8601 format is also supported, e.g., PT30S is 30 seconds. PT5M is 5 minutes. PT2H is 2 hours. P1D is 1 day.

timefold.solver.termination.unimproved-spent-limit

How long the solver can run without finding a new best solution after finding a new best solution. For example: 30s is 30 seconds. 5m is 5 minutes. 2h is 2 hours. 1d is 1 day. The ISO8601 format is also supported, e.g., PT30S is 30 seconds. PT5M is 5 minutes. PT2H is 2 hours. P1D is 1 day.

timefold.solver.termination.best-score-limit

Terminates the solver when a specific score (or better) has been reached. For example: 0hard/-1000soft terminates when the best score changes from 0hard/-1200soft to 0hard/-900soft. Wildcards are supported to replace numbers. For example: 0hard/*soft to terminate when any feasible score is reached.

timefold.benchmark.solver-benchmark-config-xml

A classpath resource to read the benchmark configuration XML. Defaults to solverBenchmarkConfig.xml. If this property isn’t specified, that solverBenchmarkConfig.xml is optional.

timefold.benchmark.result-directory

Where the benchmark results are written to. Defaults to target/benchmarks.

timefold.benchmark.solver.termination.spent-limit

How long solver should be run in a benchmark run. For example: 30s is 30 seconds. 5m is 5 minutes. 2h is 2 hours. 1d is 1 day. Also supports ISO-8601 format, see Duration.

4.2. Injecting managed resources

The Spring Boot integration allows the injection of several managed resources, including SolverConfig, SolverFactory, SolverManager, SolutionManager, ScoreManager, 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, domain-access-type, etc.) for the planning problem are identified and loaded into the solver configuration.

The available resouses can be injected as follows:

  • Java

  • Kotlin

@RestController
@RequestMapping("/path")
public class Resource {

    @Autowired
    SolverConfig solverConfig;

    @Autowired
    SolverFactory<Timetable> solverFactory;

    @Autowired
    SolverManager<Timetable, String> solverManager;

    @Autowired
    SolutionManager<Timetable, SimpleScore> simpleSolutionManager; (1)

    @Autowired
    ScoreManager<Timetable, SimpleScore> simpleScoreManager; (1)

    @Autowired
    ConstraintVerifier<TimetableConstraintProvider, Timetable> constraintVerifier;

    ...
}
1 You can find all the available score types in the Constraints and Score page.
@RestController
@RequestMapping("/path")
class Resource {

    @Autowired
    var solverConfig:SolverConfig?

    @Autowired
    var solverFactory:SolverFactory<Timetable>?

    @Autowired
    var solverManager:SolverManager<Timetable, String>?

    @Autowired
    var simpleSolutionManager:SolutionManager<Timetable, SimpleScore>? (1)

    @Autowired
    var simpleScoreManager:ScoreManager<Timetable, SimpleScore>? (1)

    @Autowired
    var constraintVerifier:ConstraintVerifier<TimetableConstraintProvider, Timetable>? = null

    ...
}
1 You can find all the available score types in the 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.

  • Java

  • Kotlin

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<Timetable, String> overrideSolverManager(SolverFactory<Timetable> solverFactory) {
        SolverManagerConfig solverManagerConfig = new SolverManagerConfig();
        return SolverManager.create(solverFactory, solverManagerConfig);
    }
}
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<Timetable>?): SolverManager<Timetable, String> {
        val solverManagerConfig = SolverManagerConfig()
        return SolverManager.create(solverFactory, solverManagerConfig)
    }
}

Consider using multiple solver configurations instead of manually creating resources.

4.3. 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.

4.3.1. Solver configuration properties

The configuration properties for multiple solvers are defined using the namespace timefold.solver.<solverName>, where <solverName> is the name of the related solver. The <solverName> property is only necessary when using multiple solvers. For defining a single solver, refer to the Timefold configuration properties section. Following properties are supported:

timefold.solver.<solverName>.solver-config-xml

A classpath resource to read the 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 configuration continues without using any configuration file. The specified resources take precedence over the default file solverConfig.xml, even if both files' resources are located in the classpath.

timefold.solver.<solverName>.environment-mode

Enable runtime assertions to detect common bugs in your implementation during development.

timefold.solver.<solverName>.daemon

Enable daemon mode. In daemon mode, non-early termination pauses the solver instead of stopping it, until the next problem fact change arrives. This is often useful for real-time planning. Defaults to false.

timefold.solver.<solverName>.move-thread-count

Enable multi-threaded solving for a single problem, which increases CPU consumption. Defaults to NONE. Note that this is a feature of the Enterprise edition, which is Timefold’s commercial offering. See multithreaded incremental solving.

timefold.solver.<solverName>.domain-access-type

How Timefold Solver should access the domain model. See the domain access section for more details. Defaults to REFLECTION. The other possible value is GIZMO.

timefold.solver.<solverName>.nearby-distance-meter-class

Enable the Nearby Selection quick configuration. If the Nearby Selection distance meter class is specified, the solver evaluates the available move selectors and automatically enables Nearby Selection for the compatible move selectors.

timefold.solver.<solverName>.termination.spent-limit

How long the solver can run. For example: 30s is 30 seconds. 5m is 5 minutes. 2h is 2 hours. 1d is 1 day. The ISO8601 format is also supported, e.g., PT30S is 30 seconds. PT5M is 5 minutes. PT2H is 2 hours. P1D is 1 day.

timefold.solver.<solverName>.termination.unimproved-spent-limit

How long the solver can run without finding a new best solution after finding a new best solution. For example: 30s is 30 seconds. 5m is 5 minutes. 2h is 2 hours. 1d is 1 day. The ISO8601 format is also supported, e.g., PT30S is 30 seconds. PT5M is 5 minutes. PT2H is 2 hours. P1D is 1 day.

timefold.solver.<solverName>.termination.best-score-limit

Terminates the solver when a specific score (or better) has been reached. For example: 0hard/-1000soft terminates when the best score changes from 0hard/-1200soft to 0hard/-900soft. Wildcards are supported to replace numbers. For example: 0hard/*soft to terminate when any feasible score is reached.

4.3.2. 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:

# 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.

  • Java

  • Kotlin

@RestController
@RequestMapping("/path")
public class Resource {

    @Autowired
    @Qualifier("fastSolver")
    SolverManager<...> fastSolver;

    @Autowired
    @Qualifier("regularSolver")
    SolverManager<...> regularSolver;

    ...
}
@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.

  • teachersSolverConfig.xml

  • roomsSolverConfig.xml

<solver xmlns="https://timefold.ai/xsd/solver" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="https://timefold.ai/xsd/solver https://timefold.ai/xsd/solver/solver.xsd">
  <solutionClass>org.acme.schooltimetabling.domain.TeacherToLessonSchedule</solutionClass>
  <entityClass>org.acme.schooltimetabling.domain.Teacher</entityClass>
</solver>
<solver xmlns="https://timefold.ai/xsd/solver" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="https://timefold.ai/xsd/solver https://timefold.ai/xsd/solver/solver.xsd">
  <solutionClass>org.acme.schooltimetabling.domain.Timetable</solutionClass>
  <entityClass>org.acme.schooltimetabling.domain.Lesson</entityClass>
</solver>

Set the solvers with the specific XML files in the application.properties file:

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.

  • Java

  • Kotlin

@PlanningSolution
public class TeacherToLessonSchedule {
    ...
}

@PlanningEntity
public class Teacher {
    ...
}

@RestController
@RequestMapping("/path")
public class Resource {

    @Autowired
    @Qualifier("teacherSolver")
    SolverManager<TeacherToLessonSchedule, String> teacherToLessonScheduleSolverManager;

    @Autowired
    @Qualifier("roomSolver")
    SolverManager<Timetable, String> lessonToRoomTimeslotSolverManager;

    ...
}
@PlanningSolution
class TeacherToLessonSchedule {
    ...
}

@PlanningEntity
class Teacher {
    ...
}

@RestController
@RequestMapping("/path")
class Resource {

    @Autowired
    @Qualifier("teacherSolver")
    var teacherToLessonScheduleSolverManager: SolverManager<TeacherToLessonSchedule, String>?

    @Autowired
    @Qualifier("roomSolver")
    var lessonToRoomTimeslotSolverManager: SolverManager<Timetable, String>?

    ...
}

Multi-stage planning can also be accomplished by using a separate solver configuration for each optimization stage.

5. Other environments

5.1. Java platform module system (Jigsaw)

When using Timefold Solver from code on the modulepath (Java 9 and higher), open your packages that contain your domain objects, constraints and solver configuration to all modules in your module-info.java file:

module org.acme.vehiclerouting {
    requires ai.timefold.solver.core;
    ...

    opens org.acme.vehiclerouting.domain; // Domain classes
    opens org.acme.vehiclerouting.solver; // Constraints
    ...
}

Otherwise Timefold Solver can’t reach those classes or files, even if they are exported.

6. 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 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 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.

keepTheUserInControl

7. Sizing hardware and software

Before sizing a Timefold Solver service, first understand the typical behaviour of a Solver.solve() call:

sizingHardware

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 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 score calculation 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 nearby selection and caching move selector, neither of which are used by default.

  • CPU power: More is better.

    • Improving CPU speed directly increases the score calculation 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 daemon mode or if your SolverEventListener writes the best solution to disk or the network).

  • Number of CPU cores: one CPU core per active Solver, plus at least one 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 score calculation 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 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 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.