Solve, Export, and Decode#
The model layer supports export to PySAT formulas, decoding, and solve().
Export to CNF / WCNF#
Use:
model.to_cnf()model.to_wcnf()
Rules:
to_cnf()requires the model to contain no soft clausesto_wcnf()exports both hard and soft clauses
to_cnf() raises ValueError if soft clauses are present.
Soft clauses#
model.obj[w] += constraint adds weighted soft clauses.
For a unit literal a:
model.obj[5] += ameans pay5ifais falsemodel.obj[5] += ~ameans pay5ifais true
Objective replacement#
The objective has two modes:
additive mode via
model.obj += ...andmodel.obj[w] += ...replacement mode via
model.obj = expr(ormodel.obj.set(expr))
Replacement mode clears active objective terms and installs the new objective. Additive mode keeps previous objective terms.
Use:
m = Model()
a = m.bool("a")
b = m.bool("b")
m.obj[3] += a # additive
m.obj[2] += ~b # additive
m.obj = (a + 5) # replace objective
m.obj.clear() # remove objective terms managed by replacement API
Notes:
model.obj = exprexpects a linear expression (Literal,Term,PBExpr,IntVar, lazy int expr)for literal/clause soft constraints, use additive APIs (
obj +=,obj[w] +=, oradd_soft)
Lexicographic objective API#
For strict priority optimization (tier 0 first, then tier 1, …), use
model.tier_obj.
Two declaration styles are supported:
declarative:
model.tier_obj.set_lexicographic(primary_cost, secondary_cost)
dynamic:
model.tier_obj[0, 5] += primary_term model.tier_obj[1, 1] += secondary_term
Rules:
lower tier index means higher priority
tiers are solved in lexicographic order
model.obj/add_softandmodel.tier_objare mutually exclusive in one model stateuse
model.tier_obj.clear()to drop tier objectives
Solve strategies:
model.solve(lex_strategy="incremental"): sequential lex optimization (default)model.solve(lex_strategy="stratified"): one solve call with scaled weights
If stratified scaling would overflow integer bounds, solving raises a
ValueError/OverflowError; use lex_strategy="incremental" in that case.
Assumptions apply to the whole lex query and are used for every tier step.
Result fields:
SolveResult.tier_costs: per-tier costs (highest priority first)SolveResult.tier_models: per-tier raw models for incremental lex solve;Nonefor stratified solveSolveResult.cost: final-tier cost for incremental lex solve
Example:
m = Model()
a = m.bool("a")
b = m.bool("b")
m.tier_obj[0, 1] += a # tier 0
m.tier_obj[1, 10] += ~b # tier 1
r = m.solve(lex_strategy="incremental")
print(r.tier_costs, r.cost)
Soft groups and PB constraints#
If the soft object compiles to multiple clauses (for example a PB constraint), the model uses targeted relaxation:
one weighted soft penalty literal
plus the compiled clause network conditional as hard constraints
This preserves penalty per logical constraint.
Integer objective terms#
obj[w] += int_var is supported and lowered to weighted soft unit clauses on
the ladder bits (plus a constant objective offset for the lower bound).
This is restricted to int_var.lb >= 0.
Decode solver models#
Use:
assignment = model.decode_model(raw_lits)
or via the convenience result object from Model.solve().
AssignmentView supports decoding:
Literal->boolEnumVar->str | NoneIntVar->intvectors -> lists
matrices / matrix views -> nested lists
dicts -> dicts
Convenience Solving#
Model.solve() is the main entry point and uses model-native incremental
state by default.
Behavior:
hard-only models: incremental SAT backend (PySAT, default
g4)soft models with default
backend="auto"and no explicit solver: one-shot Hermax RC2 path (viahermax.non_incremental.RC2)explicit
backend="maxsat"with incremental mode: requires a HermaxIPAMIRSolverclass/factory or instanceexplicit
solver=...withbackend="auto"and no bound backend: one-shot solve through that solver
Returns hermax.model.SolveResult with:
statuscost(Nonefor SAT)raw_modelassignment(decoded view)
Incremental Defaults#
Model.solve() defaults to incremental behavior:
m = Model()
a = m.bool("a")
m.solve() # binds SAT backend
m &= ~a # routed incrementally
m.solve()
Once a backend is bound, model updates are reflected on the bound backend when they are materialized for solving. Soft-clause behavior depends on bound mode:
bound MaxSAT backend: soft additions/updates are applied on the MaxSAT side
bound SAT backend: soft additions are cached in the model and applied when solving upgrades/rebinds to MaxSAT
Backend transition rules:
SAT-bound model + newly added soft constraints: by default, next
solve()upgrades to MaxSAT (sat_upgrade="upgrade")use
sat_upgrade="error"to make SAT->MaxSAT upgrade fail explicitlyMaxSAT-bound model cannot switch to SAT backend
Assumptions#
Model.solve(assumptions=...) accepts:
DIMACS integers (non-zero)
Literalunit
Term(coefficient+1or-1)
Examples:
a = m.bool("a")
b = m.bool("b")
r = m.solve(assumptions=[a.id, ~b, 1 * a, -1 * b])
Weight Updates#
Model.add_soft(...) returns a hermax.model.SoftRef:
group_id: logical soft group identifiersoft_ids: concrete lowered soft clause ids
Use update_soft_weight(target, w) with:
SoftRef(recommended)one soft id (
int)sequence of soft ids
Example:
x = m.int("x", 0, 5)
ref = m.add_soft(x, 3) # may lower into many soft clauses
m.update_soft_weight(ref, 7)
Weight updates use positive weights in the public API. Zero-weight removal is used internally by objective replacement and diffing.
Floating objective weights#
Floating weights are supported for objective APIs when objective precision is enabled:
m = Model()
x = m.bool("x")
m.set_objective_precision(decimals=2)
m.obj = 3.5 * x
m.obj[1.25] += ~x
ref = m.add_soft(x, 2.75)
m.update_soft_weight(ref, 4.10)
Rules:
precision is disabled by default
with precision disabled, objective weights must be integer (floats such as
1.0are rejected)with precision enabled, soft/objective float weights are rounded to the configured decimal precision
changing precision re-rounds existing objective soft weights from their original raw values
Float coefficients are not allowed in PB/Card constraints.
Hermax solver integration#
Model.solve() can use Hermax MaxSAT backends directly via the IPAMIR
solver interface defined in hermax.core.ipamir_solver_interface.
Supported forms:
pass a solver class / factory (the model exports WCNF and the solver is constructed with
formula=...)pass a solver instance (the model is replayed into that instance through the IPAMIR API)
Examples:
from hermax.incremental.UWrMaxSAT import UWrMaxSAT
r = m.solve(solver=UWrMaxSAT)
print(r.status, r.cost)
inst = UWrMaxSAT()
r = m.solve(solver=inst)
inst.close()
Portfolio usage is also supported because hermax.portfolio.PortfolioSolver
implements the same IPAMIR solver interface:
from hermax.portfolio import PortfolioSolver
from hermax.incremental.UWrMaxSAT import UWrMaxSAT
r = m.solve(
solver=PortfolioSolver,
solver_kwargs={
"solver_classes": [UWrMaxSAT],
"max_workers": 1,
"selection_policy": "first_optimal_or_best_until_timeout",
},
)
Notes:
solver_kwargsis only valid whensolveris a class/factory.backend="maxsat"in incremental mode requires a Hermax IPAMIR solver.default
backend="auto"keeps one-shot Hermax RC2 for soft models.
Example#
m = Model()
a = m.bool("a")
b = m.bool("b")
m &= (a | b)
m.obj[2] += ~a
m.obj[1] += ~b
r = m.solve()
print(r.status, r.cost)
print(r[a], r[b])
Manual solver roundtrip#
For explicit solver control, export and solve manually:
wcnf = m.to_wcnf()
from pysat.examples.rc2 import RC2
with RC2(wcnf) as rc2:
raw = rc2.compute()
assignment = m.decode_model(raw)