Modelling Examples#
Conventions#
Hermax solves minimization problems.
model.obj[w] += litadds a soft unit clause[lit]of weightw.A soft unit clause
[lit]pays costwwhenlitis false.To penalize a Boolean decision
xwhen it is true, usemodel.obj[w] += ~x.
Example 01: Toy Soft Clauses#
Introduce the basic MaxSAT workflow with hard clauses and weighted soft unit clauses.
New primitives#
hermax.model.Modelhermax.model.Model.bool()Hard constraints with
model &=Soft constraints with
model.obj[w] +=hermax.model.Model.solve()
Model#
This is the smallest useful weighted partial MaxSAT instance: a hard CNF core plus a weighted set of soft preferences. It exposes Hermax’s literal/soft-clause polarity convention without introducing PB encoders or finite-domain variables.
Code#
from hermax.model import Model
m = Model()
a = m.bool("a")
b = m.bool("b")
c = m.bool("c")
m &= (a | b) # a OR b: at least one must be true
m &= (~a | c) # (NOT a) OR c: if a then c
m.obj[3] += a # soft clause [a]: pay 3 if a is false
m.obj[2] += ~b # soft clause [~b]: pay 2 if b is true
m.obj[1] += c # soft clause [c]: pay 1 if c is false
r = m.solve()
print("status:", r.status)
print("cost:", r.cost)
print("a,b,c:", r[a], r[b], r[c])
Output#
This output shows one optimal assignment and the resulting weighted MaxSAT cost.
$ python examples/model/01_toy_soft_clauses.py
status: optimum
cost: 0
a,b,c: True False True
Example 02: Knapsack#
Show a weighted pseudo-Boolean capacity constraint and a MaxSAT objective that maximizes profit by minimizing penalties for not selecting items.
New primitives#
hermax.model.BoolVectorPB expressions over booleans (
sum(w_i * x_i) <= C)
Model#
This is the first pseudo-boolean: one weighted capacity inequality and a linear profit objective. Knapsack is one of Karp’s original NP-complete problems [1], and it is also a standard optimization reference problem in its own right [2].
The code uses soft unit clauses [x_i] so the cost is paid when x_i=0, which is exactly the transformed minimization objective.
Problem#
Code#
from hermax.model import Model
m = Model()
# Items: (weight, profit)
items = [(2, 6), (3, 8), (4, 9), (5, 10)]
take = m.bool_vector("take", len(items))
capacity = 8
m &= sum(w * take[i] for i, (w, _) in enumerate(items)) <= capacity
for i, (_, profit) in enumerate(items):
m.obj[profit] += take[i]
r = m.solve()
chosen = [i for i in range(len(items)) if r[take[i]]]
total_weight = sum(items[i][0] for i in chosen)
total_profit = sum(items[i][1] for i in chosen)
print("status:", r.status)
print("cost:", r.cost)
print("chosen_items:", chosen)
print("total_weight:", total_weight, "capacity:", capacity)
print("total_profit:", total_profit)
Output#
The solver selects a feasible subset under the capacity constraint and reports the chosen items.
$ python examples/model/02_knapsack_pb.py
status: optimum
cost: 15
chosen_items: [1, 3]
total_weight: 8 capacity: 8
total_profit: 18
Solution#
Example 03: Minimum Set Cover#
Model the classic minimum set cover problem using booleans, disjunctions, and weighted soft penalties on selected sets.
New primitives#
hermax.model.BoolDict(keyed booleans)hermax.model.Model.vector()withhermax.model.BoolVector.at_least_one()
Model#
This is a weighted set-cover formulation. It is an example of disjunctions over a dictionary of booleans and an objective tied to selected decisions. Set cover is one of Karp’s original NP-complete problems [1] and is also a common problem in Garey and Johnson [3].
The code encodes the objective with soft unit clauses [~x_S], so a
cost is paid when x_S is true.
Problem#
Code#
from hermax.model import Model
m = Model()
universe = ["u1", "u2", "u3", "u4"]
sets = {
"S1": {"u1", "u2"},
"S2": {"u2", "u3"},
"S3": {"u3", "u4"},
"S4": {"u1", "u4"},
}
set_cost = {"S1": 3, "S2": 2, "S3": 3, "S4": 2}
pick = m.bool_dict("pick", list(sets.keys()))
for u in universe:
cover_u = [pick[s] for s in sets if u in sets[s]]
m &= m.vector(cover_u, name=f"cover_{u}").at_least_one()
for s in sets:
m.obj[set_cost[s]] += ~pick[s]
r = m.solve()
chosen_sets = [s for s in sets if r[pick[s]]]
print("status:", r.status)
print("cost:", r.cost)
print("chosen_sets:", chosen_sets)
Output#
The output shows a minimum-cost cover and the selected sets.
$ python examples/model/03_set_cover.py
status: optimum
cost: 4
chosen_sets: ['S2', 'S4']
Solution#
Example 04: Minimum Vertex Cover#
Model weighted vertex cover on a small graph.
Model#
Vertex cover is the graph analogue of set cover. One clause per edge. It is one of Karp’s original NP-complete problems [1] and one of the common graph problems in Garey and Johnson [3].
As with the previous example, the code uses soft unit clauses [~x_v] so selecting a vertex
incurs its cost.
Problem#
Code#
from hermax.model import Model
m = Model()
vertices = [0, 1, 2, 3, 4]
edges = [(0, 1), (1, 2), (2, 3), (3, 4), (0, 4)]
cost = {0: 2, 1: 1, 2: 2, 3: 1, 4: 2}
cover = m.bool_dict("cover", vertices)
for u, v in edges:
m &= (cover[u] | cover[v])
for v in vertices:
m.obj[cost[v]] += ~cover[v]
r = m.solve()
chosen = [v for v in vertices if r[cover[v]]]
print("status:", r.status)
print("cost:", r.cost)
print("vertex_cover:", chosen)
Output#
The solver returns one minimum-cost vertex cover for the small graph instance.
$ python examples/model/04_vertex_cover.py
status: optimum
cost: 4
vertex_cover: [0, 1, 3]
Solution#
Example 05: Job Assignment#
Assign one worker to each task, enforce worker capacity, and minimize assignment cost.
New primitives#
hermax.model.EnumVar/hermax.model.EnumDictEnum equality literals
(assign[t] == worker)hermax.model.BoolVector.at_most_one()
Model#
A single typed variable per task replaces a full Boolean assignment matrix while still exposing exact equality literals for capacity and cost constraints. Unlike the NP-hard examples above, the classical assignment problem is polynomial-time and is famously solved by the Hungarian method [4].
The bracket term [a_t = w] is realized in code by the literal
(assign[t] == w) and softened as [~(assign[t] == w)].
Problem#
Code#
from hermax.model import Model
m = Model()
workers = ["alice", "bob", "carol"]
tasks = ["t1", "t2", "t3"]
cost = {
("t1", "alice"): 5,
("t1", "bob"): 1,
("t1", "carol"): 3,
("t2", "alice"): 2,
("t2", "bob"): 6,
("t2", "carol"): 1,
("t3", "alice"): 4,
("t3", "bob"): 3,
("t3", "carol"): 2,
}
assign = m.enum_dict("assign", tasks, choices=workers, nullable=False)
for w in workers:
chosen_by_worker = [(assign[t] == w) for t in tasks]
m &= m.vector(chosen_by_worker, name=f"worker_{w}_tasks").at_most_one()
for t in tasks:
for w in workers:
m.obj[cost[(t, w)]] += ~(assign[t] == w)
r = m.solve()
print("status:", r.status)
print("cost:", r.cost)
print("assignment:", {t: r[assign[t]] for t in tasks})
Output#
This output shows one feasible assignment that respects worker capacity and the resulting total cost.
$ python examples/model/05_job_assignment.py
status: optimum
cost: 5
assignment: {'t1': 'bob', 't2': 'alice', 't3': 'carol'}
Solution#
Example 06: Enum Subset Disjunction#
Demonstrate the fast categorical subset helper
hermax.model.EnumVar.is_in().
New primitives#
hermax.model.EnumVar.is_in()
Model#
The model isolates the categorical subset helper with a small optimization target. This pattern appears frequently in scheduling models.
Code#
from hermax.model import Model
m = Model()
shift = m.enum("shift", choices=["morning", "day", "night", "graveyard"], nullable=False)
m &= shift.is_in(["morning", "day"])
m.obj[4] += ~(shift == "morning")
m.obj[1] += ~(shift == "day")
r = m.solve()
print("status:", r.status)
print("cost:", r.cost)
print("shift:", r[shift])
Output#
The chosen shift is guaranteed to belong to the allowed subset.
$ python examples/model/06_enum_subset_shift.py
status: optimum
cost: 1
shift: day
Example 07: Allowed Configurations#
Model an extensional table constraint over a temporary typed vector view.
New primitives#
hermax.model.Model.vector()typed viewhermax.model.IntVector.is_in()
Model#
This is an extensional constraint (table of valid tuples), a core CP modelling primitive. The example uses a temporary typed vector view so the syntax stays close to the mathematical statement.
The current implementation uses row-selector literals, deduplicates repeated rows, and gates each row with exact scalar equalities.
Code#
from hermax.model import Model
m = Model()
cpu = m.int("cpu", lb=0, ub=6)
ram = m.int("ram", lb=0, ub=9)
mobo = m.int("mobo", lb=0, ub=6)
valid_configs = [
(1, 2, 1),
(2, 4, 2),
(3, 4, 3),
(4, 8, 5),
]
spec = m.vector([cpu, ram, mobo], name="system_spec")
m &= spec.is_in(valid_configs)
m &= (cpu == 2)
m &= (ram == 4)
r = m.solve()
print("status:", r.status)
print("system_spec:", r[spec])
print("cpu/ram/mobo:", r[cpu], r[ram], r[mobo])
Output#
The selected configuration is one of the allowed table rows.
$ python examples/model/07_table_allowed_configs.py
status: sat
system_spec: [2, 4, 2]
cpu/ram/mobo: 2 4 2
Example 08: Element Constraint (@)#
Show the CP element constraint using lazy array indexing:
array @ int_var.
New primitives#
array @ int_varlazy multiplexer descriptor
Model#
This is the element-constraint pattern from CP: select a constant from an array
using an integer variable, then constrain the selected value. The lazy @
descriptor avoids introducing a separate “selected-cost” integer variable.
For constant right-hand sides, the implementation simplifies this further into domain filtering (forbidding impossible index values).
Problem#
Code#
from hermax.model import Model
m = Model()
worker = m.int("worker", lb=0, ub=3)
budget = m.int("budget", lb=0, ub=120)
worker_costs = [20, 40, 75, 120]
m &= (30 <= worker_costs @ worker) # selected worker cost must be at least 30
m &= (worker_costs @ worker <= budget) # selected worker cost must fit budget
m &= (budget <= 80)
m.obj += budget
r = m.solve()
print("status:", r.status)
print("worker:", r[worker])
print("budget:", r[budget])
print("selected_cost:", worker_costs[r[worker]])
Output#
This output shows the chosen index and the selected cost value constrained by the budget.
$ python examples/model/08_multiplexer_budget.py
status: optimum
worker: 1
budget: 40
selected_cost: 40
Solution#
Example 09: Sudoku (9x9)#
Demonstrate matrix modelling, NumPy-like slicing, and all_different on rows,
columns, and 3x3 subgrids.
New primitives#
hermax.model.EnumMatrixNumPy-like indexing (
grid[r, :],grid[:, c],grid[a:b, c:d])flatten()on matrix viewshermax.model.EnumVector.all_different()
Model#
Matrix-focused example. The instance uses a fixed puzzle, so rows, columns, and 3x3 boxes are easy to inspect with the same typed-vector operations.
Warning
Sudoku “digits” are better modeled as enums than as ints. The puzzle is really about exact symbols, not about arithmetic values. If sudoku had letters instead of digits, it would be played the same way, so the numeric nature of the symbols is not relevant to the combinatorial search. Using enums is more efficient when possible.
Problem#
Code#
from hermax.model import Model
m = Model()
choices = [str(i) for i in range(1, 10)]
grid = m.enum_matrix("cell", rows=9, cols=9, choices=choices, nullable=False)
for r in range(9):
m &= grid[r, :].all_different()
for c in range(9):
m &= grid[:, c].all_different()
for br in range(0, 9, 3):
for bc in range(0, 9, 3):
m &= grid[br : br + 3, bc : bc + 3].flatten().all_different()
# Fixed Sudoku instance
givens = [
(0, 0, "5"), (0, 1, "3"), (0, 4, "7"),
(1, 0, "6"), (1, 3, "1"), (1, 4, "9"), (1, 5, "5"),
(2, 1, "9"), (2, 2, "8"), (2, 7, "6"),
(3, 0, "8"), (3, 4, "6"), (3, 8, "3"),
(4, 0, "4"), (4, 3, "8"), (4, 5, "3"), (4, 8, "1"),
(5, 0, "7"), (5, 4, "2"), (5, 8, "6"),
(6, 1, "6"), (6, 6, "2"), (6, 7, "8"),
(7, 3, "4"), (7, 4, "1"), (7, 5, "9"), (7, 8, "5"),
(8, 4, "8"), (8, 7, "7"), (8, 8, "9"),
]
for r, c, v in givens:
m &= (grid[r, c] == v)
r = m.solve()
print("status:", r.status)
for i in range(9):
print(f"row_{i + 1}:", r[grid[i, :]])
Output#
The output shows the deterministic completion of the fixed puzzle instance.
$ python examples/model/09_sudoku9_single_clue.py
status: sat
row_1: ['5', '3', '4', '6', '7', '8', '9', '1', '2']
row_2: ['6', '7', '2', '1', '9', '5', '3', '4', '8']
row_3: ['1', '9', '8', '3', '4', '2', '5', '6', '7']
row_4: ['8', '5', '9', '7', '6', '1', '4', '2', '3']
row_5: ['4', '2', '6', '8', '5', '3', '7', '9', '1']
row_6: ['7', '1', '3', '9', '2', '4', '8', '5', '6']
row_7: ['9', '6', '1', '5', '3', '7', '2', '8', '4']
row_8: ['2', '8', '7', '4', '1', '9', '6', '3', '5']
row_9: ['3', '4', '5', '2', '8', '6', '1', '7', '9']
Solution#
Example 10: No Overlap Scheduling#
Show scheduling constraints with object interval methods.
New primitives#
hermax.model.IntervalVarhermax.model.Model.interval()hermax.model.IntervalVar.ends_before(),hermax.model.IntervalVar.no_overlap()
Model#
This model uses a scheduling oriented API layer built on top of ladder
integers. IntervalVar keeps the model readable while compiling to plain
SAT/MaxSAT constraints.
Internally, the interval identity is compiled with a direct ladder-bit weld (linear number of binary clauses, zero auxiliary variables), not with a generic PB/Card equality encoder.
Code#
from hermax.model import Model
m = Model()
task_a = m.interval("A", start=0, duration=5, end=24)
task_b = m.interval("B", start=0, duration=3, end=24)
task_c = m.interval("C", start=0, duration=4, end=24)
m &= task_a.no_overlap(task_b)
m &= task_b.no_overlap(task_c)
m &= task_a.no_overlap(task_c)
m &= task_a.ends_before(task_c)
m.obj[1] += task_c.start
r = m.solve()
print("status:", r.status)
print("cost:", r.cost)
print("A:", r[task_a])
print("B:", r[task_b])
print("C:", r[task_c])
Output#
The printed intervals confirm the no-overlap and precedence constraints.
$ python examples/model/10_interval_scheduling.py
status: optimum
cost: 5
A: {'start': 0, 'end': 5, 'duration': 5}
B: {'start': 9, 'end': 12, 'duration': 3}
C: {'start': 5, 'end': 9, 'duration': 4}
Example 11: Int Variables in the Objective#
Demonstrate obj[w] += int_var (ladder-bit objective lowering) combined with
hard PB constraints.
New primitives#
model.obj[w] += int_varwithhermax.model.IntVar
Model#
This model shows typed finite-domain variables in the MaxSAT objective without introducing a separate arithmetic backend API.
model.obj[w] += x is lowered to soft clauses over the ladder threshold bits
of x (linear in the ladder width), plus a constant offset contribution from
the lower bound.
Code#
from hermax.model import Model
m = Model()
x = m.int("x", lb=0, ub=10)
y = m.int("y", lb=0, ub=10)
use_bonus = m.bool("use_bonus")
m &= (x + y >= 8)
m &= (x + 2 * y <= 14)
m &= (x + use_bonus <= 6)
m.obj[3] += x
m.obj[1] += y
m.obj[2] += ~use_bonus
r = m.solve()
print("status:", r.status)
print("cost:", r.cost)
print("x,y,use_bonus:", r[x], r[y], r[use_bonus])
Output#
This output shows a solution minimizing an objective that includes integer variables and a boolean penalty.
$ python examples/model/11_int_objective.py
status: optimum
cost: 12
x,y,use_bonus: 2 6 False
Example 12: WiFi Channel Assignment#
Show a compact domain model using nullable enum states for router channels, hard interference constraints, and soft penalties. Inspired on [5] and [6], this is a simplified version of the WiFi channel assignment.
New primitives#
hermax.model.EnumDictwithnullable=TruePractical composition of enum equality literals and soft penalties
Model#
This model combines nullable enums, graph constraints, and soft penalties. It is a good example after the smaller, isolated primitives explored earlier on this page.
The model uses nullable enums so “offline” is represented as the
absence of a selected frequency (decoded as None), rather than as an extra
binary-variable layer.
Problem#
Code#
from hermax.model import Model
m = Model()
routers = [0, 1, 2, 3]
edges = [(0, 1), (1, 2), (2, 3), (0, 3)]
freqs = ["f1", "f2", "f3"]
state = m.enum_dict("router_state", routers, choices=freqs, nullable=True)
for u, v in edges:
for f in freqs:
m &= (~(state[u] == f) | ~(state[v] == f))
for r in routers:
m.obj[5] += state[r].is_in(["f1", "f2", "f3"])
m.obj[1] += ~(state[r] == "f1")
m.obj[2] += ~(state[r] == "f2")
m.obj[1] += ~(state[r] == "f3")
r = m.solve()
print("status:", r.status)
print("cost:", r.cost)
print("router_state:", r[state])
Output#
The result shows router states and the resulting objective cost.
$ python examples/model/12_wifi_nullable_enum.py
status: optimum
cost: 4
router_state: {0: 'f3', 1: 'f1', 2: 'f3', 3: 'f1'}
Solution#
Next#
Advanced modelling examples continue in Advanced Modelling Examples.