This is the multi-page printable view of this section. Click here to print.

Return to the regular view of this page.

Getting Started

Install SolverForge and solve your first planning problem.

Get up and running with SolverForge in minutes.

Quick Start

  1. Installation - Set up Python, JDK, and install SolverForge
  2. Hello World - Build a simple school timetabling solver (CLI)
  3. Hello World with FastAPI - Add a REST API to your solver

What You’ll Learn

In the Hello World tutorial, you’ll build a school timetabling application that:

  • Assigns lessons to timeslots and rooms
  • Avoids scheduling conflicts (same teacher, room, or student group at the same time)
  • Optimizes for teacher preferences (room stability, consecutive lessons)

This introduces the core concepts you’ll use in any SolverForge application:

  • Planning entities - The things being scheduled (lessons)
  • Planning variables - The values being assigned (timeslot, room)
  • Constraints - The rules that define a valid solution
  • Solver configuration - How to run the optimization

1 - Installation

Set up Python, JDK, and install SolverForge.

Prerequisites

SolverForge requires:

  • Python 3.10 or higher (3.11 or 3.12 recommended)
  • JDK 17 or higher (for the optimization engine backend)

Check Python Version

python --version
# Python 3.11.0 or higher

If you need to install Python, visit python.org or use your system’s package manager.

Check JDK Version

java -version
# openjdk version "17.0.x" or higher

If you need to install a JDK:

  • macOS: brew install openjdk@17
  • Ubuntu/Debian: sudo apt install openjdk-17-jdk
  • Fedora: sudo dnf install java-17-openjdk
  • Windows: Download from Adoptium or Oracle

Make sure JAVA_HOME is set:

echo $JAVA_HOME
# Should output path to JDK installation

Install SolverForge

pip install solverforge-legacy

In a Virtual Environment

# Create virtual environment
python -m venv .venv

# Activate it
source .venv/bin/activate  # Linux/macOS
# or
.venv\Scripts\activate     # Windows

# Install SolverForge
pip install solverforge-legacy

Verify Installation

python -c "from solverforge_legacy.solver import SolverFactory; print('SolverForge installed successfully!')"

Project Setup

For a new project, create a pyproject.toml:

[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"

[project]
name = "my-solver-project"
version = "1.0.0"
requires-python = ">=3.10"
dependencies = [
    "solverforge-legacy == 1.24.1",
    "pytest == 8.2.2",  # For testing
]

Then install your project in development mode:

pip install -e .

IDE Setup

VS Code

Install the Python extension and configure your interpreter to use the virtual environment.

PyCharm

  1. Open your project
  2. Go to Settings > Project > Python Interpreter
  3. Select the virtual environment interpreter

Troubleshooting

JVM Not Found

If you see errors about JVM not found:

  1. Verify Java is installed: java -version
  2. Set JAVA_HOME environment variable
  3. Ensure JAVA_HOME/bin is in your PATH

Import Errors

If imports fail:

  1. Verify you’re in the correct virtual environment
  2. Re-install: pip install --force-reinstall solverforge-legacy

Memory Issues

For large problems, you may need to increase JVM memory. This is configured automatically, but you can adjust if needed.

Next Steps

Now that SolverForge is installed, follow the Hello World Tutorial to build your first solver.

2 - Hello World

Build a school timetabling solver from scratch.

In this tutorial, you’ll build a school timetabling application that assigns lessons to timeslots and rooms while avoiding conflicts.

The Problem

A school needs to schedule lessons:

  • Each lesson has a subject, teacher, and student group
  • Available timeslots (e.g., Monday 08:30, Monday 09:30, …)
  • Available rooms (Room A, Room B, Room C)

Constraints:

  • Hard: No room, teacher, or student group conflicts
  • Soft: Teachers prefer the same room, consecutive lessons

Project Structure

Create the following files:

hello_world/
├── domain.py       # Data model
├── constraints.py  # Constraint definitions
├── main.py         # Entry point
└── pyproject.toml  # Dependencies

Step 1: Define the Domain Model

Create domain.py with the problem facts and planning entities:

from dataclasses import dataclass, field
from datetime import time
from typing import Annotated

from solverforge_legacy.solver.domain import (
    planning_entity,
    planning_solution,
    PlanningId,
    PlanningVariable,
    PlanningEntityCollectionProperty,
    ProblemFactCollectionProperty,
    ValueRangeProvider,
    PlanningScore,
)
from solverforge_legacy.solver.score import HardSoftScore


# Problem facts (immutable data)
@dataclass
class Timeslot:
    day_of_week: str
    start_time: time
    end_time: time

    def __str__(self):
        return f"{self.day_of_week} {self.start_time.strftime('%H:%M')}"


@dataclass
class Room:
    name: str

    def __str__(self):
        return self.name


# Planning entity (modified by the solver)
@planning_entity
@dataclass
class Lesson:
    id: Annotated[str, PlanningId]
    subject: str
    teacher: str
    student_group: str
    # Planning variables - assigned by the solver
    timeslot: Annotated[Timeslot | None, PlanningVariable] = field(default=None)
    room: Annotated[Room | None, PlanningVariable] = field(default=None)


# Planning solution (container for all data)
@planning_solution
@dataclass
class Timetable:
    id: str
    # Problem facts with value range providers
    timeslots: Annotated[list[Timeslot], ProblemFactCollectionProperty, ValueRangeProvider]
    rooms: Annotated[list[Room], ProblemFactCollectionProperty, ValueRangeProvider]
    # Planning entities
    lessons: Annotated[list[Lesson], PlanningEntityCollectionProperty]
    # Score calculated by constraints
    score: Annotated[HardSoftScore, PlanningScore] = field(default=None)

Key Concepts

  • @planning_entity marks Lesson as something the solver will modify
  • PlanningVariable marks timeslot and room as values to assign
  • @planning_solution marks Timetable as the container
  • ValueRangeProvider tells the solver which values are available

Step 2: Define Constraints

Create constraints.py with the scoring rules:

from solverforge_legacy.solver.score import (
    constraint_provider,
    ConstraintFactory,
    Constraint,
    Joiners,
    HardSoftScore,
)

from .domain import Lesson


@constraint_provider
def define_constraints(constraint_factory: ConstraintFactory) -> list[Constraint]:
    return [
        # Hard constraints
        room_conflict(constraint_factory),
        teacher_conflict(constraint_factory),
        student_group_conflict(constraint_factory),
        # Soft constraints
        teacher_room_stability(constraint_factory),
    ]


def room_conflict(constraint_factory: ConstraintFactory) -> Constraint:
    """A room can accommodate 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.room),
        )
        .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 group 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")
    )


def teacher_room_stability(constraint_factory: ConstraintFactory) -> Constraint:
    """A teacher prefers to teach in a single room."""
    return (
        constraint_factory
        .for_each_unique_pair(
            Lesson,
            Joiners.equal(lambda lesson: lesson.teacher),
        )
        .filter(lambda lesson1, lesson2: lesson1.room != lesson2.room)
        .penalize(HardSoftScore.ONE_SOFT)
        .as_constraint("Teacher room stability")
    )

Constraint Stream Pattern

Each constraint follows this pattern:

  1. Select entities with for_each() or for_each_unique_pair()
  2. Filter to matching cases with Joiners or .filter()
  3. Penalize (or reward) with a score impact
  4. Name the constraint with as_constraint()

Step 3: Configure and Run the Solver

Create main.py:

from datetime import time

from solverforge_legacy.solver import SolverFactory
from solverforge_legacy.solver.config import (
    SolverConfig,
    ScoreDirectorFactoryConfig,
    TerminationConfig,
    Duration,
)

from .domain import Timetable, Timeslot, Room, Lesson
from .constraints import define_constraints


def main():
    # Configure the solver
    solver_config = SolverConfig(
        solution_class=Timetable,
        entity_class_list=[Lesson],
        score_director_factory_config=ScoreDirectorFactoryConfig(
            constraint_provider_function=define_constraints
        ),
        termination_config=TerminationConfig(
            spent_limit=Duration(seconds=5)
        ),
    )

    # Create the solver
    solver_factory = SolverFactory.create(solver_config)
    solver = solver_factory.build_solver()

    # Generate a problem
    problem = generate_demo_data()

    # Solve it!
    solution = solver.solve(problem)

    # Print the result
    print_timetable(solution)


def generate_demo_data() -> Timetable:
    """Create a small demo problem."""
    timeslots = [
        Timeslot("MONDAY", time(8, 30), time(9, 30)),
        Timeslot("MONDAY", time(9, 30), time(10, 30)),
        Timeslot("MONDAY", time(10, 30), time(11, 30)),
        Timeslot("TUESDAY", time(8, 30), time(9, 30)),
        Timeslot("TUESDAY", time(9, 30), time(10, 30)),
    ]

    rooms = [
        Room("Room A"),
        Room("Room B"),
        Room("Room C"),
    ]

    lessons = [
        Lesson("1", "Math", "A. Turing", "9th grade"),
        Lesson("2", "Physics", "M. Curie", "9th grade"),
        Lesson("3", "Chemistry", "M. Curie", "9th grade"),
        Lesson("4", "Biology", "C. Darwin", "9th grade"),
        Lesson("5", "History", "I. Jones", "9th grade"),
        Lesson("6", "Math", "A. Turing", "10th grade"),
        Lesson("7", "Physics", "M. Curie", "10th grade"),
        Lesson("8", "Geography", "C. Darwin", "10th grade"),
    ]

    return Timetable("demo", timeslots, rooms, lessons)


def print_timetable(timetable: Timetable) -> None:
    """Print the solution in a readable format."""
    print(f"\nScore: {timetable.score}\n")

    for lesson in timetable.lessons:
        print(f"{lesson.subject} ({lesson.teacher}, {lesson.student_group})")
        print(f"  -> {lesson.timeslot} in {lesson.room}")
        print()


if __name__ == "__main__":
    main()

Step 4: Run It

python -m hello_world.main

You should see output like:

Score: 0hard/-3soft

Math (A. Turing, 9th grade)
  -> MONDAY 08:30 in Room A

Physics (M. Curie, 9th grade)
  -> MONDAY 09:30 in Room B

Chemistry (M. Curie, 9th grade)
  -> TUESDAY 08:30 in Room B

...

A score of 0hard means all hard constraints are satisfied (no conflicts). The negative soft score indicates room for optimization of preferences.

Understanding the Output

  • 0hard = No conflicts (feasible solution!)
  • -3soft = 3 soft constraint violations (teachers using multiple rooms)

The solver found a valid timetable where:

  • No room has two lessons at the same time
  • No teacher teaches two lessons at the same time
  • No student group has two lessons at the same time

Next Steps

3 - Hello World with FastAPI

Add a REST API to your school timetabling solver.

This tutorial extends the Hello World example by adding a REST API using FastAPI. This is closer to how you’d deploy a solver in production.

Prerequisites

  • Completed the Hello World tutorial
  • FastAPI and Uvicorn installed:
pip install fastapi uvicorn

Project Structure

Extend your project:

hello_world/
├── domain.py           # Same as before
├── constraints.py      # Same as before
├── main.py             # CLI version (optional)
├── rest_api.py         # NEW: FastAPI application
└── pyproject.toml      # Add fastapi, uvicorn

Step 1: Update Dependencies

Add FastAPI to your pyproject.toml:

[project]
dependencies = [
    "solverforge-legacy == 1.24.1",
    "fastapi >= 0.100.0",
    "uvicorn >= 0.23.0",
    "pytest == 8.2.2",
]

Step 2: Create the REST API

Create rest_api.py:

import uuid
from contextlib import asynccontextmanager

from fastapi import FastAPI, HTTPException
from pydantic import BaseModel
from datetime import time

from solverforge_legacy.solver import SolverFactory, SolverManager
from solverforge_legacy.solver.config import (
    SolverConfig,
    ScoreDirectorFactoryConfig,
    TerminationConfig,
    Duration,
)

from .domain import Timetable, Timeslot, Room, Lesson
from .constraints import define_constraints


# Pydantic models for API validation
class TimeslotDTO(BaseModel):
    day_of_week: str
    start_time: str
    end_time: str

    def to_domain(self) -> Timeslot:
        return Timeslot(
            self.day_of_week,
            time.fromisoformat(self.start_time),
            time.fromisoformat(self.end_time),
        )


class RoomDTO(BaseModel):
    name: str

    def to_domain(self) -> Room:
        return Room(self.name)


class LessonDTO(BaseModel):
    id: str
    subject: str
    teacher: str
    student_group: str
    timeslot: TimeslotDTO | None = None
    room: RoomDTO | None = None

    def to_domain(self, timeslots: list[Timeslot], rooms: list[Room]) -> Lesson:
        ts = None
        if self.timeslot:
            ts = next(
                (t for t in timeslots if t.day_of_week == self.timeslot.day_of_week
                 and t.start_time.isoformat() == self.timeslot.start_time),
                None
            )
        rm = None
        if self.room:
            rm = next((r for r in rooms if r.name == self.room.name), None)

        return Lesson(self.id, self.subject, self.teacher, self.student_group, ts, rm)


class TimetableDTO(BaseModel):
    id: str
    timeslots: list[TimeslotDTO]
    rooms: list[RoomDTO]
    lessons: list[LessonDTO]
    score: str | None = None

    def to_domain(self) -> Timetable:
        timeslots = [ts.to_domain() for ts in self.timeslots]
        rooms = [r.to_domain() for r in self.rooms]
        lessons = [l.to_domain(timeslots, rooms) for l in self.lessons]
        return Timetable(self.id, timeslots, rooms, lessons)

    @classmethod
    def from_domain(cls, timetable: Timetable) -> "TimetableDTO":
        return cls(
            id=timetable.id,
            timeslots=[
                TimeslotDTO(
                    day_of_week=ts.day_of_week,
                    start_time=ts.start_time.isoformat(),
                    end_time=ts.end_time.isoformat(),
                )
                for ts in timetable.timeslots
            ],
            rooms=[RoomDTO(name=r.name) for r in timetable.rooms],
            lessons=[
                LessonDTO(
                    id=l.id,
                    subject=l.subject,
                    teacher=l.teacher,
                    student_group=l.student_group,
                    timeslot=TimeslotDTO(
                        day_of_week=l.timeslot.day_of_week,
                        start_time=l.timeslot.start_time.isoformat(),
                        end_time=l.timeslot.end_time.isoformat(),
                    ) if l.timeslot else None,
                    room=RoomDTO(name=l.room.name) if l.room else None,
                )
                for l in timetable.lessons
            ],
            score=str(timetable.score) if timetable.score else None,
        )


# Global solver manager
solver_manager: SolverManager | None = None
solutions: dict[str, Timetable] = {}


@asynccontextmanager
async def lifespan(app: FastAPI):
    """Initialize solver manager on startup."""
    global solver_manager

    solver_config = SolverConfig(
        solution_class=Timetable,
        entity_class_list=[Lesson],
        score_director_factory_config=ScoreDirectorFactoryConfig(
            constraint_provider_function=define_constraints
        ),
        termination_config=TerminationConfig(
            spent_limit=Duration(seconds=30)
        ),
    )

    solver_factory = SolverFactory.create(solver_config)
    solver_manager = SolverManager.create(solver_factory)

    yield

    # Cleanup on shutdown
    if solver_manager:
        solver_manager.close()


app = FastAPI(
    title="School Timetabling API",
    description="Optimize school timetables using SolverForge",
    lifespan=lifespan,
)


@app.post("/timetables", response_model=str)
async def submit_problem(timetable_dto: TimetableDTO) -> str:
    """Submit a timetabling problem for solving."""
    job_id = str(uuid.uuid4())
    problem = timetable_dto.to_domain()

    def on_best_solution(solution: Timetable):
        solutions[job_id] = solution

    solver_manager.solve_and_listen(
        job_id,
        lambda _: problem,
        on_best_solution,
    )

    return job_id


@app.get("/timetables/{job_id}", response_model=TimetableDTO)
async def get_solution(job_id: str) -> TimetableDTO:
    """Get the current best solution for a job."""
    if job_id not in solutions:
        raise HTTPException(status_code=404, detail="Job not found")

    return TimetableDTO.from_domain(solutions[job_id])


@app.delete("/timetables/{job_id}")
async def stop_solving(job_id: str):
    """Stop solving a problem early."""
    solver_manager.terminate_early(job_id)
    return {"status": "terminated"}


@app.get("/demo-data", response_model=TimetableDTO)
async def get_demo_data() -> TimetableDTO:
    """Get demo problem data for testing."""
    timeslots = [
        Timeslot("MONDAY", time(8, 30), time(9, 30)),
        Timeslot("MONDAY", time(9, 30), time(10, 30)),
        Timeslot("TUESDAY", time(8, 30), time(9, 30)),
        Timeslot("TUESDAY", time(9, 30), time(10, 30)),
    ]
    rooms = [Room("Room A"), Room("Room B")]
    lessons = [
        Lesson("1", "Math", "A. Turing", "9th grade"),
        Lesson("2", "Physics", "M. Curie", "9th grade"),
        Lesson("3", "History", "I. Jones", "9th grade"),
        Lesson("4", "Math", "A. Turing", "10th grade"),
    ]

    return TimetableDTO.from_domain(Timetable("demo", timeslots, rooms, lessons))

Step 3: Run the API

uvicorn hello_world.rest_api:app --reload

The API is now running at http://localhost:8000.

Step 4: Test the API

Get Demo Data

curl http://localhost:8000/demo-data

Submit a Problem

# Get demo data and submit it for solving
curl http://localhost:8000/demo-data | curl -X POST \
  -H "Content-Type: application/json" \
  -d @- \
  http://localhost:8000/timetables

This returns a job ID like "a1b2c3d4-...".

Check the Solution

curl http://localhost:8000/timetables/{job_id}

Stop Solving Early

curl -X DELETE http://localhost:8000/timetables/{job_id}

API Documentation

FastAPI automatically generates interactive API docs:

  • Swagger UI: http://localhost:8000/docs
  • ReDoc: http://localhost:8000/redoc

Architecture Notes

SolverManager

SolverManager handles concurrent solving jobs:

  • Each job runs in its own thread
  • Multiple problems can be solved simultaneously
  • Solutions are updated as the solver improves them

Pydantic Models

We use separate Pydantic DTOs for:

  • API request/response validation
  • JSON serialization
  • Decoupling API schema from domain model

Production Considerations

For production deployments:

  1. Persistence: Store solutions in a database
  2. Scaling: Use a message queue for distributed solving
  3. Monitoring: Add logging and metrics
  4. Security: Add authentication and rate limiting

Next Steps