Constraint-based arrangement engine for group rotation, timetabling, and any scheduling problem with rules.
Project description
quench
Arrange people into groups — or schedule things into time slots — while following rules you define.
Built for problems like:
- Group rotation — cycle 20 kids through 4 activity groups over several sessions, making sure the same kids don't keep ending up together
- Timetabling — assign 10 classes across rooms and periods so no student is double-booked and total walking distance is minimised
- Any arrangement problem — staff shifts with "do not pair" rules, sports teams with balance constraints, meeting rooms with attendee requirements
The engine finds the best arrangement it can given your rules. You define what "best" means.
Pure standard library. Zero external dependencies. Python 3.7+.
Install
pip install quench
Quick start
from quench import Entity, Slot, Scorer, Solver, hard, soft
people = [Entity("Alice"), Entity("Bob"), Entity("Carol"), Entity("Dan")]
groups = [Slot("Morning", capacity=2), Slot("Afternoon", capacity=2)]
def no_overflow(state):
return any(len(state.group(g)) > g.capacity for g in groups)
def prefer_separate(state):
return 1.0 if state[Entity("Alice")] == state[Entity("Bob")] else 0.0
scorer = Scorer([
hard(no_overflow, name="capacity"),
soft(prefer_separate, weight=5.0, name="alice_bob_apart"),
])
solver = Solver(people, groups, scorer, init="balanced")
result = solver.solve(seed=42)
print(result)
# Result(SA, feasible, cost=0.000, improvement=100.0%, iters=50,000)
for g in groups:
members = [e.id for e in result.best_state.group(g)]
print(f"{g.id}: {', '.join(members)}")
# Morning: Alice, Carol
# Afternoon: Bob, Dan
Constraints
Hard constraints must never be broken. The engine rejects any arrangement that violates one.
from quench import hard
def no_overflow(state):
# return True if the constraint is VIOLATED
return any(len(state.group(g)) > g.capacity for g in groups)
capacity_rule = hard(no_overflow, name="capacity")
Soft constraints are goals to minimise.
from quench import soft
def prefer_separate(state):
return 1.0 if state[Entity("Alice")] == state[Entity("Bob")] else 0.0
separation_rule = soft(prefer_separate, weight=5.0, name="alice_bob_apart")
weight controls relative importance. A weight of 5.0 means this constraint matters five times more than one with weight 1.0.
Multiple sessions with history
from quench import History
history = History()
for session in range(1, 4):
scorer = Scorer([
hard(no_overflow, name="capacity"),
history.as_soft_constraint(weight=3.0),
])
solver = Solver(people, groups, scorer, init="balanced")
result = solver.solve(seed=session)
history.update(result.best_state)
history.save("history.json") # save after a session
history = History.load("history.json") # restore in the next run
Reading the result
result.best_state # the final arrangement
result.best_cost # total penalty score — lower is better, 0.0 is perfect
result.feasible # True if no hard constraint was violated
result.improvement_pct # % cost reduction from the starting arrangement
result.converged # rough signal: did cost flatten in the final 20% of the run?
result.breakdown(scorer) # per-constraint cost breakdown as a dict
Solver options
Engine
Solver(..., engine="auto") # default: SA for <40 entities, PT for >=40
Solver(..., engine="sa") # Simulated Annealing — fast, good for smaller problems
Solver(..., engine="pt") # Parallel Tempering — better for large or complex problems
Starting arrangement
Solver(..., init="balanced") # round-robin distribution — recommended for equal-size groups
Solver(..., init="random") # random distribution — use with the move neighbor function
Tuning SA
Solver(..., engine="sa", T0=80.0, alpha=0.997, iterations=40_000)
Tuning PT
Solver(..., engine="pt", T_min=0.1, T_max=100.0, n_chains=6, iterations=50_000)
Neighbor function
| Function | Behaviour | Use when |
|---|---|---|
swap (default) |
Exchange two entities' slots | Equal-size groups required |
move |
Reassign one entity to a random slot | Variable group sizes |
mixed |
70% swap, 30% move | Soft capacity constraints |
make_swap_k(k) |
Swap k pairs simultaneously | Large problems, hot exploration |
from quench import move, make_swap_k
Solver(..., neighbor_fn=move)
Solver(..., neighbor_fn=make_swap_k(3))
Multiple independent solutions
results = solver.sample(n=5, seed=0) # 5 independent runs, sorted best-first
What the engine cannot guarantee
- It may not find the global optimum. SA and PT are heuristics — they search well but do not prove optimality.
- Hard constraints must be satisfiable. If your rules make every arrangement infeasible, the engine returns
result.feasible == False. - More iterations generally means better results. If you're not satisfied with the output, try increasing
iterationsor loweringalpha(SA) before anything else.
Source and examples
Full source, working examples, and documentation: github.com/OhmHobby/quench
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file quench-0.1.1.tar.gz.
File metadata
- Download URL: quench-0.1.1.tar.gz
- Upload date:
- Size: 33.2 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.14.0
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
4427cb7bc5736174f00ef8cfeffde21df576b0dbe13b2b321c583f0bd9bc88c5
|
|
| MD5 |
1ec2618c441d664478cd254432a6399a
|
|
| BLAKE2b-256 |
67c6f18bbc6af59eba96586bccec89c7787658cd3f61e879d1b8327e437dd521
|
File details
Details for the file quench-0.1.1-py3-none-any.whl.
File metadata
- Download URL: quench-0.1.1-py3-none-any.whl
- Upload date:
- Size: 22.8 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.14.0
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
f8c29344a2abdc1c3212c263a5f349007dbe26f6777299d59d6ace6a6ddf0a8d
|
|
| MD5 |
4e2b77cdb2db614a21b56ff23a256e16
|
|
| BLAKE2b-256 |
e6b321f97bd06153190f73153c0c168abf099a3e59da21bb184d90d5aa8a647a
|