Docs
  • Solver
  • Models
    • Field Service Routing
    • Employee Shift Scheduling
  • Platform
Try models
  • Timefold Solver 1.22.1
  • Define the constraints and calculate the score
  • Edit this Page
  • latest
    • latest
    • 0.8.x

Timefold Solver 1.22.1

    • Introduction
    • PlanningAI Concepts
    • Getting Started
      • Overview
      • Hello World Quick Start Guide
      • Quarkus Quick Start Guide
      • Spring Boot Quick Start Guide
      • Vehicle Routing Quick Start Guide
    • Using Timefold Solver
      • Using Timefold Solver: Overview
      • Configuring Timefold Solver
      • Modeling planning problems
      • Running Timefold Solver
      • Benchmarking and tweaking
    • Constraints and Score
      • Constraints and Score: Overview
      • Score calculation
      • Understanding the score
      • Adjusting constraints at runtime
      • Load balancing and fairness
      • Performance tips and tricks
    • Optimization algorithms
      • Optimization Algorithms: Overview
      • Construction heuristics
      • Local search
      • Exhaustive search
      • Move Selector reference
    • Responding to change
    • Integration
    • Design patterns
    • FAQ
    • New and noteworthy
    • Upgrading Timefold Solver
      • Upgrading Timefold Solver: Overview
      • Upgrade to the latest version
      • Upgrade from OptaPlanner
      • Backwards compatibility
    • Enterprise Edition

Define the constraints and calculate the score

A score represents the quality of a specific solution. The higher the better. Timefold Solver looks for the best solution, which is the solution with the highest score found in the available time. It might be the optimal solution.

Because this use case has hard and soft constraints, use the HardSoftScore class to represent the score:

  • Hard constraints must not be broken. For example: A room can have at most one lesson at the same time.

  • Soft constraints should not be broken. For example: A teacher prefers to teach in a single room.

Hard constraints are weighted against other hard constraints. Soft constraints are weighted too, against other soft constraints. Hard constraints always outweigh soft constraints, regardless of their respective weights.

To calculate the score, you could implement an EasyScoreCalculator class:

  • Java

  • Kotlin

  • Python

public class TimetableEasyScoreCalculator implements EasyScoreCalculator<Timetable, HardSoftScore> {

    @Override
    public HardSoftScore calculateScore(Timetable timetable) {
        List<Lesson> lessons = timetable.getLessons();
        int hardScore = 0;
        for (Lesson a : lessons) {
            for (Lesson b : lessons) {
                if (a.getTimeslot() != null && a.getTimeslot().equals(b.getTimeslot())
                        && a.getId() < b.getId()) {
                    // A room can accommodate at most one lesson at the same time.
                    if (a.getRoom() != null && a.getRoom().equals(b.getRoom())) {
                        hardScore--;
                    }
                    // A teacher can teach at most one lesson at the same time.
                    if (a.getTeacher().equals(b.getTeacher())) {
                        hardScore--;
                    }
                    // A student can attend at most one lesson at the same time.
                    if (a.getStudentGroup().equals(b.getStudentGroup())) {
                        hardScore--;
                    }
                }
            }
        }
        int softScore = 0;
        // Soft constraints are only implemented in the timefold-quickstarts code
        return HardSoftScore.of(hardScore, softScore);
    }

}
class TimetableEasyScoreCalculator : EasyScoreCalculator<Timetable, HardSoftScore> {
    override fun calculateScore(solution: Timetable): HardSoftScore {
        val lessons = solution.lessons
        var hardScore = 0
        for (a in lessons) {
            for (b in lessons) {
                if (a.timeslot != null && a.timeslot == b.timeslot && a.id!! < b.id!!) {
                    // A room can accommodate at most one lesson at the same time.
                    if (a.room != null && a.room == b.room) {
                        hardScore--
                    }
                    // A teacher can teach at most one lesson at the same time.
                    if (a.teacher == b.teacher) {
                        hardScore--
                    }
                    // A student can attend at most one lesson at the same time.
                    if (a.studentGroup == b.studentGroup) {
                        hardScore--
                    }
                }
            }
        }
        val softScore = 0
        // Soft constraints are only implemented in the timefold-quickstarts code
        return HardSoftScore.of(hardScore, softScore)
    }
}
from timefold.score.score import easy_score_calculator, HardSoftScore

@easy_score_calculator
def school_timetable_constraints(solution: Timetable):
    lessons = solution.lessons
    hard_score = 0
    for a in lessons:
        for b in lessons:
            if a.timeslot != null and a.timeslot == b.timeslot and a.id < b.id:
                # A room can accommodate at most one lesson at the same time.
                if a.room != null and a.room == b.room:
                    hard_score -= 1

                # A teacher can teach at most one lesson at the same time.
                if a.teacher == b.teacher:
                    hard_score -= 1

                # A student can attend at most one lesson at the same time.
                if a.student_group == b.student_group:
                    hard_score -= 1
    soft_score = 0
    # Soft constraints are only implemented in the timefold-quickstarts code
    return HardSoftScore.of(hard_score, soft_score)

Unfortunately that does not scale well, because it is non-incremental: every time a lesson is assigned to a different time slot or room, all lessons are re-evaluated to calculate the new score.

Instead, create a TimetableConstraintProvider class to perform incremental score calculation. It uses Timefold Solver’s Constraint Streams API which is inspired by Java Streams and SQL:

  • Java

  • Kotlin

  • Python

Create a src/main/java/org/acme/schooltimetabling/solver/TimetableConstraintProvider.java class:

package org.acme.schooltimetabling.solver;

import org.acme.schooltimetabling.domain.Lesson;
import ai.timefold.solver.core.api.score.buildin.hardsoft.HardSoftScore;
import ai.timefold.solver.core.api.score.stream.Constraint;
import ai.timefold.solver.core.api.score.stream.ConstraintFactory;
import ai.timefold.solver.core.api.score.stream.ConstraintProvider;
import ai.timefold.solver.core.api.score.stream.Joiners;

public class TimetableConstraintProvider implements ConstraintProvider {

    @Override
    public Constraint[] defineConstraints(ConstraintFactory constraintFactory) {
        return new Constraint[] {
                // Hard constraints
                roomConflict(constraintFactory),
                teacherConflict(constraintFactory),
                studentGroupConflict(constraintFactory),
                // Soft constraints are only implemented in the timefold-quickstarts code
        };
    }

    Constraint roomConflict(ConstraintFactory constraintFactory) {
        // A room can accommodate at most one lesson at the same time.
        return constraintFactory
                // Select each pair of 2 different lessons ...
                .forEachUniquePair(Lesson.class,
                        // ... in the same timeslot ...
                        Joiners.equal(Lesson::getTimeslot),
                        // ... in the same room ...
                        Joiners.equal(Lesson::getRoom))
                // ... and penalize each pair with a hard weight.
                .penalize(HardSoftScore.ONE_HARD)
                .asConstraint("Room conflict");
    }

    Constraint teacherConflict(ConstraintFactory constraintFactory) {
        // A teacher can teach at most one lesson at the same time.
        return constraintFactory
                .forEachUniquePair(Lesson.class,
                        Joiners.equal(Lesson::getTimeslot),
                        Joiners.equal(Lesson::getTeacher))
                .penalize(HardSoftScore.ONE_HARD)
                .asConstraint("Teacher conflict");
    }

    Constraint studentGroupConflict(ConstraintFactory constraintFactory) {
        // A student can attend at most one lesson at the same time.
        return constraintFactory
                .forEachUniquePair(Lesson.class,
                        Joiners.equal(Lesson::getTimeslot),
                        Joiners.equal(Lesson::getStudentGroup))
                .penalize(HardSoftScore.ONE_HARD)
                .asConstraint("Student group conflict");
    }

}

Create a src/main/kotlin/org/acme/schooltimetabling/solver/TimetableConstraintProvider.kt class:

package org.acme.kotlin.schooltimetabling.solver

import ai.timefold.solver.core.api.score.buildin.hardsoft.HardSoftScore
import ai.timefold.solver.core.api.score.stream.Constraint
import ai.timefold.solver.core.api.score.stream.ConstraintFactory
import ai.timefold.solver.core.api.score.stream.ConstraintProvider
import ai.timefold.solver.core.api.score.stream.Joiners
import org.acme.kotlin.schooltimetabling.domain.Lesson
import org.acme.kotlin.schooltimetabling.solver.justifications.*
import java.time.Duration

class TimeTableConstraintProvider : ConstraintProvider {

    override fun defineConstraints(constraintFactory: ConstraintFactory): Array<Constraint> {
        return arrayOf(
            // Hard constraints
            roomConflict(constraintFactory),
            teacherConflict(constraintFactory),
            studentGroupConflict(constraintFactory),
            // Soft constraints
            teacherRoomStability(constraintFactory),
            teacherTimeEfficiency(constraintFactory),
            studentGroupSubjectVariety(constraintFactory)
        )
    }

    fun roomConflict(constraintFactory: ConstraintFactory): Constraint {
        // A room can accommodate at most one lesson at the same time.
        return constraintFactory
            // Select each pair of 2 different lessons ...
            .forEachUniquePair(
                Lesson::class.java,
                // ... in the same timeslot ...
                Joiners.equal(Lesson::timeslot),
                // ... in the same room ...
                Joiners.equal(Lesson::room)
            )
            // ... and penalize each pair with a hard weight.
            .penalize(HardSoftScore.ONE_HARD)
            .justifyWith { lesson1: Lesson, lesson2: Lesson, _ ->
                RoomConflictJustification(lesson1.room, lesson1,lesson2)}
            .asConstraint("Room conflict")
    }

    fun teacherConflict(constraintFactory: ConstraintFactory): Constraint {
        // A teacher can teach at most one lesson at the same time.
        return constraintFactory
            .forEachUniquePair(
                Lesson::class.java,
                Joiners.equal(Lesson::timeslot),
                Joiners.equal(Lesson::teacher)
            )
            .penalize(HardSoftScore.ONE_HARD)
            .justifyWith { lesson1: Lesson, lesson2: Lesson, _ ->
                TeacherConflictJustification(lesson1.teacher, lesson1, lesson2)}
            .asConstraint("Teacher conflict")
    }

    fun studentGroupConflict(constraintFactory: ConstraintFactory): Constraint {
        // A student can attend at most one lesson at the same time.
        return constraintFactory
            .forEachUniquePair(
                Lesson::class.java,
                Joiners.equal(Lesson::timeslot),
                Joiners.equal(Lesson::studentGroup)
            )
            .penalize(HardSoftScore.ONE_HARD)
            .justifyWith { lesson1: Lesson, lesson2: Lesson, _ ->
                StudentGroupConflictJustification(lesson1.studentGroup, lesson1, lesson2)}
            .asConstraint("Student group conflict")
    }

    fun teacherRoomStability(constraintFactory: ConstraintFactory): Constraint {
        // A teacher prefers to teach in a single room.
        return constraintFactory
            .forEachUniquePair(
                Lesson::class.java,
                Joiners.equal(Lesson::teacher)
            )
            .filter { lesson1: Lesson, lesson2: Lesson -> lesson1.room !== lesson2.room }
            .penalize(HardSoftScore.ONE_SOFT)
            .justifyWith { lesson1: Lesson, lesson2: Lesson, _ ->
                TeacherRoomStabilityJustification(lesson1.teacher, lesson1, lesson2)}
            .asConstraint("Teacher room stability")
    }

    fun teacherTimeEfficiency(constraintFactory: ConstraintFactory): Constraint {
        // A teacher prefers to teach sequential lessons and dislikes gaps between lessons.
        return constraintFactory
            .forEach(Lesson::class.java)
            .join(Lesson::class.java,
                Joiners.equal(Lesson::teacher),
                Joiners.equal { lesson: Lesson -> lesson.timeslot?.dayOfWeek })
            .filter { lesson1: Lesson, lesson2: Lesson ->
                val between = Duration.between(
                    lesson1.timeslot?.endTime,
                    lesson2.timeslot?.startTime
                )
                !between.isNegative && between <= Duration.ofMinutes(30)
            }
            .reward(HardSoftScore.ONE_SOFT)
            .justifyWith{ lesson1: Lesson, lesson2: Lesson, _ ->
                TeacherTimeEfficiencyJustification(lesson1.teacher, lesson1, lesson2)}
            .asConstraint("Teacher time efficiency")
    }

    fun studentGroupSubjectVariety(constraintFactory: ConstraintFactory): Constraint {
        // A student group dislikes sequential lessons on the same subject.
        return constraintFactory
            .forEach(Lesson::class.java)
            .join(Lesson::class.java,
                Joiners.equal(Lesson::subject),
                Joiners.equal(Lesson::studentGroup),
                Joiners.equal { lesson: Lesson -> lesson.timeslot?.dayOfWeek })
            .filter { lesson1: Lesson, lesson2: Lesson ->
                val between = Duration.between(
                    lesson1.timeslot?.endTime,
                    lesson2.timeslot?.startTime
                )
                !between.isNegative && between <= Duration.ofMinutes(30)
            }
            .penalize(HardSoftScore.ONE_SOFT)
            .justifyWith { lesson1: Lesson, lesson2: Lesson, _ ->
                StudentGroupSubjectVarietyJustification(lesson1.studentGroup, lesson1, lesson2)}
            .asConstraint("Student group subject variety")
    }

}

Create a school_timetabling_constraints function in src/hello_world/constraints.py:

from timefold.solver.score import (constraint_provider, HardSoftScore, Joiners,
                                   ConstraintFactory, Constraint)
from .domain import Lesson

@constraint_provider
def define_constraints(constraint_factory: ConstraintFactory):
    return [
        room_conflict(constraint_factory),
        teacher_conflict(constraint_factory),
        student_group_conflict(constraint_factory),

        # Soft constraints are only implemented in the timefold-quickstarts code
    ]


def room_conflict(constraint_factory: ConstraintFactory) -> Constraint:
    # A room can accommodate at most one lesson at the same time.
    return (constraint_factory
            # Select each pair of 2 different lessons ...
            .for_each_unique_pair(Lesson,
                                  # ... in the same timeslot ...
                                  Joiners.equal(lambda lesson: lesson.timeslot),
                                  # ... in the same room ...
                                  Joiners.equal(lambda lesson: lesson.room))
            # ... and penalize each pair with a hard weight.
            .penalize(HardSoftScore.ONE_HARD)
            .as_constraint("Room conflict"))


def teacher_conflict(constraint_factory: ConstraintFactory) -> Constraint:
    # A teacher can teach at most one lesson at the same time.
    return (constraint_factory
            .for_each_unique_pair(Lesson,
                                  Joiners.equal(lambda lesson: lesson.timeslot),
                                  Joiners.equal(lambda lesson: lesson.teacher))
            .penalize(HardSoftScore.ONE_HARD)
            .as_constraint("Teacher conflict"))


def student_group_conflict(constraint_factory: ConstraintFactory) -> Constraint:
    # A student can attend at most one lesson at the same time.
    return (constraint_factory
            .for_each_unique_pair(Lesson,
                                  Joiners.equal(lambda lesson: lesson.timeslot),
                                  Joiners.equal(lambda lesson: lesson.student_group))
            .penalize(HardSoftScore.ONE_HARD)
            .as_constraint("Student group conflict"))

The ConstraintProvider scales an order of magnitude better than the EasyScoreCalculator: O(n) instead of O(n²).

  • © 2025 Timefold BV
  • Timefold.ai
  • Documentation
  • Changelog
  • Send feedback
  • Privacy
  • Legal
    • Light mode
    • Dark mode
    • System default