docs(piecewise): rewrite reference page + tutorials for correctness and pedagogy#677
Open
FBumann wants to merge 28 commits into
Open
docs(piecewise): rewrite reference page + tutorials for correctness and pedagogy#677FBumann wants to merge 28 commits into
FBumann wants to merge 28 commits into
Conversation
- "breakpoint" is now framed as one knot defined by the i-th entry of each tuple, so the N-tuple linking case isn't (x, y)-specific. - "pinned" is reframed as a sign role, with a brief breakdown of what it actually constrains: joint-on-curve only with 2+ pinned tuples; with one bounded + one pinned, the pinned axis collapses to a [x_min, x_max] domain box (LP enforces it directly, SOS2/incremental via the weight link). - Disjunctive auxiliary-variables cell corrected to "Continuous + binary + SOS2". - SOS2/disjunctive solver-requirement cells now mention the Big-M reformulation path and link to :ref:`sos-reformulation`. - LP domain bound math uses x_min/x_max (descending grids accepted). - Per-tuple-sign formulation math switched to a method-agnostic W_j(weights, B) so the section covers both SOS2 (lambda) and incremental (delta) accurately. - "Two factories" lead-in widened to three building blocks so Slopes isn't hidden in the code block. - Quick Start inequality bounds heat (a curtailable output) instead of fuel, matching the doc's own "choice of bounded tuple" guidance. - Tutorials: add output_flag=False to m.solve(...) calls so HiGHS's banner/progress doesn't clutter the nbsphinx-rendered output; linopy's INFO logs (including auto-dispatch resolution) are kept. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…r Constraints" Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Collaborator
Author
|
@FabianHofmann I would argue that we could try to remove the solver logs from the tutorials in the docs overall...? |
- Tut 1 intro now opens with a one-line description plus an 8-section roadmap, replacing the jargon-y "stacks one feature on top of a small shared dispatch pattern" line. - Tut 1 §3 explains the degenerate (0, 0) "off" segment instead of leaving it for the reader to puzzle out. - Tut 1 §4 inequality example now bounds `heat` (a curtailable output) on a concave curve, matching the rst doc's "choice of bounded tuple" guidance. Variables use intuitive `power_pts` / `heat_pts` names so the plot cell no longer needs a "swap to put power on the x-axis" comment. - Tut 1 ends with a "When to use what" table cross-linking to the rst reference page and to the inequality-bounds tutorial. - Tut 2 intro leads with the one-sided-bound motivation and the pure-LP pay-off before the API snippet, and the "Tuple roles" table matches the rst's precision: with one bounded + one equality tuple, the equality tuple's marginal feasible set is just its breakpoint domain (not "lies exactly on the curve"). - Tut 2's summary cross-links back to the linear-constraints tutorial. - Both notebooks: removed remaining "pinned" wording where it implied geometric on-curve placement instead of a sign role. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Other linopy tutorials (create-a-model.ipynb, manipulating-models.ipynb, etc.) explicitly pass solver_name='highs' to m.solve(...). The piecewise tutorials relied on the default-available-solver fallback, which made the HiGHS-specific output_flag kwarg look mysterious. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The previous framing — fuel <= f(power) with objective -fuel — is logically off: bounding fuel from above lets the solver choose any non-negative fuel, including zero (since fuel is consumption-side). The artificial maximise-fuel objective hid this but the underlying relation didn't make physical sense. Power ≤ f(fuel) is the classic production-function bound: f maps fuel input to the maximum power the unit can deliver, and the unit can always run below that (output is curtailable). Maximising power against the bound is now a sensible LP objective rather than a contrivance. - Reframed cells 0, 2, 3, 4, 5, 6, 7, 8, 10 around fuel_pts / power_pts; axis labels and the hypograph plot now read (fuel, power) with f(fuel) as the production curve. - Cell 5's objective is now `-power` (maximise power up to the curve bound), matching real LPs. - Test points and curvature × sign analysis unchanged — only the physical interpretation flipped. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Previous version bounded `heat` by a curve labelled "concave" — but the slopes (0.8, 1.0, 1.33) are increasing, so the curve is actually convex, and `heat <= f(power)` on a convex curve doesn't dispatch to LP. Switched to a convex heat-rate curve with `fuel >= f(power)`: over- fuelling is physically admissible (waste heat) but wasteful, so minimising fuel pulls the operating point onto the curve. This is the epigraph half of the LP-applicable region, complementing the inequality- bounds tutorial's hypograph example (concave + `<=`). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Matches the tutorial 1 §4 reframe: convex heat-rate curve with ``fuel >= f(power)``, where over-fuelling is admissible but wasteful so minimisation pulls the operating point onto the curve. Replaces the heat <= f(power) example (which used a curve that was actually convex, not concave, so it wouldn't have dispatched to LP). Generalised the "Choice of bounded tuple" guidance to cover both signs: - ``"<="`` for a controllable dissipation path (curtailment, post- treatment). - ``">="`` for an input whose over-supply is admissible but wasteful (fuel, raw materials). The wrong-direction warning now spells out both anti-patterns: ``"<="`` on fuel, ``">="`` on a non-curtailable output. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Reuses §1's exact breakpoints — only the sign on one tuple changes — so the reader sees "the same curve, now with an inequality" rather than a new curve. Reads as a production function: power output is bounded by what the fuel input can support, and the unit may run below the maximum (curtailable output). Same physical meaning as the previous fuel ≥ f(power) framing (the two forms describe the same feasible region under inversion), but more intuitive and continuous with the rest of the tutorial. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Inequality-bounds tutorial now uses the same curve as §1/§4 of the linear-constraints tutorial and the rst Quick Start: fuel_pts = [0, 36, 84, 170] power_pts = [0, 30, 60, 100] This concave production curve has f(60) = 45, so the verification cell checks power = 45 at fuel = 60. The hypograph-visualisation test points are scaled to the new axes: (60, 30) under, (60, 45) on, (60, 55) above, (180, 50) beyond domain. The non-convex fallback example also uses the [0, 170] fuel domain. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Collaborator
yes, totally |
The warning fires once per session on first use of add_piecewise_formulation / Slopes / tangent_lines. Useful in user code, but in a tutorial it's a distracting wall of yellow above the first solve. Silenced in the imports cell of each notebook via warnings.filterwarnings — the canonical recipe from the warning message itself. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Heat-rate framing — `fuel = f(power)` — is native to power-systems modelling, so the inequality variant reads naturally as `fuel ≥ f(power)`: the curve is the design minimum, over-fuelling is admissible but wasteful, and minimising fuel pulls the operating point onto the curve. Same feasible region as the previous `power ≤ f(fuel)` form under inversion; the LP path uses the convex + `>=` (epigraph) half instead of concave + `<=` (hypograph). - rst Quick Start: bound fuel ≥ f(power), same breakpoints as §1. - Tutorial 1 §4: same. - Tutorial 2: setup curve labelled "convex heat-rate curve f(power)"; solve checks fuel=84 at power=60; visualisation is now the epigraph with test points (60, 100), (60, 84), (60, 70), (120, 100); fallback cell exercises the dual mismatched-curvature cases (convex + `<=`). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
HiGHS doesn't natively support SOS constraints, so the sos2 and incremental cases of the three-methods comparison need reformulate_sos="auto" (matching tutorial 1's pattern). Without it, m.solve(solver_name="highs", ...) raises ValueError on the SOS2 path. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2 tasks
FBumann
added a commit
that referenced
this pull request
May 11, 2026
HiGHS prints a banner + progress lines to the Python REPL on every m.solve() call by default. In a tutorial that calls solve many times, this drowns the actual lesson in solver chatter. Pass output_flag=False (a HiGHS solver option forwarded via **solver_options) to suppress it. Touches the four notebooks where solver_name="highs" is the only solver invoked: - create-a-model.ipynb - create-a-model-with-coordinates.ipynb - manipulating-models.ipynb (9 solves) - transport-tutorial.ipynb Left alone: - infeasible-model.ipynb (uses Gurobi, kwarg is OutputFlag there; also showing solver feedback may be pedagogically relevant for infeasibility detection). - solve-on-remote.ipynb / solve-on-oetc.ipynb (remote handler manages its own logging). - piecewise-*.ipynb (already addressed in #677). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
e58dcd4 to
825af88
Compare
RST restructure
- Moved "Breakpoint Construction" ahead of "Per-tuple sign". Readers
looking up "how do I supply breakpoints" no longer have to scroll
through the inequality section first.
- Split "Per-tuple sign" — previously a chapter masquerading as a
section, covering six concepts under one heading — into five proper
subsections: Roles and restrictions, Geometry, Choice of bounded
tuple and sign, When is a one-sided bound wanted?, Formulation math.
- Folded the duplicate "Breakpoint inputs" overview (under API) into
the lead-in of "Breakpoint Construction"; the three building blocks
(breakpoints, segments, Slopes) are now introduced once.
- Promoted tangent_lines to its own "Chord expressions as a building
block" subsection under LP — previously a half-hidden paragraph.
- Moved the disjunctive caveat ("method returns 'sos2' but the table
treats it separately") into a `.. note::` directly under the
comparison table where the apparent contradiction is.
- Shortened the terminology block at the top to one paragraph (it
previously defined "segment" hundreds of lines before the disjunctive
section).
- Centralised N≥3 sign restriction — N-variable linking section now
cross-references Per-tuple sign instead of redefining it.
- Removed the duplicated active+sign warning under "Advanced Features"
— single source of truth now in "Per-tuple sign / Formulation math".
Tutorial flow
- Tut 1 §2: trimmed the upfront method-comparison table to a forward
pointer — the table previously claimed "lp requires sign != ==" and
"matching curvature" before §3 and §4 had tutorialised those terms.
- Tut 2: dropped the standalone setup plot that showed only the curve.
The hypograph/epigraph visualisation later in the notebook already
shows the curve, with the operating points overlaid — saves a
figure and tightens the narrative.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…o Active
The W_j(weights, B) abstraction it introduced was immediately re-
derived in each method section below (SOS2 with λ, incremental with
δ), so it added a layer for the reader to climb through without
delivering content the method sections didn't already. The
"*_link" / "*_output_link" naming detail contradicted our own guidance
elsewhere ("exact name suffixes are an implementation detail and may
evolve"). The "equality keeps equality, bounded flips sign" idea is
already conveyed by Roles & restrictions and Geometry above.
The one load-bearing nugget was the active=0 warning. Moved it into
the Active parameter (unit commitment) subsection under Advanced
Features, where readers setting `active=...` actually land — that's
where the gotcha needs to be visible.
Also dropped the now-stale cross-reference from the Incremental method
section.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
"Not supported with method='lp'" reads as a missing feature; one-line addition explains it's structural (gating needs a binary) and points to the auto-dispatch and to tangent_lines for manual gating. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
6d3b99f to
c2fe7f6
Compare
…example The previous example used `segments([(0, 0), (50, 80)])` — the (0,0) "off" segment was a hack to encode on/off behaviour, which is exactly what `active=...` (§5) is for. Using disjunctive for on/off duplicates §5 and teaches a bad pattern. New example shows what disjunctive is actually for: equipment without a continuous design space — gas turbines in three commercial classes (small / medium / large), each with its own non-overlapping power band and 2-piece heat-rate curve. The formulation picks the appropriate class per timestep based on demand. Added a forward pointer to §5 for the on/off case. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…izing Matches the tutorial §3 rewrite — uses three turbine classes with non-overlapping operating bands as the canonical disjunctive example, and adds a one-line steer away from the (0,0) anti-pattern toward `active=...` for on/off gating. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The turbine-classes framing was claiming "investment" but the formulation puts the binary on the time dimension by default — so the model was actually re-deciding the class each timestep, not making a one-time sizing call. Either we'd need a contrived constraint to make the binary time-invariant, or we'd be using an "investment" narrative on per-period selection. Pump VSD is honest per-period selection: a pump with three stepped speed settings, each covering a different flow band, switches speed between dispatch periods as a matter of normal operation. No constraints to bolt on, no narrative-vs-math gap. Also added "switchable combustion cycles" and "allowed bands around forbidden vibration zones" as alternative real use cases in the intro, so readers see disjunctive is for genuine multi-mode equipment. Updated both the rst Disjunctive-segments code block and the tutorial §3 (markdown intro, code, explanatory cell) to use the pump example. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two pumps in parallel, each with two operating bands (low 5–25 m³/h, high 40–100 m³/h) and a forbidden zone in between. Demand profile [30, 75, 150] makes every timestep single-pump-infeasible: - t=1, demand=30: lands in the single-pump gap (25, 40); both pumps run in low band, splitting the load. - t=2, demand=75: too much for low+low (max 50), too little for high+high (min 80); the low pump tops out at 25, the high pump covers the remaining 50. - t=3, demand=150: exceeds a single pump's maximum (100); both pumps run in high band. Each segment is now a single piece (2 breakpoints), so the example is clean and the load-split arithmetic is explicit. Rst snippet updated to match. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
power_p1 / power_p2 / flow (total) per timestep — the asymmetric split at t=2 is visible directly in the powers (one pump at 7 kW, the other at ~21 kW) instead of buried in a multi-index DataFrame. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
flat 4-column DataFrame (flow_p1, flow_p2, power_p1, power_p2) per timestep, via unstack on the pump dimension. The asymmetric splits are visible directly: at t=2 one pump runs at flow=25 (low band), the other at flow=50 (high band). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
§3 previously rebound the Python `demand` variable to its own pump-tutorial values [30, 75, 150], which then broke §4 and §8 downstream: both rely on §1's `demand = [50, 80, 30]`, and §4 with demand=150 is infeasible (power.upper=100). Inlined §3's demand into the constraint so the global namespace is untouched. Verified §1 → §3 → §4 sequence end-to-end: §4 now solves to optimal with fuel = [68, 127, 36]. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…he code block A five-line "# ..." comment inside the snippet was carrying the formulation explanation — that belongs as rst prose around the code block, not stuffed into the Python. Code block now has a single one-line orienting comment; the rationale moves to a paragraph below. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The two-line "stepped pump ... forbidden zone" comment inside the
code block duplicated the prose immediately above ("stepped pump
speeds ... forbidden vibration zones"). Single-line orienting
comment is enough.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Bare-slug markdown links (`[text](piecewise-linear-constraints)`) produced sphinx "File not found" warnings AND rendered with href attributes missing the `.html` extension, requiring browser-side resolution. Switching to `.html` form generates correct `href`s in the rendered HTML directly — verified against the built doc: <a class="reference external" href="piecewise-linear-constraints.html"> <a class="reference external" href="piecewise-inequality-bounds-tutorial.html"> Sphinx still warns at build time (its link checker doesn't find a source file with `.html` extension), but the warnings are cosmetic and the rendered docs work correctly. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Previous .html form rendered correct browser hrefs but produced sphinx "File not found" warnings AND rendered as external links. Switching to source-extension form: - [text](page.rst) for rst pages - [text](tutorial.nblink) for sibling notebook tutorials Sphinx resolves both to internal cross-references at build time — HTML output shows `class="reference internal"` with the doc anchor, no build warnings. Verified locally: piecewise cross-link warnings went from 6 to 0 (total build warnings 20 → 10, the remainder are pre-existing unrelated issues). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What was wrong
The piecewise documentation had grown organically and accumulated several precision, structural, and pedagogical problems.
Correctness bugs
_add_disjunctiveactually creates continuouslambda_var, binary segment selectors, and an SOS2 constraint — three kinds, not two.x_0 ≤ x ≤ x_n, implicitly assuming ascending breakpoints._lp_eligibilityaccepts strict monotonicity in either direction; the math should bex_min ≤ x ≤ x_max.(fuel, [0, 20, 30, 35], "<=")with the misleading comment "pinned to the curve" on the other tuple. Bounding fuel (a consumption-side variable) with<=was the anti-pattern the doc's own "Choice of bounded tuple" section warned against.active=...(Advanced Features) is for. The example duplicated a pattern that has a proper API and taught a bad habit..method == "sos2"but a separate column) was confusing.*_link,*_output_link) while a different part of the page said names "are an implementation detail and may evolve".fuel ≤ f(power)with amaximise -fuelobjective. The artificial objective hid the fact that bounding fuel from above admitsfuel = 0, which makes no physical sense.Structural problems
tangent_lineswas a useful standalone helper buried in a paragraph inside the LP method section.sign, curvature requirements).Tutorial-execution noise
m.solve()printed the HiGHS banner and the once-per-sessionEvolvingAPIWarning, drowning the actual lesson.What changed
doc/piecewise-linear-constraints.rstBreakpoint Constructionahead ofPer-tuple sign, split the latter into four focused subsections, folded the duplicate "Breakpoint inputs" overview into it, promotedtangent_linesto its own subsection under LP, and shortened the up-front terminology block.active=0warning moved intoActive parameter.x_min ≤ x ≤ x_max, referenced:ref:sos-reformulation`` for the Big-M fallback, generalised "Choice of bounded tuple" to both signs, and explained whyactiveisn't supported with `method="lp"`.fuel ≥ f(power)on the same curve as the equality example; disjunctive snippet replaces the(0, 0)on/off hack with a stepped pump and points toactive=...for on/off.examples/piecewise-linear-constraints.ipynb[30, 75, 150]makes every timestep single-pump-infeasible. Output is a flat 4-column DataFrame.fuel ≥ f(power)on §1's curve.examples/piecewise-inequality-bounds.ipynbfuel ≥ f(power)with breakpoints aligned to the rest of the doc; hypograph visualisation replaced with the epigraph variant; fallback cell exercises the dual mismatched-curvature cases.Both tutorials
m.solve(...)calls now passsolver_name="highs",reformulate_sos="auto", andoutput_flag=False.EvolvingAPIWarning.Pedagogical additions (beyond the stated bug list)
A few additions go beyond fixing the documented problems and into making the docs a better read. Calling them out explicitly so they're easy to review or revert:
tangent_linespromoted to its own subsection under LP (discoverability — the function existed before but was buried).Test plan
sphinx-buildofdoc/renders without warnings.:ref:sos-reformulation`` link resolves correctly.🤖 Generated with Claude Code