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

Return to the regular view of this page.

Reference

API reference and frequently asked questions.

Quick reference guides and answers to common questions.

Topics

  • API Summary - Quick reference for key classes and functions
  • FAQ - Frequently asked questions

Key Imports

# Domain modeling
from solverforge_legacy.solver.domain import (
    planning_entity,
    planning_solution,
    PlanningId,
    PlanningVariable,
    PlanningListVariable,
    PlanningEntityCollectionProperty,
    ProblemFactCollectionProperty,
    ValueRangeProvider,
    PlanningScore,
    PlanningPin,
    InverseRelationShadowVariable,
    PreviousElementShadowVariable,
    NextElementShadowVariable,
    CascadingUpdateShadowVariable,
)

# Constraints
from solverforge_legacy.solver.score import (
    constraint_provider,
    ConstraintFactory,
    Constraint,
    Joiners,
    ConstraintCollectors,
    HardSoftScore,
    HardMediumSoftScore,
    SimpleScore,
)

# Solver
from solverforge_legacy.solver import (
    SolverFactory,
    SolverManager,
    SolutionManager,
    ProblemChange,
)

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

Score Types

Score TypeLevelsUse Case
SimpleScore1Single optimization objective
HardSoftScore2Feasibility (hard) + optimization (soft)
HardMediumSoftScore3Hard + important preferences + nice-to-have
BendableScoreNCustom number of levels
*DecimalScore-Decimal precision variants

1 - API Summary

Quick reference for SolverForge Python API.

Quick reference for commonly used SolverForge APIs.

Domain Decorators

from solverforge_legacy.solver.domain import (
    planning_entity,
    planning_solution,
)
DecoratorPurpose
@planning_entityMark a class as a planning entity
@planning_solutionMark a class as the planning solution

Type Annotations

from solverforge_legacy.solver.domain import (
    PlanningId,
    PlanningVariable,
    PlanningListVariable,
    PlanningEntityCollectionProperty,
    ProblemFactCollectionProperty,
    ValueRangeProvider,
    PlanningScore,
    PlanningPin,
    PlanningPinToIndex,
)
AnnotationUse WithPurpose
PlanningIdEntity fieldUnique identifier
PlanningVariableEntity fieldVariable to optimize
PlanningListVariableEntity fieldOrdered list of entities
PlanningEntityCollectionPropertySolution fieldCollection of entities
ProblemFactCollectionPropertySolution fieldImmutable input data
ValueRangeProviderSolution fieldPossible values for variables
PlanningScoreSolution fieldWhere score is stored
PlanningPinEntity fieldLock entity assignment
PlanningPinToIndexEntity fieldLock list position

Usage Pattern

from typing import Annotated
from dataclasses import dataclass, field

@planning_entity
@dataclass
class Lesson:
    id: Annotated[str, PlanningId]
    timeslot: Annotated[Timeslot | None, PlanningVariable] = field(default=None)

Shadow Variable Annotations

from solverforge_legacy.solver.domain import (
    InverseRelationShadowVariable,
    PreviousElementShadowVariable,
    NextElementShadowVariable,
    CascadingUpdateShadowVariable,
)
AnnotationPurpose
InverseRelationShadowVariableBack-reference to list owner
PreviousElementShadowVariablePrevious element in list
NextElementShadowVariableNext element in list
CascadingUpdateShadowVariableComputed value that cascades

Score Types

from solverforge_legacy.solver.score import (
    SimpleScore,
    HardSoftScore,
    HardMediumSoftScore,
    HardSoftDecimalScore,
)
TypeLevelsExample
SimpleScore1-5
HardSoftScore2-2hard/-15soft
HardMediumSoftScore3-1hard/-3medium/-10soft
HardSoftDecimalScore2 (decimal)-2hard/-15.5soft

Common Operations

score = HardSoftScore.of(-2, -15)
score.hard_score      # -2
score.soft_score      # -15
score.is_feasible     # False (hard_score < 0)

# Constants
HardSoftScore.ZERO
HardSoftScore.ONE_HARD
HardSoftScore.ONE_SOFT

Constraint Streams

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

ConstraintFactory Methods

MethodPurpose
for_each(Class)Start stream with all instances
for_each_unique_pair(Class, *Joiners)All unique pairs
for_each_including_unassigned(Class)Include entities with null variables

Stream Operations

MethodPurpose
.filter(predicate)Filter elements
.join(Class, *Joiners)Join with another class
.if_exists(Class, *Joiners)Keep if matching exists
.if_not_exists(Class, *Joiners)Keep if no matching exists
.group_by(groupKey, collector)Group and aggregate
.flatten_last(mapper)Expand collection
.map(mapper)Transform elements
.complement(Class, filler)Add missing elements

Terminal Operations

MethodPurpose
.penalize(Score)Add penalty
.penalize(Score, weigher)Weighted penalty
.reward(Score)Add reward
.reward(Score, weigher)Weighted reward
.penalize_decimal(Score, weigher)Decimal penalty
.as_constraint(name)Name the constraint

Joiners

from solverforge_legacy.solver.score import Joiners
JoinerPurpose
Joiners.equal(extractor)Match on equality
Joiners.equal(extractorA, extractorB)Match properties
Joiners.less_than(extractorA, extractorB)A < B
Joiners.less_than_or_equal(extractorA, extractorB)A <= B
Joiners.greater_than(extractorA, extractorB)A > B
Joiners.greater_than_or_equal(extractorA, extractorB)A >= B
Joiners.overlapping(startA, endA, startB, endB)Time overlap
Joiners.overlapping(startA, endA)Same start/end extractors
Joiners.filtering(predicate)Custom filter

Collectors

from solverforge_legacy.solver.score import ConstraintCollectors
CollectorResult
count()Number of items
count_distinct(mapper)Distinct count
sum(mapper)Sum of values
min(mapper)Minimum value
max(mapper)Maximum value
average(mapper)Average value
to_list(mapper)Collect to list
to_set(mapper)Collect to set
load_balance(keyMapper, loadMapper)Fairness measure
compose(c1, c2, combiner)Combine collectors

Solver Configuration

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

SolverConfig

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)
    ),
)

TerminationConfig Options

PropertyTypePurpose
spent_limitDurationTime limit
unimproved_spent_limitDurationTime without improvement
best_score_limitstrTarget score
best_score_feasibleboolStop when feasible

Solver API

from solverforge_legacy.solver import (
    SolverFactory,
    SolverManager,
    SolutionManager,
    SolverStatus,
)

SolverFactory

solver_factory = SolverFactory.create(config)
solver = solver_factory.build_solver()
solution = solver.solve(problem)

SolverManager

solver_manager = SolverManager.create(solver_factory)

# Async solving
solver_manager.solve_and_listen(
    problem_id,
    problem_finder=lambda _: problem,
    best_solution_consumer=on_best_solution,
)

# Control
solver_manager.terminate_early(problem_id)
status = solver_manager.get_solver_status(problem_id)
solver_manager.close()

SolverStatus

StatusMeaning
NOT_SOLVINGNot started
SOLVING_ACTIVECurrently solving
SOLVING_SCHEDULEDQueued

SolutionManager

solution_manager = SolutionManager.create(solver_factory)
analysis = solution_manager.analyze(solution)
score = solution_manager.update(solution)

Duration

from solverforge_legacy.solver.config import Duration

Duration(seconds=30)
Duration(minutes=5)
Duration(hours=1)

Event Listeners

from solverforge_legacy.solver import BestSolutionChangedEvent

def on_best_solution(event: BestSolutionChangedEvent):
    print(f"Score: {event.new_best_score}")
    print(f"Time: {event.time_spent}")

Problem Changes

from solverforge_legacy.solver import ProblemChange

class AddLessonChange(ProblemChange):
    def __init__(self, lesson: Lesson):
        self.lesson = lesson

    def do_change(self, solution: Timetable, problem_change_director):
        problem_change_director.add_entity(
            self.lesson,
            lambda l: solution.lessons.append(l)
        )

Import Summary

# Domain modeling
from solverforge_legacy.solver.domain import (
    planning_entity,
    planning_solution,
    PlanningId,
    PlanningVariable,
    PlanningListVariable,
    PlanningEntityCollectionProperty,
    ProblemFactCollectionProperty,
    ValueRangeProvider,
    PlanningScore,
    PlanningPin,
    InverseRelationShadowVariable,
    PreviousElementShadowVariable,
    NextElementShadowVariable,
    CascadingUpdateShadowVariable,
)

# Scores and constraints
from solverforge_legacy.solver.score import (
    constraint_provider,
    ConstraintFactory,
    Constraint,
    Joiners,
    ConstraintCollectors,
    HardSoftScore,
    HardMediumSoftScore,
    SimpleScore,
)

# Solver
from solverforge_legacy.solver import (
    SolverFactory,
    SolverManager,
    SolutionManager,
    SolverStatus,
    BestSolutionChangedEvent,
    ProblemChange,
)

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

2 - FAQ

Frequently asked questions about SolverForge.

General

What is SolverForge?

SolverForge is a Python constraint solver for planning and scheduling optimization problems. It uses constraint streams to define rules and metaheuristic algorithms to find high-quality solutions.

What problems can SolverForge solve?

SolverForge excels at:

  • Employee scheduling: Shift assignment with skills, availability, fairness
  • Vehicle routing: Route optimization with capacity, time windows
  • School timetabling: Class scheduling with room and teacher constraints
  • Meeting scheduling: Room booking with attendee conflicts
  • Task assignment: Job shop, bin packing, resource allocation

How is SolverForge licensed?

SolverForge is open source software released under the Apache License 2.0. This allows commercial use, modification, and distribution.

Installation

What are the requirements?

  • Python 3.10 or later
  • JDK 17 or later (SolverForge uses the JVM for solving)

Why does SolverForge need Java?

SolverForge’s solving engine runs on the JVM for performance. The Python API communicates with the JVM transparently via JPype.

How do I install it?

pip install solverforge-legacy

Make sure JAVA_HOME is set or Java is on your PATH.

Modeling

What’s the difference between problem facts and planning entities?

TypeChanges During SolvingExample
Problem factsNo (input data)Rooms, Timeslots, Employees
Planning entitiesYes (variables assigned)Lessons, Shifts, Visits

When should I use PlanningVariable vs PlanningListVariable?

TypeUse CaseExample
PlanningVariableAssign one valueLesson → Timeslot
PlanningListVariableOrdered list of entitiesVehicle → list of Visits

Can I use Pydantic instead of dataclasses?

Yes. Both dataclasses and Pydantic models work. The quickstart examples show both patterns.

How do I pin (lock) an assignment?

Add PlanningPin to a boolean field:

@planning_entity
class Lesson:
    pinned: Annotated[bool, PlanningPin] = False

Set pinned=True to prevent the solver from changing that entity’s variables.

Constraints

What’s the difference between hard and soft constraints?

TypeMeaningExample
HardMust not violateNo room conflicts
SoftPrefer to satisfyTeacher prefers certain room

Hard constraints define feasibility. Soft constraints define quality.

Why use for_each_unique_pair instead of for_each + join?

for_each_unique_pair is more efficient and avoids counting conflicts twice:

# Good - each pair counted once
constraint_factory.for_each_unique_pair(
    Lesson,
    Joiners.equal(lambda l: l.timeslot),
    Joiners.equal(lambda l: l.room),
)

# Less efficient - (A,B) and (B,A) both matched
constraint_factory.for_each(Lesson).join(Lesson, ...)

How do I debug a constraint?

  1. Use SolutionManager.analyze() to see the score breakdown:
analysis = solution_manager.analyze(solution)
for c in analysis.constraint_analyses():
    print(f"{c.constraint_name}: {c.score}")
  1. Examine individual matches:
for match in constraint_analysis.matches():
    print(f"  {match.justification}")

Why is my score always infeasible?

Common causes:

  • Not enough resources (rooms, timeslots, employees) for entities
  • Conflicting hard constraints that can’t all be satisfied
  • Uninitialized entities (variables still None)

Try:

  • Increasing termination time
  • Relaxing some hard constraints to soft
  • Adding more resources

Solving

How long should I let the solver run?

Depends on problem size and complexity:

Problem SizeTypical Time
Small (< 100 entities)10-60 seconds
Medium (100-1000 entities)1-10 minutes
Large (> 1000 entities)10+ minutes

Use benchmarking to find the optimal time for your problem.

Why isn’t the score improving?

Possible causes:

  • Stuck in local optimum (try different algorithm)
  • All hard constraints satisfied (now optimizing soft)
  • Constraints are too restrictive

Try:

  • Simulated Annealing or Late Acceptance instead of Tabu Search
  • Longer termination time
  • Review constraint design

How do I stop solving early?

With Solver:

# External termination (from another thread)
solver.terminate_early()

With SolverManager:

solver_manager.terminate_early(problem_id)

Can I get progress updates during solving?

Yes, use SolverManager with a listener:

solver_manager.solve_and_listen(
    problem_id,
    problem_finder=lambda _: problem,
    best_solution_consumer=lambda solution: print(f"Score: {solution.score}"),
)

Performance

How do I make constraints faster?

  1. Use Joiners instead of filter():
# Fast - indexing
Joiners.equal(lambda lesson: lesson.timeslot)

# Slower - Python filter
.filter(lambda l1, l2: l1.timeslot == l2.timeslot)
  1. Cache computed values in entity fields
  2. Avoid expensive operations in lambdas

How do I scale to larger problems?

  • Increase termination time
  • Use more efficient constraints
  • Consider partitioning large problems
  • Use PlanningListVariable for routing problems

Should I use multiple threads?

The solver is single-threaded by design for score calculation consistency. Use SolverManager to solve multiple problems concurrently.

Integration

Can I use SolverForge with FastAPI?

Yes! See the FastAPI Integration guide. Key pattern:

@asynccontextmanager
async def lifespan(app: FastAPI):
    global solver_manager
    solver_manager = SolverManager.create(solver_factory)
    yield
    solver_manager.close()

How do I serialize solutions to JSON?

Use Pydantic models or dataclasses.asdict():

# With dataclasses
import json
from dataclasses import asdict

json.dumps(asdict(solution))

# With Pydantic
solution.model_dump_json()

See Serialization for handling references.

Can I save and load solutions?

Yes, serialize to JSON and deserialize back:

# Save
with open("solution.json", "w") as f:
    json.dump(solution_to_dict(solution), f)

# Load
with open("solution.json") as f:
    problem = dict_to_solution(json.load(f))

Troubleshooting

“No JVM shared library file (libjvm.so) found”

Java isn’t installed or JAVA_HOME isn’t set:

# Check Java
java -version

# Set JAVA_HOME (example for Linux)
export JAVA_HOME=/usr/lib/jvm/java-17-openjdk

“Score corruption detected”

A constraint is producing inconsistent scores. Common causes:

  • Non-deterministic lambdas
  • External state changes
  • Incorrect shadow variable updates

Run with RUST_LOG=debug to see details.

“OutOfMemoryError” in the JVM

Increase JVM heap:

export JAVA_TOOL_OPTIONS="-Xmx4g"

More Help