Getting Started
Install SolverForge and solve your first planning problem.
Get up and running with SolverForge in minutes.
Quick Start
- Installation - Set up Python, JDK, and install SolverForge
- Hello World - Build a simple school timetabling solver (CLI)
- 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
Using pip (Recommended)
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:
IDE Setup
VS Code
Install the Python extension and configure your interpreter to use the virtual environment.
PyCharm
- Open your project
- Go to Settings > Project > Python Interpreter
- Select the virtual environment interpreter
Troubleshooting
JVM Not Found
If you see errors about JVM not found:
- Verify Java is installed:
java -version - Set
JAVA_HOME environment variable - Ensure
JAVA_HOME/bin is in your PATH
Import Errors
If imports fail:
- Verify you’re in the correct virtual environment
- 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 modifyPlanningVariable marks timeslot and room as values to assign@planning_solution marks Timetable as the containerValueRangeProvider 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:
- Select entities with
for_each() or for_each_unique_pair() - Filter to matching cases with
Joiners or .filter() - Penalize (or reward) with a score impact
- Name the constraint with
as_constraint()
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:
- Persistence: Store solutions in a database
- Scaling: Use a message queue for distributed solving
- Monitoring: Add logging and metrics
- Security: Add authentication and rate limiting
Next Steps