mixle.relations module¶
Relations over structured spaces, enumerated in order of a residual.
A Relation is not an optimization problem – it is a constraint imposed on a structured
space (matchings, spanning trees, strings near a center, hidden-state sequences, feature subsets),
whose members are enumerated ranked by a residual/cost. Finding the single best member is incidental;
the value is the whole ranked set. You specify the relation, then ask it for an enumerator() –
the same shape as a distribution yielding a sampler() / estimator() / enumerator(). Every
relation shares one surface:
relation.solve() -> the minimal-residual Solution (or None if the relation is empty)
relation.top(k) -> the k smallest-residual members as a list
relation.enumerator() -> a lazy iterator over members, smallest residual first
for solution in relation: ...
Each item is a Solution namedtuple (value, objective) – it reads as sol.value /
sol.objective and still unpacks as value, objective = sol. value is the member itself
(an assignment, a nearby string, a state sequence, a feature subset, …) and objective is its
residual: a cost (minimized) or score (maximized); sense records which.
Assignment, spanning tree, the edit-distance ball, k-best Viterbi, shortest path, and best-subset
regression are all specified and consumed the same way, each delegating to whatever engine fits
(Murty for assignment, Gabow for spanning trees, A* / best_first_paths() for paths,
mixle.enumeration.hmm_paths.hmm_best_paths() for Viterbi, Dijkstra / nearest_first()
for the edit-distance ball, exhaustive ranking for best-subset).
The two shared low-level engines are best_first_paths() (k-best paths to a goal) and
nearest_first() (distinct states outward from a center – an expanding metric ball).
>>> from mixle.relations import Assignment
>>> sol = Assignment([[1, 9], [9, 1]]).solve()
>>> sol.value, sol.objective # the column assignment and its total cost
(array([0, 1]), 2.0)
- admm_bounded_least_squares(a, b, lower=0.0, upper=np.inf, *, rho=1.0, max_iter=5000, tol=1.0e-8)[source]
Solve
min_x ||A x - b||^2subject tolower <= x <= upperby ADMM.The alternating-direction method of multipliers splits the problem as
f(x) = ||A x - b||^2plus the box indicatorg(z), withx = z, and alternates: anx-update (the ridge solve(A^T A + rho I) x = A^T b + rho (z - u), factorized once), az-update (projectx + uonto the box), and the scaled dual updateu += x - z. This is the augmented-Lagrangian path “beyond pure penalty”: it converges to the exact constrained optimum (lower=0, upper=infrecovers non-negative least squares). Returns the bounded solutionx.lower <= upperelementwise, neither NaN;rho > 0(the ridge term is SPD only then);max_iter >= 1. An infiniteupperstays legal – it is how the NNLS case is spelled.
- class Assignment(cost, maximize=False)[source]
Bases:
RelationLinear assignment / bipartite matching: match rows to columns at extremal total cost.
The solution value is
col_ind–col_ind[i]is the column assigned to rowi.- Parameters:
cost (np.ndarray)
maximize (bool)
- class BestSubsetRegression(X, y, criterion='aic', max_size=None, intercept=True)[source]
Bases:
RelationBest-subset feature selection for least squares, enumerated in increasing selection criterion.
Solution values are feature-index tuples ranked by
criterion: residual sum of squares ("rss"), Akaike ("aic") or Bayesian ("bic") information criterion (Gaussian form). Best-subset selection is inherently exponential, so this scores subsets exhaustively up tomax_sizefeatures – capmax_size(and/or the number of features) for largep.- Parameters:
- branch_and_bound_milp(c, a_ub=None, b_ub=None, integer=None, bounds=None, *, sense='min', tol=1.0e-6)[source]
Solve a mixed-integer linear program by branch-and-bound over the LP relaxation.
Minimizes (
sense="min") or maximizes (sense="max")c @ xsubject toa_ub @ x <= b_uband per-variablebounds(lo, hi), with the variables indexed byintegerconstrained to integers (default: all). Returns(objective, x)orNoneif infeasible. Each node solves the continuous relaxation withscipy.optimize.linprog(HiGHS) and, if an integer variable is fractional, branches intox_i <= floorandx_i >= ceil; best-bound search prunes nodes that cannot beat the incumbent. Exact for bounded integer feasible regions.tolis the integrality/pruning tolerance and must be finite and strictly below0.5: it is the width within which a relaxation coordinate counts as already integral, and half a unit is the most that can ever mean. See the note at its validation below for what a wider one returned.
- cardinality_constrained_milp(c, a_ub, b_ub, max_nonzero, bounds, *, sense='min')[source]
Minimize/maximize
c @ xwith at mostmax_nonzeroof the variables nonzero.Adds a cardinality (sparsity) constraint to the linear program
a_ub @ x <= b_ubwith per-variableboundsvia the standard big-M indicator formulation: a binaryz_igates each variable (lower_i z_i <= x_i <= upper_i z_i, soz_i = 0forcesx_i = 0) andsum z_i <= max_nonzero; the extended mixed-integer program is solved bybranch_and_bound_milp(). Every bound must be finite – the bounds are the big-M constants of the indicator rows. Returns(objective, x)(the sparse optimizer) orNoneif infeasible. This is the indicator/ set-membership/cardinality constraint primitive (best-subset selection, sparse design).
- class Design(cost, open, flow)[source]
Bases:
NamedTupleA network-design solution:
cost(total),open(opened-arc/facility mask), and the inducedflow.- cost: float
Alias for field number 0
- open: ndarray
Alias for field number 1
- flow: ndarray
Alias for field number 2
- class EditDistance(center, alphabet, *, sub_cost=None, ins_cost=None, del_cost=None, max_distance=None)[source]
Bases:
RelationEnumerate strings outward from a center by (non-uniform) edit distance – an edit-distance ball.
You give a single center string and an alphabet, not two endpoints (the distance between two fixed strings is just one number). The enumerator yields strings in increasing edit distance from the center: the center itself at distance 0, then its 1-edit neighbours, then 2-edit, and so on – a Dijkstra expansion over string space with per-operation costs (
nearest_first()). The ball is infinite (insertions grow strings without bound), so bound it withmax_distanceor by takingtop(k)/enumerator(k). Solution values are strings (or symbol tuples, matching the center’s type); the objective is the edit distance from the center.- Parameters:
center (Iterable[Any]) – The center string (or sequence of symbols).
alphabet (Iterable[Any]) – The symbols available for substitution and insertion.
sub_cost (Callable[[Any, Any], float] | None) –
(a, b) -> costof substitutingawithb(default unit; 0 if equal).ins_cost (Callable[[Any], float] | None) –
c -> costof inserting symbolc(default 1).del_cost (Callable[[Any], float] | None) –
a -> costof deleting symbola(default 1).max_distance (float | None) – Only enumerate strings within this edit distance (
None= unbounded/lazy).
- class Flow(value, flow)[source]
Bases:
NamedTupleA resolved flow:
value(total/objective cost) andflow[u, v]per-arc, like max_flow’s output.- value: float
Alias for field number 0
- flow: ndarray
Alias for field number 1
- graph_coloring(adjacency)[source]
Exact minimum proper vertex coloring of an undirected graph.
adjacencyis ann x nsymmetric 0/1 (or boolean) matrix with a zero diagonal. Returns(k, coloring)wherekis the chromatic number andcoloring[v]in0..k-1gives no two adjacent vertices the same color. Solved by backtracking with the standard symmetry break (a vertex may introduce at most one new color), tryingk = 1, 2, ...until colorable – exact, but worst-case exponential, so intended for small/medium graphs.
- irreducible_infeasible_subset(a_ub, b_ub, bounds=None)[source]
Find an irreducible infeasible subset (IIS) of the linear constraints
a_ub @ x <= b_ub.Returns the row indices of a minimal infeasible subset: the subsystem is itself infeasible, yet dropping any single one of its rows makes it feasible (within the variable
bounds, default unbounded). ReturnsNoneif the full system is already feasible. Uses the deletion filter – tentatively remove each constraint and keep it removed whenever the remainder stays infeasible – so the result certifies which constraints conflict, the standard infeasibility diagnostic.
- min_cost_flow(cap, cost, supply)[source]
Min-cost feasible flow meeting node
supply(positive = source) under arccap/cost.Successive-shortest-path with node potentials: a super-source feeds every
supply > 0node and everysupply < 0node feeds a super-sink (arc capacity|supply|, cost 0), then the residual super-source -> super-sink path of least reduced cost is repeatedly augmented (Dijkstra, since the potentials keep every reduced cost non-negative) until the super-sink is unreachable. Potentials are seeded with a Bellman-Ford pass so arbitrary-signcostentries are handled, then updated by the per-round Dijkstra distances (the standard Johnson’s-algorithm maintenance).Successive-shortest-path alone only guarantees minimality among source-to-sink augmenting paths – it says nothing about a profitable cycle that never touches the super-source/super-sink at all. Once feasibility is reached,
_cancel_negative_cycles()runs a Bellman-Ford negative-cycle-canceling pass over the resulting residual graph and pushes flow around any negative-cost cycle it finds (up to its bottleneck capacity) – which strictly lowers cost without disturbing any node’s supply/demand balance, since a cycle has zero net flow at every node it passes through. This is what makes the returned flow truly minimum rather than merely feasible.cap/costaren x narc matrices;supplyis lengthnand must sum to (approximately) zero. Returns Flow(value=total cost, flow=(n, n) arc flows); raisesValueErrorif the supply cannot be fully routed under the given capacities, or if the cost structure admits an unbounded negative cycle (infinite capacity available to an improving cycle, so no finite minimum cost exists).
- multicommodity_flow(cap, cost, demands)[source]
Multi-commodity min-cost flow (different products/grades share arc
cap) via LP relaxation.cap/costaren x narc matrices (an arc exists wherecap > 0);demandslists(source, sink, amount)rows, one per commodity. Each commodity gets its own flow variables with its own conservation constraints (per-commodity supply at its source, demand at its sink, balance elsewhere); every arc’s combined commodity flow is capped atcap[u, v]. Solved as a single LP (scipy.optimize.linprog, HiGHS) over the stacked per-commodity arc variables. Returns Flow with the aggregate cost and the summed arc flows (flow = sum_k x_k); raisesValueErrorif the demands cannot be met under the shared capacities.
- network_design(nodes, arcs, fixed_costs, demands)[source]
Capacitated fixed-charge network design (which arcs/facilities to open) via big-M MILP.
nodeslists node ids;arcslists candidate directed arcs (fixed_costs[i]is the cost of openingarcs[i]);demandsis a length-len(nodes)net supply/demand vector aligned withnodes(positive = supply, negative = demand, summing to zero) – the same convention asmin_cost_flow()’ssupply. Continuous flowx[arc] >= 0is big-M-linked to the binary open/closed decisiony[arc](x[arc] <= M * y[arc],Msized from the total demand so it never binds a genuinely open arc); flow balanceA x = demandsis encoded as the paired inequalitiesA x <= demandsand-A x <= -demands(branch_and_bound_milp()exposes onlya_ub/b_ub). The frozen signature carries no per-unit routing cost, so the objective is the total opening costfixed_costs @ y(the classic uncapacitated-routing fixed-charge network design: which arcs to build so the demand vector is routable at minimum build cost). ReturnsDesign(cost, open, flow)whereopenis the boolean opened-arc mask andflowis the induced(n, n)arc flow; raisesValueErrorif no arc-opening choice can route the demands.
- class Relation[source]
Bases:
ABCA constraint over a structured space whose members are enumerated ranked by a residual.
Subclasses implement
enumerator()(yieldingSolutionitems);solve(),top()and iteration come for free.senseis"min"(residual minimized, members out in increasing cost) or"max"(residual maximized, members out in decreasing score).- abstractmethod enumerator(k=None)[source]
Lazily yield
Solutionitems best-first; at mostkif given (None= all).
- solve()[source]
The single optimal
Solution, orNoneif the problem is infeasible.- Return type:
Solution | None
- top(k)[source]
The
kbest solutions as a list.kis an exact non-negative count:top(0)used to return ONE solution andtop(2.9)three, because the engines counted emissions after yielding and compared against the raw float (MXR-080-1903).
- sampler(seed=None, *, temperature=1.0, k=None, uniform=False, rng=None)[source]
Return a
RelationSamplerthat draws members under a Gibbs measure over the objective.A relation is a specification of a structured space, not itself a random object; sampling it means imposing a distribution over its members, which needs an RNG and a temperature. So – like every other mixle object – it hands back a sampler (
relation.sampler(seed).sample(size)) that owns the stream and the Gibbs measure, rather than being sampled directly.Each enumerated member is weighted
exp(-objective / temperature)whensense == "min"(low cost favoured) orexp(objective / temperature)whensense == "max".temperature -> 0concentrates on the optimum;-> inf(oruniform=True) is uniform. The draw is an exact Gibbs sample only when the relation is finite and fully enumerated (k=None); passkto truncate an infinite/large relation to itskbest (the dropped tail is the lowest-weight mass – a good low-temperature approximation, andkis required if infinite).- Parameters:
seed (int | None) – scalar seed for the sampler’s RandomState (ignored if
rngis given).temperature (float) – Gibbs temperature (default 1.0).
k (int | None) – enumerate at most this many members (
None= all; required if infinite).uniform (bool) – ignore objectives and sample uniformly over the enumerated members.
rng – a shared
numpy.random.RandomState(takes precedence overseed).
- Return type:
RelationSampler
- class RelationSampler(relation, seed=None, *, temperature=1.0, k=None, uniform=False, rng=None)[source]
Bases:
objectDraws members of a
Relationunder a Gibbs measureexp(-objective / temperature).Constructed via
Relation.sampler(). It enumerates the relation’s members once (lazily, on the first draw) and caches the resulting categorical, so repeatedsamplecalls are low-overhead.size=Nonereturns one member value;size=intreturns a list of that many draws.
- class ShortestPath(start, successors, is_goal=None, *, sense='min', heuristic=None)[source]
Bases:
Relationk-shortest-path / best-first search over an arbitrary state graph.
Specify the graph by
startandsuccessors; the solution value is the list of states fromstartto a goal. By default a sink (a state with no successors) is a goal, so a finite DAG search needs no goal test; passis_goalfor infinite graphs or early goals. Usesense="max"for highest-score paths and an admissibleheuristicfor A*.- Parameters:
- max_clique(adjacency)[source]
A maximum clique (largest mutually-adjacent vertex set) of an undirected graph.
adjacencyis ann x nsymmetric 0/1 (or boolean) matrix with a zero diagonal. Returns the sorted vertices of one maximum clique via Carraghan-Pardalos branch-and-bound (prune when the current clique plus the remaining candidates cannot beat the incumbent) – exact, worst-case exponential, intended for small/medium graphs.
- max_independent_set(adjacency)[source]
A maximum independent set (largest pairwise-non-adjacent vertex set) – a max clique of the complement.
- is_stable_matching(match, proposer_prefs, receiver_prefs)[source]
Return
Trueiffmatchhas no blocking pair (a mutually-preferred unmatched proposer/receiver).
- max_flow(capacity, source, sink)[source]
Maximum
source -> sinkflow in a directed network (Edmonds-Karp).capacityis ann x nnon-negative matrix of arc capacities. Returns(value, flow)whereflow[u, v]is the flow on arcu -> v(conserved at every node but the source/sink) andvalueis the total flow out ofsource. Edmonds-Karp augments along BFS shortest paths in the residual network, so it runs inO(V E^2)and terminates on real-valued capacities.capacitymust be square and NaN-free, andsource/sinkmust be distinct in-range nodes; see_require_square(),_reject_nan()and the note below for what each of those used to do instead (MXR-080-1903).
- min_arborescence(weight, root=0)[source]
Minimum-weight spanning arborescence rooted at
root(directed MST; Chu-Liu/Edmonds).weightis ann x nmatrix of directed arc costs with+inffor absent arcs. Returns(total, parent)whereparent[v]is the chosen in-arc tail for each non-rootv(andparent[root] = -1), forming the minimum-cost arborescence in which every node is reachable fromroot; returnsNoneif no such arborescence exists.NaN and
-infare refused for the same reason as intsp_held_karp(): arc presence is decided bynp.isfinite, so both used to read as “absent arc” (MXR-080-1903).
- min_cut(capacity, source, sink)[source]
Minimum
source/sinkcut of a directed network (via max-flow; the max-flow min-cut theorem).Returns
(capacity, source_side, cut_edges): the cut capacity (equal to the max-flow value), the set of nodes on the source side (reachable fromsourcein the final residual graph), and the saturated arcs crossing from the source side to the sink side.Input contract as
max_flow()’s, and enforced there: a NaN capacity used to make this return capacity0.0with an EMPTY cut-edge list on a graph that plainly has arcs (MXR-080-1903).
- stable_matching(proposer_prefs, receiver_prefs)[source]
Proposer-optimal stable matching via Gale-Shapley.
proposer_prefs[i]is proposeri’s receivers in descending preference;receiver_prefs[j]likewise for receiverj. Preference lists may be partial (an unlisted partner is unacceptable) and the two sides may differ in size. Returnsmatchwithmatch[i]the receiver assigned to proposeri(or-1if unmatched). The result is the proposer-optimal stable matching: it is stable (no blocking pair) and every proposer gets the best partner achievable in any stable matching.Reference: Gale & Shapley, “College admissions and the stability of marriage”, Amer. Math. Monthly (1962).
- tsp_held_karp(distance)[source]
Exact minimum-cost Hamiltonian cycle through all nodes (Held-Karp).
distanceis ann x nmatrix of arc costs (may be asymmetric);+infmarks a missing arc. NaN and-infare refused (MXR-080-1903): the DP decides arc presence withnp.isfinite, so both used to read as “missing”, silently answering for a smaller graph – and a-infarc means an unboundedly profitable one, which is not the same fact as an absent one at all. Returns(cost, tour)wheretourstarts at node 0, visits every node once, and the cost includes the closing arc back to 0. RaisesValueErrorwhen the finite arcs admit no Hamiltonian cycle. The Held-Karp bitmask DP is exact inO(2^n n^2)time /O(2^n n)memory, so it is intended for smalln(roughly <= 15-18); beyond that use a heuristic.
- class Solution(value, objective)[source]
Bases:
NamedTupleOne enumerated solution:
value(the solution itself) andobjective(its cost/score).- value: Any
Alias for field number 0
- objective: float
Alias for field number 1
- class SpanningTree(weights)[source]
Bases:
RelationSpanning trees of a weighted undirected graph, enumerated in increasing total edge weight.
The solution value is the list of
(i, j)edges. Non-finite weights are forbidden edges.- Parameters:
weights (np.ndarray)
- class ViterbiPath(log_init, log_trans, log_obs)[source]
Bases:
Relationk most-likely hidden-state sequences of an HMM, enumerated in decreasing joint log-probability.
Standard Viterbi returns only the single best path; this reduces the trellis (nodes
(t, s)) to a longest-log-prob path and yields the topk. The solution value is a length-Tlist of state indices. Delegates tomixle.enumeration.hmm_paths.hmm_best_paths(), whose backward Viterbi completion value is a tight admissible heuristic: emission log-densities may be positive (any density > 1), so a zero-heuristicsense="max"search would pop suboptimal goals first.- Parameters:
log_init (Any) –
log p(state s at t=0), lengthS.log_trans (Any) –
log p(s' | s), shape(S, S).log_obs (Any) –
log p(observation_t | state s), shape(T, S)(emission log-likelihoods).
- best_first_paths(start, successors, is_goal=None, *, sense='min', heuristic=None, max_results=None, return_paths=True)[source]
Lazily enumerate goal states in monotone order of total additive cost/score.
- Parameters:
start (Any) – The initial state.
successors (Callable[[Any], Iterable[tuple[Any, float]]]) –
state -> iterable of (next_state, step)wherestepis the edge cost (sense="min") or edge score (sense="max").is_goal (Callable[[Any], bool] | None) – Predicate; when true for a popped state it is emitted (and not expanded).
None(the default) treats any sink – a state with no successors – as a goal, which covers DAG/trellis/edit-graph searches without a separate goal test.sense (str) –
"min"to minimise total cost (increasing order out) or"max"to maximise total score (decreasing order out).heuristic (Callable[[Any], float] | None) – Optional admissible estimate of the remaining cost (a lower bound,
min) or remaining score (an upper bound,max);0at goals.Noneis the always-admissible zero heuristic (uniform-cost search).max_results (int | None) – Stop after yielding this many goals (
None= exhaust the graph).return_paths (bool) – Yield
(path_list, total)when true, else(goal_state, total).
- Yields:
(path_or_state, total)in best-first order; with an admissible heuristic the order is exact, and on a DAG/trellis the search enumerates k-best paths.- Return type:
- nearest_first(start, neighbors, *, key=None, max_distance=None, max_results=None)[source]
Enumerate distinct states outward from
startin increasing distance (Dijkstra).The dual of
best_first_paths(): instead of enumerating paths to goal states, this enumerates the reachable states themselves, each once, nearest first – an expanding metric “ball” aroundstart.neighbors(state) -> iterable of (next_state, step_cost)with non-negative steps;key(state)gives a hashable identity for de-duplication (default: the state itself). The space may be infinite, so bound it withmax_distanceand/ormax_results(or just consume the lazy iterator finitely).- Yields:
(state, distance)wheredistanceis the shortest total cost fromstart, in nondecreasing order.- Parameters:
- Return type: