Repository navigation
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 constants are computed from the bounds of the expression;
big_m_valueis used (with aUserWarning) only when the expression is unbounded. An indicator's binary must be a single variable. - Adaptive piecewise breakpoints are deterministic (quantiles of the curvature mixed with a uniform density) and exactly
num_segments + 1; a linear function gets uniform breakpoints. num_segments,adaptiveandmethodofadd_piecewise,LXPiecewiseLinearTermandLXPiecewiseLinearizer.approximate_functiondefault toNoneand are taken fromLXLinearizerConfig(pwl_num_segments,adaptive_breakpoints,pwl_method); values given on the term win.LXBilinearLinearizer.linearize_bilinearandLXPiecewiseLinearizer.approximate_functiontake single variables (a scalar or a one-instance family).LXOptimizer.solve(model)andmodel.to_canonical()relax goals automatically on a shallow copy; the user's model is not changed andprepare_goal_programming()is no longer needed (calling it still relaxes the goals in place). The goal mode can change between solves.- Priority-0 goals are objective terms, not relaxed rows: a
.ge()goal's expression is maximized and a.le()goal's minimized, times its weight;get_goal_deviations()returnsNonefor them. Priority-0.eq(), indexed or nonlinear goals, and negative or non-integer priorities raiseValueError(integer-valued numbers such asnumpy.int64(1)or2.0are accepted;Trueis not). - The model's own objective (
minimize/maximize, also the cost-based form) is the lowest priority of a model with goals: in weighted mode it is added in minimization form with weight 1, soobjective_valueis the combined value; in sequential mode it is the final stage. A nonlinear objective with goals raisesValueError. - Sequential goal programming is lexicographic: each solved priority is bounded by its optimum (
Σ weight·undesired deviation ≤ optimum + SEQUENTIAL_TOLERANCE·max(1, |optimum|)) in the later stages, and the model objective is solved last. A stage that is not optimal stops the sequence and its solution is returned.LXOptimizer.solve()runs sequential mode itself;solve_sequential()'srelaxed_constraintsis optional;LXGoalProgrammingSolver.solve_sequential()/solve_weighted()solve in their own mode whatever the model's goal mode (a model prepared for the other mode raisesValueError). build_weighted_objectiveskips priority-0 goals.- Solving a model prepared with
prepare_goal_programming()afterset_goal_modechanged its mode raisesValueError(sequential → weighted ignored the goals). LXSolution.is_goal_satisfied(and the "Goals Satisfied" line ofsummary()) checks only the undesired deviations: exceeding a>=goal or staying below a<=goal is satisfied.- A sequential stage that is not solved to optimality is reported with a warning on the
lumix.goal_programming.solverlogger instead ofprint. - Model copies (
copy.deepcopy(model), used by what-if and scenario analysis) share data instances and callables with the original instead of deep-copying them; ORM objects are no longer detached bydeepcopy(an ORM-backed family is queried once at copy time). Detaching is used only when a model is pickled. LXSensitivityAnalyzer.analyze_variable/analyze_constrainttake an instance or row name ("production[widget]","capacity[3]") or a scalar name, and raiseValueErrorfor an indexed family name;analyze_all_variables()/analyze_all_constraints()and the methods built on them (binding constraints, bottlenecks, reports) are keyed per instance and per row.LXWhatIfAnalyzer.find_bottlenecksreturns the improvement per unit in the objective's own direction (positive is better, also for minimization), ranks an unbounded relaxation asinf, skips other relaxations that are not feasible with a warning, and raisesValueErrorwithout a feasible baseline.LXScenarioAnalyzer.get_best_scenario()considers only feasible results and, by default (maximize=None), follows the base model's objective sense; it used to maximize unless told otherwise.- Unknown scenario modification types and target types (e.g.
"objective") raiseValueErrorinstead of being ignored. - What-if
to=on a constraint whose right-hand side is a function sets every row to that value; the change record'soriginal_value/new_value/deltaareNonefor such a constraint.modify_variable_boundfillschanges_applied. LXScenarioAnalyzerandLXWhatIfAnalyzeraccept the model and optimizer as keyword arguments (base_model=/model=,optimizer=).- LumiX prints no log lines by default; call
lumix.enable_logging()or configurelogging.LXModelLogger(name, level=None)sets a level only whenlevelis given and never adds a handler. LXSolution.get_total_deviation(name)sums only the undesired deviations (an over-achieved>=goal no longer reports a deviation); goal charts and the dashboard use it.LXGoalStage.objective_valueisNonefor a stage without a feasible solution (it was 0).LXSolutionVisualizer.plot_constraint_utilization()needs the model (LXSolutionVisualizer(solution, model)) and no longer needs duals.- Dashboard and scenario charts no longer prefix objective values with
$. LXConstraintSensitivity.is_bindingfollows the row's slack (zero slack is binding), not a nonzero shadow price; degenerate binding rows are now reported as binding.find_bottleneckstries equality rows in both directions and reports the better one.model_factorymust return new constraint and variable objects on every call.LXResultStoretables:lumix_runs.scenarioandlumix_run_values.nameareTEXT.LXSolution._undesired_deviationsmoved to the end of the dataclass fields and is excluded from comparisons.- OR-Tools and CP-SAT report
time_limit(it wasnot_solved/unknown) when the time limit stops the search before any solution, as Gurobi and GLPK do. LXModel.add_constraintraisesValueErrorfor a second constraint with the same name (it hid the first one from duals, activity and analysis).by_percentin what-if changes moves the right-hand side by a share of its absolute value (-10relaxed by 20 % for a>=row is-12);tighten_constrainton an equality lowers the right-hand side (it raised it, likerelax_constraint).- A goal solve without a solution has no goal deviations:
get_goal_deviationsandis_goal_satisfiedreturnNone, andsummary()has no goal lines. LXVariable.bounds()raisesTypeErrorfor a function (per-instance bounds were documented but crashed at solve time); use an indexed constraint for per-instance limits.- What-if
delta_objective/delta_percentagearenanwhen the baseline or the changed model has no solution, andsensitivity_rangereportsnanfor such points (an infeasible run used to count as a change to an objective of 0). compare_scenarios()lists scenarios best first in the model's sense with runs without a solution last, shown as-.
Deprecated
LXLinearizerConfigfieldsdefault_method,tolerance,verbose_logging,prefer_sos2,auto_detect_boundsandmccormick_tighten_boundshave no effect and issue aDeprecationWarningwhen set to a non-default value; so do theprefer_sos2andmccormick_tighten_boundsarguments ofenable_linearization(). They will be removed in 0.4.0.extract_custom_objectivesalways returns an empty list (priority 0 goals are objective terms, not relaxed rows) and issues aDeprecationWarning; usefinal_objective(model, custom_goals). It will be removed in 0.4.0.
Fixed
- A1: a constant in a constraint's left-hand side was ignored by OR-Tools, CP-SAT, CPLEX and GLPK.
- A2: repeated terms on one variable overwrote each other (OR-Tools rows/objective, CPLEX/GLPK objective; GLPK aborted the process).
- A3: binary bounds were ignored and fractional integer bounds truncated (OR-Tools, CP-SAT).
- A4: an empty data family became one phantom variable or one constraint row.
- A5: same-type constraint rows dropped other instances even when the coefficient referenced the row; CP-SAT did not narrow at all.
- A6: CP-SAT scaled coefficients without scaling the right-hand side, rounded the objective constant, corrupted rows mixing integer and scaled continuous variables, kept scaling state between solves, capped open bounds at INT32 and rounded tiny coefficients to 0.
- A7: Gurobi discarded the incumbent on time, node and solution limits.
- A8: CPLEX status codes were mapped from wrong literals (infeasible MIPs reported as feasible).
- B1: OR-Tools crashed on integer variables with an open bound.
- B2: OR-Tools ignored
gap_toleranceandthreads;use_solver(**kwargs)and lateruse_solver()/enable_*()calls after a solve were ignored. - B3: CP-SAT gap used an unscaled bound; CP-SAT warned about
enable_sensitivityon every solve. - B4: NamedTuple data was unpacked into several arguments in
sum_over. - B10: GLPK reported infeasible/unbounded problems as "undefined" and printed solver output for MIPs.
- Gurobi INF_OR_UNBD is resolved to infeasible or unbounded; CPLEX
output_clonelogworks. - A9:
.rhs(value).rhs(func)kept the value; the last call now wins. - A10:
as_goal()captured the constraint sense at call time (.as_goal(1).ge()was relaxed as<=). - A11: a constraint with data but no
indexed_bysilently built one combined row. - A12:
cost()/cost_multi()were never used. - A13:
where_multibeforeindexed_by_product, and.where()on product variables, were ignored. - B5:
indexed_by_joindid nothing. - B6: quadratic objectives crashed every adapter;
LXQuadraticExpression.__add__dropped the other expression. - A14: binary × continuous products had wrong signs in two of their four constraints, and the product's bounds excluded 0 when the continuous variable's lower bound was positive.
- A15: linearization modified the user's objective expression, so repeated solves drifted and a second
linearize_model()call added duplicate auxiliary variables. - A16: nonlinear terms on variable families acted on the sum of the family instead of each instance.
- A17: a maximized
|x|, a minimizedminor a maximizedmaxreturned wrong optima; two terms on the same variables created auxiliary variables with the same name. - A18: McCormick relaxations were reported as optimal without a warning; integer × integer products were relaxed instead of exact.
- A19: indicator Big-M rows had a wrong sign,
>=and==indicators crashed, and the constant of the expression was dropped. - A20:
LXNonLinearFunctions.powerreturned 0 for negative inputs; a fractional exponent on a domain with negative values raisesValueError. - B7: nonlinear terms in constraints crashed.
- B8: adaptive piecewise breakpoints were random and one short of
num_segments + 1. - B9:
LXLinearizerConfigsettings were never applied to piecewise terms. - The constant of an
LXNonLinearExpressionobjective was dropped. - A21: sequential goal programming was not lexicographic: earlier priorities were not bounded, so the result was that of the last priority alone.
- A22: every row of an indexed goal contained the deviations of all rows; each row now has its own pair, named
<name>_<key>. - A25: priority-0 goals optimized nothing, and the model's own objective was silently replaced by the goal objective.
- A26: goals were solved as hard constraints unless
prepare_goal_programming()was called first. - Goals with an
LXNonLinearExpressioncrashed during relaxation. - A23: what-if and scenario copies deep-copied data instances while lambdas kept the originals, so identity checks and instance-keyed dicts in closures gave wrong results or
KeyError. - A24: scenario
add=/multiply=did nothing on a constraint whose right-hand side is a function; they now apply row by row and compose in order. - B11: sensitivity analysis reported no shadow prices for indexed constraints and only the first instance (without a reduced cost) for a variable family.
- B12:
find_bottlenecksranked minimization models backwards and failed on constraints whose right-hand side is a function. - B15: a copy of a model built from SQLAlchemy objects with unloaded relationships failed;
detach_orm_objectnow loads them while a session is available. get_best_scenariocould return an infeasible scenario.- B13:
LXRationalConverter(method="continued_fraction")raisedZeroDivisionErrorfor every non-integer. - B14:
LXDashboard.plot()crashed unless both sensitivity and goal data were present. - B16: every
LXOptimizerreset thelumix.optimizerlogger to INFO and added a console handler, so applications could not silence LumiX and saw each line twice under a configured root logger. - B17: README snippets (
LXBilinearTerm(..., bounds_x=...),LXPiecewiseLinearTerm(variable=..., slopes=...),add_scenario("base", {}),solve_all()) and documented imports (from lumix import CPSAT_CAPABILITIES, GLPK_CAPABILITIES, LXPiecewiseLinearizer) failed. - The constraint-utilization chart drew every non-binding row at a fixed 50 %; it now shows LHS / RHS of each row.
- The scenario waterfall accumulated the scenario differences; each scenario is now a bar from the baseline to its own objective, and infeasible scenarios (reported objective 0) are left out of every scenario chart and ranking.
- The goal deviation waterfall's "Total" bar left out the last goal.
- Linearization warnings (McCormick, big-M, indicator, piecewise domain, deprecated settings) pointed at LumiX internals instead of the user's line; the deprecated-setting warning of
enable_linearization(...)was therefore hidden by Python's default filters. - A zero-weight
add_abs(x, 0)oradd_product(x, y, 0)raised "needs finite bounds". - In an indexed constraint, a nonlinear term over two families of different data types was narrowed (or not) depending on argument order.
- Auxiliary variables of a term on a single variable were reported as
{None: value}. - Sequential goal programming rebuilt the stage list for every stage solve.
- Index keys of an indexed goal that differ but print the same (
1and"1") raised an internal "duplicate index key" error; the error now names the goal. find_bottlenecksmoved equality rows only upwards, so it could miss or misreport an improvement.- A
model_factorythat reused a constraint or variable object of the base model let what-if and scenario changes modify the base model; it now raisesValueError. LXWhatIfAnalyzer(baseline_solution=...)never recorded the given baseline in the result store.LXSensitivityAnalyzerdid not see constraints added after its first lookup, and a lookup of a scalar constraint enumerated every indexed family.LXConstraintSensitivity.slackwas never filled.- Documentation pages called API that does not exist (
identify_binding_constraints,set_variable_bound,apply_change,get_non_binding_constraints,LXCustomLinearizer,lower_bound_func), used goals with data but noindexed_by, treated a goal's deviations as a number, and described a linearization engine LumiX no longer has; code includes in the examples pages had drifted into docstrings. LXRationalConverterreported an error of2|x|for exact negative values withmethod="continued_fraction", and an error of 0 for any value it rounded to an integer.- The README's "Advanced Analysis" snippet used constraint names that the Simple Example does not define.
- CPLEX reported an LP stopped by a limit as
feasiblewith a point that violates the constraints; such a stop is nowtime_limitwithout values. - CPLEX reported some infeasible LPs as
unbounded, GLPK reported infeasible LPs asunboundedand infeasible MIPs asinf_or_unbounded, and OR-Tools (SCIP) reported a feasible unbounded MIP asinfeasible. - GLPK aborted the Python process, and Gurobi raised, on a variable or constraint name longer than 255 characters or with a control character; names given to the solvers are now shortened (with a hash) and cleaned.
- GLPK never freed its problems, so memory grew with every solve.
- CP-SAT widened equalities that have an exact solution on its grid when another equality had none (
x + y == 1gave 1.001), and failed with a rawTypeErroron a very large finite bound (it is now capped like an open bound). - Gurobi's documented
LogToConsole=1printed nothing; documented CPLEX parameters such asmip_limits_cutsfactorwere skipped as unknown, and the CPLEX log could not be shown (verbose=Truenow shows it; any CPLEX parameter can be given by its path,_for.). - CPLEX warns when it cannot bound a nonconvex objective (status 133) instead of reporting the point it stopped at as an ordinary feasible solution.
- A sequential goal model without its own objective that was prepared with
prepare_goal_programming()was analysed as a maximization:find_bottlenecksreported improvements with the wrong sign andget_best_scenariopicked the scenario with the largest deviation. - A goal model whose solve found no solution (infeasible, unbounded) reported every goal as satisfied with zero deviation.
row_activityand sensitivityslack/is_bindingreported a goal's relaxed row (deviations included, always binding) instead of the goal itself.LXLinearizer(...).linearize_model()returned a model without the goal mode and theprepare_goal_programming()state, so a sequential goal model was solved in weighted mode (or with no objective at all).- What-if
relax_constraint/tighten_constraint/increase_constraint_rhs/decrease_constraint_rhswithby_percentmoved a negative right-hand side the wrong way, and relaxing a prepared>=goal raised its target. add_abs,add_min,add_max,add_product,add_piecewiseandadd_indicatoraccepted an expression where a variable is needed and failed at solve time withAttributeError; they now raiseTypeErrorat once.- A variable with data but no
indexed_byover unhashable instances (a plain@dataclass) failed with an internalTypeError; the error now says to addindexed_by. - Scenario charts: the comparison bar marked the largest objective as "Best" (the worst scenario when minimizing, or an infeasible one); the percentage, decision-variable and radar charts still drew infeasible scenarios, the radar ignored the objective sense, and
LXScenarioCompare.plot()divided by zero on a zero baseline. - The dashboard's "Variable Values" panel looked only at the first five instances of each family and could stay empty for a solution with nonzero values.
- The "Constraint Status" panel of
LXSolutionVisualizer.plot()counted binding rows by nonzero dual; with the model it now uses the slack, asis_bindingdoes. LXRationalConverter(method="stern_brocot")never considered the upper starting bound and could be far less accurate than Farey (2.99999999 gave 29999/10000).- CPLEX's documented
mip_emphasisparameter pointed at a parameter path that does not exist and was skipped. - Documentation: loop examples whose lambdas read the loop variable after the loop (every row used the last item),
from_data(LXCartesianProduct(...)), per-instance bounds, constraints with data but noindexed_by,add_piecewise(slopes=), nonlinear-function examples that did not run or left out the auxiliary rows, solver pages that used API that does not exist (solution.mip_gap,solution.variable_values,model.copy(), ...), and logger names thatlumix.enable_logging()does not reach. - Building rows of a constraint family over the variable's own data type took quadratic time (8,000 rows: 4.9 s); it is linear again (0.13 s).
- The scenario waterfall used an infeasible baseline as its base, and the comparison bar and sensitivity curve drew infeasible results at 0;
compare_scenarios()sorted largest-first whatever the objective sense and printed infeasible runs as 0.00 / -100 %. row_activity,LXSolutionVisualizer.plot()andplot_constraint_utilization()raised on a model with a nonlinear term in a constraint (rows with nonlinear terms are now left out).- The golden LP files of the model tests were ignored by
.gitignore, so the test failed on a fresh clone.