The Algorithm Contract¶
Your algorithm is the meaty part of your scheduler. BeeKeeper handles ingestion, rule evaluation, and dispatch; your Algorithm.run decides who goes where.
The signature¶
class MyAlgorithm[TEntity: AnyEntity, TAllocReq: AnyRequest](
Algorithm[TEntity, TAllocReq],
):
def run(
self,
allocations: Iterable[TAllocReq],
entities: Iterable[TEntity],
candidates: Mapping[int, list[Candidate[TEntity]]],
rules: Iterable[StatefulRule[TEntity, TAllocReq]],
) -> AssignmentState[TEntity, TAllocReq]:
...
What you get¶
allocations: the raw allocation requests in input order. The same iterable the input adapter produced.entities: the raw entity list. Useful when an algorithm wants to consider entities outside the candidate map (rare).candidates: the pruned, scored candidate map from stage 2, keyed byid(allocation). This is what most algorithms iterate over. EachCandidatecarries an entity reference and an aggregated score.rules: the stateful rules. You're responsible for consulting these as you assign; the framework doesn't run them for you.
What you must return¶
An AssignmentState[TEntity, TAllocReq] carrying every successful assignment. Use state.add_assignment(Assignment(allocation=..., assigned_entities=(...))) to record an assignment. The state's assignments property is what output adapters read.
What you must guarantee¶
- Honor the candidate map. Don't assign an entity that's not in the candidate list for an allocation. The map represents the framework's pre-pruning of incompatibilities; bypassing it skips your domain's preliminary rules.
- Honor
required_count. If an allocation needs N entities, assign exactly N or skip the allocation entirely. Don't half-fill. - Consult stateful rules. Before adding a
Assignment, every stateful rule must evaluatecompatible=True. The framework does not double-check.
Failing explicitly¶
If your algorithm can't produce a result it considers complete (a constraint solver returns INFEASIBLE; a backtracking search exhausts without filling every allocation), raise IncompleteSolutionError rather than returning a partial. The framework's flow stage walks through an algorithm chain (passed via BeeKeeper(algorithm=[primary, fallback, ...])); if your algorithm raises, the next algorithm in the chain runs from scratch.
from beekeeper import IncompleteSolutionError
# Inside your run(...) method:
if not solution_complete:
raise IncompleteSolutionError(
"MyAlgorithm couldn't satisfy the stateful rules with the given candidates",
partial_state=state, # optional — for caller inspection
)
LoadBalancingAssignmentAlgorithm never raises; it returns whatever schedule it could fill. That makes it a good chain terminator: algorithm=[backtracking, load_balancing] tries the smarter algorithm first and falls back to load-balancing on failure.
What you may do (but don't have to)¶
- Use the score.
Candidate.scoreis the geometric mean of the soft rules' verdicts. Highest-scored is the algorithm's hint of "best candidate." A simple algorithm picks the top-scored. A constraint solver might use it as a heuristic. - Iterate allocations in any order. Input order is one option. Most-constrained-first, by-date, by-priority — your call.
- Skip allocations. Allocations that can't be filled (empty candidate list, all candidates fail stateful rules) just don't get an
Assignment. The output adapter can compute the diff if needed. - Use
state.get_assignments_done_by(entity)to look up an entity's existing assignments while deciding the next one — this is what stateful rules typically do.
A reference implementation¶
LoadBalancingAssignmentAlgorithm is a short reference: for each allocation, sort candidates by score / (1 + load) where load is the count of allocations the entity already has, then pick the top compatible candidates until required_count is met. The load penalty disperses work across the entity pool instead of concentrating it on whoever has the raw highest score. It's not optimal — it doesn't backtrack, doesn't optimize globally — but it gives every example in this docs site a runnable algorithm without writing one from scratch.
For nontrivial scheduling, the same Algorithm contract is implemented by BacktrackingAssignmentAlgorithm and OrToolsAssignmentAlgorithm — see the bundled implementations table.