solvi.sets¶
Decisions over a set: the answers of many items made consistent under set-level constraints (at most one / exactly one per key, mutual exclusion, a capacity per key or per answer), exact per connected component or by a stated greedy approximation, every change cited, the record replayable.
Decisions over a set: the answers of many items made consistent under set-level constraints — at most one / exactly one "yes" per key (one counterpart per product, one owner per record), mutual exclusion, a capacity per key or per answer (at most 3 tasks per shift) — from the items' own answers and probabilities.
from solvi.sets import AtMostOne, Item, decide_set
items = [Item.of(system.ask(s), "match", id=(s["abt"], s["buy"]), keys={"abt": s["abt"], "buy": s["buy"]})
for s in states]
out = decide_set(items, [AtMostOne("abt"), AtMostOne("buy")])
out[("a1", "b7")].answer, out[("a1", "b7")].why # "changed from 'yes' to 'no' to satisfy at_most_one(abt) on
# abt=a1: ('a1', 'b3') holds 'yes' (0.99)"
out.changed, out.feasible, out.method, out.components
out.replay() # the record re-checked and re-solved: {"ok", "mismatches", ...}
SetDecision.from_dict(out.to_dict()) # JSON in, JSON out
What is chosen: the most probable combination of answers that satisfies every constraint — the product of the items' probabilities, taken as independent (the objective of the constraints between answers inside one request). An item that is not free — an answer without probabilities (a rule's, a hard check's, a forced one), an abstention, a multi-label, span, ranking or estimate answer — never changes; its answer counts in the groups as given (an abstention counts nowhere). A free item's answer may change to any answer it gives a probability above 0.
How. A constraint puts items in groups (by a key, by explicit lists of ids, or all items in one) and bounds how many of
each group hold a counted answer. Only groups that can bind are kept; items linked by them form connected components, and
each component is decided alone: a component whose answers as given already satisfy its groups keeps them (every item at
its most probable answer is the optimum), the others are solved.
- method="exact" (default): an integer program per component (HiGHS through scipy.optimize.milp, relative gap 0) —
proven optimal unless time_limit (seconds per component) stops it; then the component's status says "time limit"
and the best combination found is used, so the set is not exact and exact is False. A tie between equally probable
combinations is broken by Item.tie (a second program: among the optimal combinations, the largest sum of the ties
of the items kept at their given answer), then by the solver, deterministically for one scipy version.
- method="greedy": the stated approximation, linear in the items. From the surest item down (the probability of its
given answer, then its tie, then input order), each free item takes its most probable answer whose groups still have
room; then groups below their minimum take the item that loses least by moving into them. Not optimal: with a1–b1 at
0.95 and a2–b1, a1–b2 at 0.9 under one counterpart per offer it keeps one pair where the most probable combination
keeps two; it can also leave a group below its minimum that a search would fill.
A component that cannot satisfy its groups (fixed answers that conflict, a minimum no free item can meet, a greedy that
got stuck) keeps its answers as given: feasible is False, violations names each broken group, and every item of a
broken group says "not repaired" in its why — nothing is silently left broken.
Every changed item cites what changed it: the group whose maximum its given answer would break, with the items that hold
the group's counted answers ("kept"), or the group whose minimum needed it. The record holds each item's probabilities,
its given and final answer, the groups as evaluated (so replay needs neither the items' objects nor the key functions),
the method and every component's status. replay() re-checks the record — the final answers satisfy every group that is
not reported broken, fixed answers are unchanged, every change is cited, the objective is recomputed — and re-solves it:
under method="exact" no combination may be more probable than the recorded one (an equally probable different one is a
note, not a mismatch).
Not done here: constraints that are not counts over groups — transitivity of matches (a~b and b~c → a~c), "if a then b", sums of weights; soft constraints with a cost; items whose errors are not independent (the objective treats them as independent); learning anything — the probabilities are the items' own.
Item
dataclass
¶
Item(id: Any, probs: dict | None = None, answer: Any = None, keys: dict = dict(), tie: float = 0.0, source: Any = None, why: str = '')
One item of the set: its id (hashable, JSON-able: a str, an int or a tuple of them), the probabilities of the
answers it may take (probs; None for a fixed item), its answer as given (None: the most probable; for a fixed
item, the answer that never changes — None is an abstention, counted in no group), the keys constraints group it by,
a tie score (see the module docs), the stored id of the response it came from and its own reason.
of
classmethod
¶
An item from a Response's answer to question: free when the answer is a yes/no, choice or ordinal answer with
probabilities and status "ok" (a learned head, a model decision, a rule returning a Decision); fixed otherwise —
a forced or rule answer without probabilities as given, an abstention as None. The response's stored id is kept
as the item's source.
Capacity
dataclass
¶
Capacity(key: Any = None, max: int | None = None, min: int = 0, answer: Any = 'yes', groups: list | None = None, name: str | None = None)
At most max (and at least min) items of each group hold a counted answer.
key: how items are grouped — the name of a key in Item.keys (an item without it is in no group), a function
item → key (None: in no group), or None: all items in one group. groups: explicit lists of item ids instead of a key
(mutual exclusion between listed items). answer: the counted answer, a list of them, or EACH (each answer value is a
group of its own: a capacity per shift). name: the constraint's name in the record (default from the kind and key).
Decided
dataclass
¶
Decided(id: Any, answer: Any, was: Any, confidence: float | None, fixed: bool, cited: list = list(), why: str = '', source: Any = None)
One item's final answer: answer, the answer as given (was), its probability (confidence), whether it is
fixed, what changed it (cited: [{"constraint", "group", "kept": [ids]} or {"constraint", "group", "needed": n}])
and the reason (why: the item's own, then the change).
SetDecision
dataclass
¶
SetDecision(items: dict, constraints: list, method: str, components: list, feasible: bool, violations: list, exact: bool, record: dict = dict())
The decision over a set: items (id → Decided, in input order; out[id], iteration), changed, feasible,
violations ([{"constraint", "group", "count", "min", "max"}]), method, exact (every component solved to a
proven optimum), components (each solved component: its items, groups, status, objective, seconds), the
constraints and the input as recorded. to_dict / from_dict; replay().
replay ¶
Re-check the record and re-solve it → {"ok", "mismatches": [(item or group, what, why)], "notes": [...]}.
Checked: every final answer is one the item may take (a fixed item's is its own); every group that is not reported broken holds between its min and max, and every reported one is broken; every changed item is free and cites a constraint; the recorded objective per component is that of the final answers. Then the record is solved again with its method: under "exact", a more probable combination than the recorded one is a mismatch, an equally probable different one a note; under "greedy", different answers are a mismatch.
AtMostOne ¶
At most one item of each group holds answer (one counterpart per product; with groups=, mutual exclusion).
ExactlyOne ¶
Exactly one item of each group holds answer (one owner per record).
Exclusive ¶
Mutual exclusion: of each listed group of item ids (pairs, or more), at most one holds answer.
decide_set ¶
The answers of items made consistent under constraints (see the module docs) → a SetDecision.
items: [Item] (Item.of(response, question, ...) for System answers), ids unique. constraints: [Capacity] (AtMostOne, ExactlyOne, Exclusive, Capacity). method: "exact" (default; an integer program per connected component) or "greedy" (the stated approximation). time_limit: seconds per component for "exact".