Repository navigation
feat(api): a result gives the rate at which its objective moves with each value of a parameter - #2073
Conversation
…each value of a parameter Result.marginal(name) returns one row per row of the parameter's table: the derivative of the Lagrangian in that row, with every mask held as built. The compiler reads the one varied leaf as a term in a read, so the objective, each constraint row weighted by its dual, and each binding bound's reduced cost add up to the rate. Live results and archives answer; a parameter under a divisor, in a power or squared is refused. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018B3nA1Q4XLWBq2jJFFLFWR
|
Important Review skippedAuto reviews are disabled on base/target branches other than the default branch. Please check the settings in the CodeRabbit UI or the
|
Merging this PR will regress 2 benchmarks1 warning
Warning Please fix the performance issues or acknowledge them on CodSpeed. Performance ChangesTip Investigate this regression by commenting Comparing Footnotes
|
Documentation build overview
114 files changed ·
|
…d for Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018B3nA1Q4XLWBq2jJFFLFWR
… its pull request Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018B3nA1Q4XLWBq2jJFFLFWR
…eads Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018B3nA1Q4XLWBq2jJFFLFWR
…an input is worth, and which reader answers it The marginal leaves the change-and-solve-again tutorial, whose path it broke, for a how-to beside the dual and the reduced cost. It ranks inputs by a 1% rise and says when a sweep answers instead. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018B3nA1Q4XLWBq2jJFFLFWR
…ual, beside dual and reduced_cost GAMS calls both a dual and a reduced cost a marginal, so `marginal` read as the reduced cost to much of the audience. The reader is the dual of a parameter, in dual's convention. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018B3nA1Q4XLWBq2jJFFLFWR
Note
The following content was generated by AI.
result.parameter_dual('p_max')returns the dual of each row of a parameter: the rate at which the optimal objective rises with it, in the parameter's own table shape. Every mask is held as built. Checked against central finite differences on HiGHS, Gurobi and Xpress.Follow-up, built in #2074: at a mask threshold, report the rate as the value rises past it, or ±inf where the objective jumps, from the current solve and with no re-solve. #2074 holds the cases it decides and the ones it leaves as built.
Definition, how it works, verification, what was left out
Definition
The rate of the optimal objective per unit rise of one row of the parameter, in
dual's sign convention, for every sense and sink. It is the derivative of the Lagrangian in that row, with the model held as built:A mask threshold is treated like a basis change: the rate is one-sided there, as
dualalready is at a degenerate solve. The docstring says so in one sentence and namesModel.updateplus a re-solve for the other side.How
Compiler.variednames the one leaf that stays a term in a read. A parameter row is a column of its own, labelled by its row-major position. A variable keeps its own columns.readback.parameter_dualcompiles the objective and each constraint that reads the parameter, weights each row bydual(c), and adds the coefficients up per label.scan_archive(...).result.parameter_dual) and results afterModel.updateanswer. A result loaded withload_resulthas no model behind it, and says so.Model._evaluatorbecameModel._readers, returning the evaluator and the parameter dual.tests/differential.pyfollows the rename._engine/readback.pynames the parameter dual.docs/howto/prices.md), reads the dual, the reduced cost and the parameter dual by the question each answers. It ranks inputs by a 1% rise, and says when a scenario sweep answers instead.Your first modelandComing from linopylink to it. The tutorialChange it and solve againkeeps one sentence pointing there.The name
The reader started as
marginaland was renamedparameter_dualina861e06. GAMS calls both a dual and a reduced cost a marginal (.m), somarginalread as the reduced cost to much of the audience. A singlemarginalover constraints, variables and parameters was weighed and not taken: a constraint may share its name with a variable or a parameter, anddual, the archive'sdual/and the language'sdual(c)would lose their word. The three readers now read as one family:dual,reduced_cost,parameter_dual.Refused
parameter_dualis called, before any compile. The message names the rewrite: declare the quantity as a parameter of its own.dual's message.Verified
85d4e5d,pixi run check: 5891 passed, 579 skipped, 4 xfailed.38d37d6turns an unreachable guard into an assert (below). On it,tests/test_marginal.py(123 passed, 5 skipped), lint and typecheck were run. The whole suite was not re-run unmutated.8b18858:pixi run docs-build(strict),docs-test(116 passed),test_changelog,test_doc_examplesandtest_docs_site(162 passed) are clean.bc8e234(the how-to):pixi run docs-build(strict),docs-test(116 passed),test_doc_examplesandtest_docs_site(123 passed) are clean. The suite was not re-run for that docs-only commit.a861e06(the rename):pixi run check5892 passed, 579 skipped, 4 xfailed;docs-build(strict) anddocs-test(116 passed) are clean.test-via-pyarrow,test-floors,test-bench.tests/test_parameter_dual.py, 123 cases on HiGHS, Gurobi and Xpress (Xpress skips the quadratic model). Each row of each parameter is moved by ±1e-4, re-solved, and compared with the parameter dual at rel 1e-5. The models cover:test_reduced_cost);missing: absent.parameter_dual('load') == dual('balance'), andparameter_dual('cost')equals the summed primal.Mutation table
Each guard was deleted in turn and the full suite was run. The first eight rows come from
tools/mutate.py. The last four were taken by hand with the same precautions (git checkout --restore,__pycache__dropped on both sides), because the tool deletes lines and cannot negate a branch or change a constant. The tree came back clean after every run.KeyError(readback.parameter_dual)readback.parameter_dual)Result.parameter_dualchecks first, so no caller reaches it. Now an assert (38d37d6)Result.parameter_dual)False)False)False)maximizeflips which bound binds (toward = 1.0)Deliberately not done
Sweep.parameter_dual.to_pandas/to_dataarrayfor parameter duals.Departure from AGENTS.md
The
_readsskip survives its deletion and has no probe, because no value can tell it apart. Only the cost of a call can, and that was not measured.🤖 Generated with Claude Code
https://claude.ai/code/session_018B3nA1Q4XLWBq2jJFFLFWR