Skip to content

Releases: tdelphi1981/LumiX

LumiX 0.3.2

Choose a tag to compare

@tdelphi1981 tdelphi1981 released this 18 Oct 11:05
dab8971

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_version stays 1.
  • lumix.disable_logging() removes the handler enable_logging() added and lets lumix records propagate again.
  • LXCPSATSolver.solve(..., scaling_factor=...) sets the grid of continuous variables for one solve; it is also reachable as LXOptimizer().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 a ValueError naming 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 feasible at 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 reported unbounded.
  • OR-Tools/SCIP reported some unbounded MIPs as optimal with 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 reported unbounded.
  • 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_points and LXLinearizerConfig accepted 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 a ValueError.
  • LXNonLinearFunctions.log, sqrt and tan used sentinel values (±1e10, 0) outside their domain. They now raise a ValueError naming the domain.
  • LXIndicatorTerm accepted any sense string and failed later with a KeyError; it now raises a ValueError at construction.
  • A goal |x| <= 3 on 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 a ValueError.
  • 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 -c pointed at sys:1 instead of the caller's line. The three deepcopy warnings now point at the user's code too.
  • Visualization: a template given to LXVisualizationConfig was overwritten, and theme colours were never used; the dashboard summary and other fixed-height charts were resized to the default height; interactive=False had 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, and threshold defaults to None (a threshold filters by |shadow price|).
  • LXWhatIfResult.delta_percentage is relative to |baseline|, so its sign changes for a negative baseline, and it is nan for a zero baseline (it was 0.0).
  • LXNonLinearFunctions helpers follow the linearizer config's pwl_num_segments and adaptive_breakpoints unless segments is 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_quadratic are keyed by the index keys (key_i, key_j) instead of the canonical positions.
  • enable_logging() sets propagate = False on the lumix logger while its handler is attached.
  • LXVisualizationConfig.template and color_sequence are kept when given; when left out, they follow theme, 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 unbounded instead of feasible when 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) stays feasible with a warning.
  • OR-Tools/SCIP reports unbounded instead of optimal when a value reaches 1e18 or the objective 1e19.
  • LXVisualizationConfig(template=...) defaults to None (filled from theme).
  • Charts that set their own height keep it: configure(height=...) sizes only charts without one.
  • The scenario comparison shows - / N/A instead 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's adaptive_breakpoints (it was True).
  • pwl_num_segments and num_segments must be integers >= 1.
  • LXVariableSensitivity.is_at_bound uses the bounds the model solves with, so a binary at 0 or 1 is at its bound.
  • pct_change reports a change below 1e-9 percent as 0, so tables never print -0.0%.
  • The unused LXSolverInterface._candidates helper is removed.
  • Non-finite coefficients, objective constants and impossible bounds raise a ValueError.

LumiX 0.3.1

Choose a tag to compare

@tdelphi1981 tdelphi1981 released this 17 Oct 19:59
8641571

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 a ValueError naming 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 ValueError naming the row.

Changed

  • A dropped row has no shadow price: solution.get_shadow_price(name) returns None for it.

LumiX 0.3.0

Choose a tag to compare

@tdelphi1981 tdelphi1981 released this 15 Oct 22:05
6bd8b9e

Added

  • LXModel.to_canonical() and LXCanonicalModel: a solver-independent form of a model that every adapter builds from.
  • Coefficients and filters of add_term may take the constraint row instance as a second argument.
  • Scalar variables: an LXVariable without 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 (on solve() or use_solver("cpsat", ...)) accepts coefficients that cannot be represented exactly.
  • LXVariable.cost_expression(), LXModel.cost_objective(), and minimize() / maximize() without an argument, which use the variables' cost() / cost_multi().
  • indexed_by_join(...) builds the (first, second) pairs returned by join_func, keyed by key_func.
  • Quadratic objectives: Gurobi and CPLEX solve them natively (nonconvex included); add_quadratic covers every instance pair of two families with a numeric or coeff(a, b) coefficient, add_squared the diagonal; add_quadratic_term is an alias of add_quadratic. Other solvers raise a clear error, or, with enable_linearization(), linearize the products (see Changed).
  • Expression arithmetic: -, unary -, +=, *=, sum([...]), right-hand +/*, and any real constant (NumPy scalars included).
  • LXSolution.relaxation: True when 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_nonlinear lists the nonlinear objective terms as LXCanonicalNonlinear items, one per variable instance (kinds abs, min, max, product, piecewise; piecewise items carry numeric breakpoints, values and method). LXCanonicalNonlinear has the new fields term (position of the source term in the expression) and key (instance key, None for single variables); schema_version stays 1.
  • Quadratic expression arithmetic: lin - quad, quad - lin, quad - 3, -quad, quad * k and k * quad.
  • On indexed_by_product and join families, .cost(...) and the callable coefficients of add_quadratic / add_squared may take one argument per part (lambda d, t: ...), like cost_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) and piecewise_points(...) in lumix.linearization.techniques.piecewise.
  • Goal programming: LXSolution.goal_stages, a list of LXGoalStage(priority, objective_value, status) (exported from lumix), one per solve of sequential mode; the final stage (model objective and priority-0 goals) has priority 0.
  • LXRowKeyFilter(var, row_key) in lumix.core.expressions: a row-aware filter that keeps, in each row, the instance keyed by row_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; objective then holds the final stage (model objective and priority-0 goals). Empty in weighted mode.
  • solve_goal_programming(model, optimizer, mode=LXGoalMode.SEQUENTIAL) (it raised NotImplementedError); the mode given overrides the model's mode without changing model.goal_mode.
  • SEQUENTIAL_TOLERANCE in lumix.goal_programming.solver: relative slack of the sequential stage bounds (default 0.0, exact).
  • final_objective(model, custom_goals) in lumix.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 with enable_linearization().
  • model_factory= on LXScenarioAnalyzer and LXWhatIfAnalyzer: 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.
  • LXResultStore and LXStoredRun (lumix.analysis.store, needs lumix-opt[orm]): pass result_store= to either analyzer to write every solve to the lumix_runs / lumix_run_values tables (SQLAlchemy), with optional shadow prices and reduced costs; read them back with runs() and values().
  • LXSensitivityAnalyzer.analyze_constraint_family(name) and analyze_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 a NullHandler); calling it again replaces its handler.
  • CPSAT_CAPABILITIES, GLPK_CAPABILITIES, LXPiecewiseLinearizer and LXRowKeyFilter are exported from lumix, as the documentation already showed.
  • LXNonLinearExpression.add_indicator(..., sense="<=", rhs=0.0): indicator rows with >= and == and a right-hand side (any other sense raises ValueError).
  • lumix.solution.row_activity(model, solution) and LXRowActivity: 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_term separately (also after e1 + e2 or expr * 2). Parameters with a default value do not count, so lambda 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 feasible with 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, not optimal; 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 that max_denom cannot represent within 1e-9 relative error, or rows whose common denominator exceeds 64 bits, raise ValueError unless approximate_coefficients=True; scale_objective has 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=True to solve() or use_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_unbounded when 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 with KeyError at solve).
  • A constraint with data but no indexed_by(...) raises ValueError (it built one combined row).
  • A quadratic expression in a constraint raises ValueError.
  • where_multi() on a variable with a single index raises ValueError (it was ignored); use where().
  • Assigning model.objective_expr replaces an objective set by minimize() / maximize() without an argument.
  • LXNonLinearExpression terms (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 (else ValueError); a single variable is broadcast to every instance. LXQuadraticExpression.add_quadratic still multiplies all pairs.
  • Absolute values, min and max that the plain epigraph form cannot represent exactly (a maximized |x| or max, a minimized min, |x| with a negative weight, rows such as |x| >= c, max(...) >= c or min(...) <= c, and every use in an == row) get binary variables and are exact; they need finite bounds on their variables (else ValueError).
  • 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 uses big_m_value with a UserWarning; continuous × continuous uses McCormick envelopes with a UserWarning and sets solution.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 of abs, min, max and 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 families aux_<kind>_<vars>_<n>_<role> indexed by the instance keys of their term; their values appear per instance in solution.variables.
  • Indicator Big-M consta...
Read more

LumiX 0.2.0

Choose a tag to compare

@tdelphi1981 tdelphi1981 released this 14 Oct 11:32
1863a43

Added

  • keys= on LXLinearExpression.add_term, add_multi_term and sum_over: select the
    instances of a term by index key instead of a where filter. Builds the same model,
    much faster for large variable families.
  • LXKeyFilter, the callable that stores keys= (it works anywhere a where works).
  • 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 with where and 0.43 s with keys= in 0.2.0.

LumiX 0.1.2

Choose a tag to compare

@tdelphi1981 tdelphi1981 released this 14 Oct 11:17
2ce3fb0

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" while LXObjectiveSense values 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 through Proto().
  • priority_to_weight(1) returned 1e5 instead of the documented 1e6.
  • Adding two expressions that share a variable silently dropped terms. e1 + e2 combined
    the two where filters with AND, so instances selected by only one of them were lost.
    Calling add_term twice 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 * scalar always raised ValueError. 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.
  • pytest coverage now measures lumix (was the old optixng name).

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 weighs 1e6. 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_term calls) on the same variable
    now contain all of their terms; models that silently lost terms will change.