Repository navigation
Releases: tdelphi1981/LumiX
Releases · tdelphi1981/LumiX
Release list
LumiX 0.3.2
Added
LXCanonicalModel.goal_deviations: for each relaxed goal, the names of its goal row family and of its positive and negative deviation families. Empty for a model without goals.schema_versionstays 1.lumix.disable_logging()removes the handlerenable_logging()added and letslumixrecords propagate again.LXCPSATSolver.solve(..., scaling_factor=...)sets the grid of continuous variables for one solve; it is also reachable asLXOptimizer().use_solver("cpsat", scaling_factor=...); it must be an integer >= 1.
Fixed
- An infinite or NaN coefficient (linear, quadratic,
abs/min/max/product) was dropped silently by the noise filter, so a different model was solved. It now raises aValueErrornaming the place and the variable instance. An infinite objective constant and variable bounds that no value satisfies ([inf, inf], NaN) raise as well. Open bounds (None,±inf) are unchanged. - CP-SAT reported an unbounded integer model as
feasibleat its open-bound cap. When a variable sits on the cap of an optimal solve, LumiX now solves the LP relaxation of the model with GLOP; if it is unbounded, the model is reportedunbounded. - OR-Tools/SCIP reported some unbounded MIPs as
optimalwith values near SCIP's infinity (objective -1.97e19). An optimum with a value of 1e18 or more, or an objective of 1e19 or more, is now reportedunbounded. - CP-SAT's overflow error for a row with a huge value talked about a "common denominator of 1"; it now names the value and the 64-bit limit.
- CPLEX solved every quadratic objective for the global optimum (optimality target 3), which gives no duals, so a convex QP had no shadow prices. The global target is now used only when CPLEX reports the objective is not convex.
- A row of one NamedTuple type was narrowed by value against instances of another NamedTuple type with equal fields.
piecewise_pointsandLXLinearizerConfigaccepted 0 or a negative number of segments; a function value that is not finite at a breakpoint was used as it is. Both now raise aValueError.LXNonLinearFunctions.log,sqrtandtanused sentinel values (±1e10, 0) outside their domain. They now raise aValueErrornaming the domain.LXIndicatorTermaccepted any sense string and failed later with aKeyError; it now raises aValueErrorat construction.- A goal
|x| <= 3on a free variable could not be linearized (an equality on a convex term needs finite bounds). One-sided goals with nonlinear terms are relaxed one-sided, so the term stays convex. solve_goal_programming(..., mode="weighted")ran the sequential mode: a string never equalled the enum. Strings are accepted in any case, and an unknown mode raises aValueError.LXWhatIfAnalyzer, the scenario comparison report and the scenario charts computed the percent change in three ways; the sign was wrong for a negative baseline. They now share one definition,(new - base) / |base| * 100.LXSensitivityAnalyzer.get_binding_constraints()returned nothing without duals (MIPs, CP-SAT). Binding now means zero slack.enable_logging("VERBOSE")attached its handler before failing, and records were printed twice when the root logger also had a handler.- Warnings raised from code run with
python -cpointed atsys:1instead of the caller's line. The three deepcopy warnings now point at the user's code too. - Visualization: a
templategiven toLXVisualizationConfigwas overwritten, and theme colours were never used; the dashboard summary and other fixed-height charts were resized to the default height;interactive=Falsehad no effect; the "enable sensitivity analysis" hint sat on the wrong panel; an infeasible solution was titled "Objective: 0.00"; utilization percentages were drawn for>=,==and non-positive rows;plot_satisfaction_status(tolerance=0)used the default tolerance; waterfall and shadow-price colours ignored the objective sense. - Docs: the rational conversion guide showed outputs from another algorithm (
355/113); the quickstart showed a non-optimal plan; example 04 required CPLEX and exited with code 0 on errors.
Behavior changes
binary()keeps bounds set before it (the model intersects them with [0, 1]); it used to reset them to [0, 1].- Rows are narrowed by value only when they have the type of the variable's instances or a built-in type.
get_binding_constraints()is slack-based, andthresholddefaults toNone(a threshold filters by|shadow price|).LXWhatIfResult.delta_percentageis relative to|baseline|, so its sign changes for a negative baseline, and it isnanfor a zero baseline (it was 0.0).LXNonLinearFunctionshelpers follow the linearizer config'spwl_num_segmentsandadaptive_breakpointsunlesssegmentsis given (each helper had its own fixed count, 20-50).- A one-sided goal with nonlinear terms has one deviation; the other is fixed to 0. Linear goals are unchanged.
- The auxiliary families of a linearized
add_quadraticare keyed by the index keys(key_i, key_j)instead of the canonical positions. enable_logging()setspropagate = Falseon thelumixlogger while its handler is attached.LXVisualizationConfig.templateandcolor_sequenceare kept when given; when left out, they followtheme, so non-default themes now use their own colours.- Constraint utilization is shown only for
<=rows with a positive right-hand side (other rows have no percentage). - CP-SAT reports
unboundedinstead offeasiblewhen a variable reaches the open-bound cap and the model's LP relaxation is unbounded. An optimum that only lies beyond the cap (a large finite bound or row) staysfeasiblewith a warning. - OR-Tools/SCIP reports
unboundedinstead ofoptimalwhen a value reaches 1e18 or the objective 1e19. LXVisualizationConfig(template=...)defaults toNone(filled fromtheme).- Charts that set their own height keep it:
configure(height=...)sizes only charts without one. - The scenario comparison shows
-/N/Ainstead of a percentage for an infeasible scenario or a zero baseline. get_binding_constraints(threshold=t)with an explicit threshold also requires zero slack, so it can return fewer rows than before.LXNonLinearFunctions.custom(adaptive=...)defaults to the config'sadaptive_breakpoints(it wasTrue).pwl_num_segmentsandnum_segmentsmust be integers >= 1.LXVariableSensitivity.is_at_bounduses the bounds the model solves with, so a binary at 0 or 1 is at its bound.pct_changereports a change below 1e-9 percent as 0, so tables never print-0.0%.- The unused
LXSolverInterface._candidateshelper is removed. - Non-finite coefficients, objective constants and impossible bounds raise a
ValueError.
LumiX 0.3.1
Fixed
- A constraint with an infinite right-hand side was solved as if the right-hand side were 0 on every solver:
.le().rhs(math.inf)became<= 0. The canonical model now drops a row that an infinite right-hand side makes bound nothing (<= +inf,>= -inf), so no solver receives an infinite value, and raises aValueErrornaming the row for one that no point can satisfy (>= +inf,<= -inf,== +-inf). An infinite constant in the expression counts the same way. - A NaN right-hand side was accepted silently. It now raises a
ValueErrornaming the row.
Changed
- A dropped row has no shadow price:
solution.get_shadow_price(name)returnsNonefor it.
LumiX 0.3.0
Added
LXModel.to_canonical()andLXCanonicalModel: a solver-independent form of a model that every adapter builds from.- Coefficients and filters of
add_termmay take the constraint row instance as a second argument. - Scalar variables: an
LXVariablewithout a data source is one variable whose solution value is a number (documented before, but it raised "has no data source"). - CP-SAT:
approximate_coefficients=True(onsolve()oruse_solver("cpsat", ...)) accepts coefficients that cannot be represented exactly. LXVariable.cost_expression(),LXModel.cost_objective(), andminimize()/maximize()without an argument, which use the variables'cost()/cost_multi().indexed_by_join(...)builds the(first, second)pairs returned byjoin_func, keyed bykey_func.- Quadratic objectives: Gurobi and CPLEX solve them natively (nonconvex included);
add_quadraticcovers every instance pair of two families with a numeric orcoeff(a, b)coefficient,add_squaredthe diagonal;add_quadratic_termis an alias ofadd_quadratic. Other solvers raise a clear error, or, withenable_linearization(), linearize the products (see Changed). - Expression arithmetic:
-, unary-,+=,*=,sum([...]), right-hand+/*, and any real constant (NumPy scalars included). LXSolution.relaxation:Truewhen the solved model was linearized with a relaxation (McCormick envelopes for a product of two continuous variables); the objective value is then a bound.LXModel.to_canonical().objective_nonlinearlists the nonlinear objective terms asLXCanonicalNonlinearitems, one per variable instance (kindsabs,min,max,product,piecewise; piecewise items carry numeric breakpoints, values and method).LXCanonicalNonlinearhas the new fieldsterm(position of the source term in the expression) andkey(instance key,Nonefor single variables);schema_versionstays 1.- Quadratic expression arithmetic:
lin - quad,quad - lin,quad - 3,-quad,quad * kandk * quad. - On
indexed_by_productand join families,.cost(...)and the callable coefficients ofadd_quadratic/add_squaredmay take one argument per part (lambda d, t: ...), likecost_multi. - Nonlinear terms in constraints are linearized; the direction comes from the constraint sense, and in an indexed constraint a row that is an instance of the term's family uses only that instance.
adaptive_breakpoints(func, x_min, x_max, num_segments)andpiecewise_points(...)inlumix.linearization.techniques.piecewise.- Goal programming:
LXSolution.goal_stages, a list ofLXGoalStage(priority, objective_value, status)(exported fromlumix), one per solve of sequential mode; the final stage (model objective and priority-0 goals) has priority 0. LXRowKeyFilter(var, row_key)inlumix.core.expressions: a row-aware filter that keeps, in each row, the instance keyed byrow_key(row); the canonical builder looks it up by key, so such rows build in linear time.LXModel.to_canonical().goal_stages: in sequential goal mode, the stage objectives ({position: coeff}) in priority order;objectivethen holds the final stage (model objective and priority-0 goals). Empty in weighted mode.solve_goal_programming(model, optimizer, mode=LXGoalMode.SEQUENTIAL)(it raisedNotImplementedError); the mode given overrides the model's mode without changingmodel.goal_mode.SEQUENTIAL_TOLERANCEinlumix.goal_programming.solver: relative slack of the sequential stage bounds (default0.0, exact).final_objective(model, custom_goals)inlumix.goal_programming.objective_builder: the model's objective combined with the priority-0 goals.- Goals whose expression is an
LXNonLinearExpression(e.g.add_abs); solve them withenable_linearization(). model_factory=onLXScenarioAnalyzerandLXWhatIfAnalyzer: a function that builds the model, called once for the base model and once per scenario or what-if run instead of copying the model (for ORM models, open a fresh session inside it). It must return a new model on every call.LXResultStoreandLXStoredRun(lumix.analysis.store, needslumix-opt[orm]): passresult_store=to either analyzer to write every solve to thelumix_runs/lumix_run_valuestables (SQLAlchemy), with optional shadow prices and reduced costs; read them back withruns()andvalues().LXSensitivityAnalyzer.analyze_constraint_family(name)andanalyze_variable_family(name): every row or instance of a family, keyed by index key.lumix.enable_logging(level="INFO", stream=None, fmt=...): prints LumiX's log records (LumiX itself attaches only aNullHandler); calling it again replaces its handler.CPSAT_CAPABILITIES,GLPK_CAPABILITIES,LXPiecewiseLinearizerandLXRowKeyFilterare exported fromlumix, as the documentation already showed.LXNonLinearExpression.add_indicator(..., sense="<=", rhs=0.0): indicator rows with>=and==and a right-hand side (any other sense raisesValueError).lumix.solution.row_activity(model, solution)andLXRowActivity: the left-hand side, right-hand side, slack and binding state of every constraint row at a solution.- A "Correctness Notes" page in the user guide: what LumiX solves exactly, what it approximates, and how it is verified.
Behavior changes
Models that were solved wrongly before now give different (correct) results; every entry below that changes a result or raises where 0.2.x did not is listed here. lumix-ir's API surface is unchanged.
- Same-type constraint narrowing applies only to one-argument coefficients and filters, decided for each
add_termseparately (also aftere1 + e2orexpr * 2). Parameters with a default value do not count, solambda item, g=g: ...is a one-argument filter. - A variable or constraint family whose instances repeat an index key raises
ValueError. - Sums that cancel to floating-point noise (
0.1 + 0.2 - 0.3) are treated as 0 in rows and right-hand sides. - A variable family with empty data has no variables and logs a warning (it used to become one unindexed variable).
- Gurobi returns
feasiblewith the incumbent when a limit stops the search. - CP-SAT: open variable bounds become at most ±2^40 (smaller when large coefficients would overflow a row), and a solution that reaches such a bound is
feasible, notoptimal; equalities on scaled continuous variables that have no exact grid solution are solved again as bands of half a grid step per variable (then a whole step), and the largest deviation is logged — a feasible continuous model is no longer reported infeasible; coefficients thatmax_denomcannot represent within 1e-9 relative error, or rows whose common denominator exceeds 64 bits, raiseValueErrorunlessapproximate_coefficients=True;scale_objectivehas no effect. - A solve without a solution (infeasible, unbounded) returns
variables == {}on every adapter (OR-Tools returned zeros, CPLEX returned values for unbounded MIPs). - Gurobi no longer prints its log; pass
verbose=Truetosolve()oruse_solver("gurobi", ...)to see it. - OR-Tools (GLOP) and CPLEX re-solve once without presolve when presolve cannot tell an infeasible LP from an unbounded one; GLPK settles a MIP with an unbounded relaxation by a feasibility check (at most 2 s) and reports
inf_or_unboundedwhen that does not finish. - OR-Tools returns no duals or reduced costs for MIPs (SCIP has none; they were reported as 0).
+and*on expressions return a new expression and no longer modify the left operand; use+=/*=for in-place changes.- Adding a second variable family with an existing name raises
ValueError(it crashed withKeyErrorat solve). - A constraint with data but no
indexed_by(...)raisesValueError(it built one combined row). - A quadratic expression in a constraint raises
ValueError. where_multi()on a variable with a single index raisesValueError(it was ignored); usewhere().- Assigning
model.objective_exprreplaces an objective set byminimize()/maximize()without an argument. LXNonLinearExpressionterms (add_abs,add_min,add_max,add_product,add_piecewise) on variable families apply to every instance and the results are summed (add_abs(x)is the sum of|x[i]|). The families of one term must have the same index keys (elseValueError); a single variable is broadcast to every instance.LXQuadraticExpression.add_quadraticstill multiplies all pairs.- Absolute values, min and max that the plain epigraph form cannot represent exactly (a maximized
|x|ormax, a minimizedmin,|x|with a negative weight, rows such as|x| >= c,max(...) >= cormin(...) <= c, and every use in an==row) get binary variables and are exact; they need finite bounds on their variables (elseValueError). - Products: binary × bounded variable and integer × (integer or continuous) are exact (the integer factor is expanded in binary digits, limited by
LXLinearizerConfig.binary_expansion_bits); binary × unbounded usesbig_m_valuewith aUserWarning; continuous × continuous uses McCormick envelopes with aUserWarningand setssolution.relaxation. - With
enable_linearization(), quadratic objectives on OR-Tools, GLPK and CP-SAT are linearized with the same product rules (families, integer and continuous factors included); on Gurobi and CPLEX they stay native. Auxiliary variables ofabs,min,maxand products of integer variables (whole coefficients) are integer, so CP-SAT accepts them; piecewise terms and McCormick envelopes use continuous auxiliaries and need a MIP solver. linearize_model()never modifies the user's model and starts afresh on every call. Auxiliary variables are familiesaux_<kind>_<vars>_<n>_<role>indexed by the instance keys of their term; their values appear per instance insolution.variables.- Indicator Big-M consta...
LumiX 0.2.0
Added
keys=onLXLinearExpression.add_term,add_multi_termandsum_over: select the
instances of a term by index key instead of awherefilter. Builds the same model,
much faster for large variable families.LXKeyFilter, the callable that storeskeys=(it works anywhere awhereworks).LXVariable.index_key(instance): the key adapters use for an instance.- User guide: Performance Considerations.
Changed
- Solver adapters fetch each variable family's instances once per build instead of once
per constraint row. ORM-backed families are queried once per build. The cache is cleared
on every build. - Solver adapters no longer re-check terms whose instances were already selected by key.
Performance
- Transportation model with 600 rows and 90,000 variables (OR-Tools): build time 4.78 s in
0.1.2; 2.88 s withwhereand 0.43 s withkeys=in 0.2.0.
LumiX 0.1.2
Added
- CONTRIBUTING.md with comprehensive development guidelines
- CODE_OF_CONDUCT.md with academic-focused community standards
- SECURITY.md with vulnerability reporting policy
- This CHANGELOG.md to track project changes
- Regression tests for the linearizer (both objective senses), the CP-SAT adapter and expression arithmetic
Fixed
- Linearizer kept every objective as a minimization.
LXLinearizer.linearize_model()
compared the objective sense to"maximize"whileLXObjectiveSensevalues are
"min"/"max". Maximized product, min and piecewise objectives returned wrong optima
(example 06 reported an area of 16 instead of 100). LXOptimizer.enable_linearization()had no effect.solve()now linearizes the model
with the solver's capabilities when linearization is enabled and the model needs it.- CP-SAT crashed the process with OR-Tools ≥ 9.13. Constraints are now named with
with_name()instead of writing throughProto(). priority_to_weight(1)returned1e5instead of the documented1e6.- Adding two expressions that share a variable silently dropped terms.
e1 + e2combined
the twowherefilters with AND, so instances selected by only one of them were lost.
Callingadd_termtwice for the same variable overwrote the first term. Both now keep every
term: an instance is included when either term includes it, with the sum of the
coefficients that apply to it. expr * scalaralways raisedValueError. It now scales every term (including
multi-indexed terms) and the constant.- Version strings in
lumix.__version__and the documentation now match the package version. pytestcoverage now measureslumix(was the oldoptixngname).
Behavior changes
These fixes intentionally change results of code that relied on the old behavior:
- Maximized nonlinear objectives solved through the linearizer now return the correct
(different) optimum. - Goal-programming objective values are 10× larger with the default weights because
priority 1 now weighs1e6. The ranking of priorities is unchanged; the optimal plan is
usually the same. enable_linearization()now linearizes. Models that were linearized by hand are not
linearized twice:needs_linearization()is false for them.- Expressions built by adding expressions (or repeated
add_termcalls) on the same variable
now contain all of their terms; models that silently lost terms will change.