Skip to content

Commit 6f54d78

Browse files
Merge branch 'master' into pre-commit-ci-update-config
2 parents 18fcb15 + 474f79b commit 6f54d78

11 files changed

Lines changed: 639 additions & 66 deletions

File tree

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
name: Test Notebooks
2+
3+
on:
4+
push:
5+
branches: [ master ]
6+
pull_request:
7+
branches: [ '*' ]
8+
schedule:
9+
- cron: "0 5 * * TUE"
10+
11+
concurrency:
12+
group: ${{ github.workflow }}-${{ github.ref }}
13+
cancel-in-progress: true
14+
15+
jobs:
16+
notebooks:
17+
name: Test documentation notebooks
18+
runs-on: ubuntu-latest
19+
20+
steps:
21+
- uses: actions/checkout@v6
22+
with:
23+
fetch-depth: 0
24+
25+
- name: Set up Python 3.12
26+
uses: actions/setup-python@v6
27+
with:
28+
python-version: "3.12"
29+
30+
- name: Install package and dependencies
31+
run: |
32+
python -m pip install uv
33+
uv pip install --system -e ".[docs]"
34+
35+
- name: Execute notebooks
36+
run: |
37+
EXIT_CODE=0
38+
for notebook in examples/*.ipynb; do
39+
name=$(basename "$notebook")
40+
41+
# Skip notebooks that require credentials or special setup
42+
case "$name" in
43+
solve-on-oetc.ipynb|solve-on-remote.ipynb)
44+
echo "Skipping $name (requires credentials or special setup)"
45+
continue
46+
;;
47+
esac
48+
49+
echo "::group::Running $name"
50+
if jupyter nbconvert --to notebook --execute --ExecutePreprocessor.timeout=600 "$notebook"; then
51+
echo "✓ $name passed"
52+
else
53+
echo "::error::✗ $name failed"
54+
EXIT_CODE=1
55+
fi
56+
echo "::endgroup::"
57+
done
58+
exit $EXIT_CODE

doc/conf.py

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -91,16 +91,13 @@
9191
9292
"""
9393

94-
nbsphinx_allow_errors = True
94+
nbsphinx_allow_errors = False
9595
nbsphinx_execute = "auto"
9696
nbsphinx_execute_arguments = [
9797
"--InlineBackend.figure_formats={'svg', 'pdf'}",
9898
"--InlineBackend.rc={'figure.dpi': 96}",
9999
]
100100

101-
# Exclude notebooks that require credentials or special setup
102-
nbsphinx_execute_never = ["**/solve-on-oetc*"]
103-
104101
# -- Options for HTML output -------------------------------------------------
105102

106103
# The theme to use for HTML and HTML Help pages. See the documentation for

doc/release_notes.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ Upcoming Version
1818
* Add the `sphinx-copybutton` to the documentation
1919
* Add SOS1 and SOS2 reformulations for solvers not supporting them.
2020
* Add semi-continous variables for solvers that support them
21+
* Add ``OetcSettings.from_env()`` classmethod to create OETC settings from environment variables (``OETC_EMAIL``, ``OETC_PASSWORD``, ``OETC_NAME``, ``OETC_AUTH_URL``, ``OETC_ORCHESTRATOR_URL``, ``OETC_CPU_CORES``, ``OETC_DISK_SPACE_GB``, ``OETC_DELETE_WORKER_ON_ERROR``).
22+
* Forward ``solver_name`` and ``**solver_options`` from ``Model.solve()`` to OETC handler. Call-level options override settings-level defaults.
2123
* Improve handling of CPLEX solver quality attributes to ensure metrics such are extracted correctly when available.
2224
* Fix Xpress IIS label mapping for masked constraints and add a regression test for matching infeasible coordinates.
2325
* Enable quadratic problems with SCIP on windows.

examples/piecewise-linear-constraints.ipynb

Lines changed: 17 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
{
44
"cell_type": "markdown",
55
"metadata": {},
6-
"source": "# Piecewise Linear Constraints Tutorial\n\nThis notebook demonstrates linopy's piecewise linear (PWL) constraint formulations.\nEach example builds a separate dispatch model where a single power plant must meet\na time-varying demand.\n\n| Example | Plant | Limitation | Formulation |\n|---------|-------|------------|-------------|\n| 1 | Gas turbine (0–100 MW) | Convex heat rate | SOS2 |\n| 2 | Coal plant (0–150 MW) | Monotonic heat rate | Incremental |\n| 3 | Diesel generator (off or 50–80 MW) | Forbidden zone | Disjunctive |\n| 4 | Concave efficiency curve | Inequality bound | LP |\n| 5 | Gas unit with commitment | On/off + min load | Incremental + `active` |\n\n**Note:** The `piecewise(...)` expression can appear on either side of\nthe comparison operator (`==`, `<=`, `>=`). For example, both\n`linopy.piecewise(x, x_pts, y_pts) == y` and `y == linopy.piecewise(...)` work."
6+
"source": "# Piecewise Linear Constraints Tutorial\n\nThis notebook demonstrates linopy's piecewise linear (PWL) constraint formulations.\nEach example builds a separate dispatch model where a single power plant must meet\na time-varying demand.\n\n| Example | Plant | Limitation | Formulation |\n|---------|-------|------------|-------------|\n| 1 | Gas turbine (0\u2013100 MW) | Convex heat rate | SOS2 |\n| 2 | Coal plant (0\u2013150 MW) | Monotonic heat rate | Incremental |\n| 3 | Diesel generator (off or 50\u201380 MW) | Forbidden zone | Disjunctive |\n| 4 | Concave efficiency curve | Inequality bound | LP |\n| 5 | Gas unit with commitment | On/off + min load | Incremental + `active` |\n\n**Note:** The `piecewise(...)` expression can appear on either side of\nthe comparison operator (`==`, `<=`, `>=`). For example, both\n`linopy.piecewise(x, x_pts, y_pts) == y` and `y == linopy.piecewise(...)` work."
77
},
88
{
99
"cell_type": "code",
@@ -90,7 +90,7 @@
9090
"cell_type": "markdown",
9191
"metadata": {},
9292
"source": [
93-
"## 1. SOS2 formulation Gas turbine\n",
93+
"## 1. SOS2 formulation \u2014 Gas turbine\n",
9494
"\n",
9595
"The gas turbine has a **convex** heat rate: efficient at moderate load,\n",
9696
"increasingly fuel-hungry at high output. We use the **SOS2** formulation\n",
@@ -173,7 +173,7 @@
173173
}
174174
},
175175
"source": [
176-
"m1.solve()"
176+
"m1.solve(reformulate_sos=\"auto\")"
177177
],
178178
"outputs": [],
179179
"execution_count": null
@@ -224,11 +224,11 @@
224224
"cell_type": "markdown",
225225
"metadata": {},
226226
"source": [
227-
"## 2. Incremental formulation Coal plant\n",
227+
"## 2. Incremental formulation \u2014 Coal plant\n",
228228
"\n",
229229
"The coal plant has a **monotonically increasing** heat rate. Since all\n",
230230
"breakpoints are strictly monotonic, we can use the **incremental**\n",
231-
"formulation which uses fill-fraction variables with binary indicators."
231+
"formulation \u2014 which uses fill-fraction variables with binary indicators."
232232
]
233233
},
234234
{
@@ -306,7 +306,7 @@
306306
}
307307
},
308308
"source": [
309-
"m2.solve();"
309+
"m2.solve(reformulate_sos=\"auto\");"
310310
],
311311
"outputs": [],
312312
"execution_count": null
@@ -357,10 +357,10 @@
357357
"cell_type": "markdown",
358358
"metadata": {},
359359
"source": [
360-
"## 3. Disjunctive formulation Diesel generator\n",
360+
"## 3. Disjunctive formulation \u2014 Diesel generator\n",
361361
"\n",
362362
"The diesel generator has a **forbidden operating zone**: it must either\n",
363-
"be off (0 MW) or run between 50–80 MW. Because of this gap, we use\n",
363+
"be off (0 MW) or run between 50\u201380 MW. Because of this gap, we use\n",
364364
"**disjunctive** piecewise constraints via `linopy.segments()` and add a\n",
365365
"high-cost **backup** source to cover demand when the diesel is off or\n",
366366
"at its maximum.\n",
@@ -446,7 +446,7 @@
446446
}
447447
},
448448
"source": [
449-
"m3.solve()"
449+
"m3.solve(reformulate_sos=\"auto\")"
450450
],
451451
"outputs": [],
452452
"execution_count": null
@@ -476,11 +476,11 @@
476476
"cell_type": "markdown",
477477
"metadata": {},
478478
"source": [
479-
"## 4. LP formulation Concave efficiency bound\n",
479+
"## 4. LP formulation \u2014 Concave efficiency bound\n",
480480
"\n",
481481
"When the piecewise function is **concave** and we use a `>=` constraint\n",
482482
"(i.e. `pw >= y`, meaning y is bounded above by pw), linopy can use a\n",
483-
"pure **LP** formulation with tangent-line constraints no SOS2 or\n",
483+
"pure **LP** formulation with tangent-line constraints \u2014 no SOS2 or\n",
484484
"binary variables needed. This is the fastest to solve.\n",
485485
"\n",
486486
"For this formulation, the x-breakpoints must be in **strictly increasing**\n",
@@ -514,7 +514,7 @@
514514
"power = m4.add_variables(name=\"power\", lower=0, upper=120, coords=[time])\n",
515515
"fuel = m4.add_variables(name=\"fuel\", lower=0, coords=[time])\n",
516516
"\n",
517-
"# pw >= fuel means fuel <= concave_function(power) auto-selects LP method\n",
517+
"# pw >= fuel means fuel <= concave_function(power) \u2192 auto-selects LP method\n",
518518
"m4.add_piecewise_constraints(\n",
519519
" linopy.piecewise(power, x_pts4, y_pts4) >= fuel,\n",
520520
" name=\"pwl\",\n",
@@ -544,7 +544,7 @@
544544
}
545545
},
546546
"source": [
547-
"m4.solve()"
547+
"m4.solve(reformulate_sos=\"auto\")"
548548
],
549549
"outputs": [],
550550
"execution_count": null
@@ -595,7 +595,7 @@
595595
"cell_type": "markdown",
596596
"metadata": {},
597597
"source": [
598-
"## 5. Slopes mode Building breakpoints from slopes\n",
598+
"## 5. Slopes mode \u2014 Building breakpoints from slopes\n",
599599
"\n",
600600
"Sometimes you know the **slope** of each segment rather than the y-values\n",
601601
"at each breakpoint. The `breakpoints()` factory can compute y-values from\n",
@@ -628,7 +628,7 @@
628628
},
629629
{
630630
"cell_type": "markdown",
631-
"source": "## 6. Active parameter Unit commitment with piecewise efficiency\n\nIn unit commitment problems, a binary variable $u_t$ controls whether a\nunit is **on** or **off**. When off, both power output and fuel consumption\nmust be zero. When on, the unit operates within its piecewise-linear\nefficiency curve between $P_{min}$ and $P_{max}$.\n\nThe `active` parameter on `piecewise()` handles this by gating the\ninternal PWL formulation with the commitment binary:\n\n- **Incremental:** delta bounds tighten from $\\delta_i \\leq 1$ to\n $\\delta_i \\leq u$, and base terms are multiplied by $u$\n- **SOS2:** convexity constraint becomes $\\sum \\lambda_i = u$\n- **Disjunctive:** segment selection becomes $\\sum z_k = u$\n\nThis is the only gating behavior expressible with pure linear constraints.\nSelectively *relaxing* the PWL (letting x, y float freely when off) would\nrequire big-M or indicator constraints.",
631+
"source": "## 6. Active parameter \u2014 Unit commitment with piecewise efficiency\n\nIn unit commitment problems, a binary variable $u_t$ controls whether a\nunit is **on** or **off**. When off, both power output and fuel consumption\nmust be zero. When on, the unit operates within its piecewise-linear\nefficiency curve between $P_{min}$ and $P_{max}$.\n\nThe `active` parameter on `piecewise()` handles this by gating the\ninternal PWL formulation with the commitment binary:\n\n- **Incremental:** delta bounds tighten from $\\delta_i \\leq 1$ to\n $\\delta_i \\leq u$, and base terms are multiplied by $u$\n- **SOS2:** convexity constraint becomes $\\sum \\lambda_i = u$\n- **Disjunctive:** segment selection becomes $\\sum z_k = u$\n\nThis is the only gating behavior expressible with pure linear constraints.\nSelectively *relaxing* the PWL (letting x, y float freely when off) would\nrequire big-M or indicator constraints.",
632632
"metadata": {}
633633
},
634634
{
@@ -666,7 +666,7 @@
666666
},
667667
{
668668
"cell_type": "code",
669-
"source": "m6.solve()",
669+
"source": "m6.solve(reformulate_sos=\"auto\")",
670670
"metadata": {
671671
"ExecuteTime": {
672672
"end_time": "2026-03-09T10:17:28.878112Z",
@@ -855,7 +855,7 @@
855855
},
856856
{
857857
"cell_type": "markdown",
858-
"source": "At **t=1**, demand (15 MW) is below the minimum load (30 MW). The solver\nkeeps the unit off (`commit=0`), so `power=0` and `fuel=0` the `active`\nparameter enforces this. Demand is met by the backup source.\n\nAt **t=2** and **t=3**, the unit commits and operates on the PWL curve.",
858+
"source": "At **t=1**, demand (15 MW) is below the minimum load (30 MW). The solver\nkeeps the unit off (`commit=0`), so `power=0` and `fuel=0` \u2014 the `active`\nparameter enforces this. Demand is met by the backup source.\n\nAt **t=2** and **t=3**, the unit commits and operates on the PWL curve.",
859859
"metadata": {}
860860
}
861861
],

examples/solve-on-oetc.ipynb

Lines changed: 85 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,13 @@
3030
"All of these steps are handled automatically by linopy's `OetcHandler`."
3131
]
3232
},
33+
{
34+
"cell_type": "markdown",
35+
"metadata": {},
36+
"source": [
37+
"> **Note:** This notebook requires Google Cloud credentials and access to the OETC platform. It is not executed during the documentation build, so no cell outputs are shown. To run it yourself, install the `linopy[oetc]` extra and configure your credentials."
38+
]
39+
},
3340
{
3441
"cell_type": "markdown",
3542
"metadata": {},
@@ -79,7 +86,12 @@
7986
"source": [
8087
"## Configure OETC Settings\n",
8188
"\n",
82-
"Next, we need to configure the OETC settings including credentials and compute requirements:"
89+
"There are two ways to configure OETC settings:\n",
90+
"\n",
91+
"1. **Manual construction** \u2014 build `OetcCredentials` and `OetcSettings` explicitly\n",
92+
"2. **`OetcSettings.from_env()`** \u2014 resolve credentials and options from environment variables\n",
93+
"\n",
94+
"### Option 1: Manual Construction"
8395
]
8496
},
8597
{
@@ -123,6 +135,48 @@
123135
"print(f\"Disk space: {settings.disk_space_gb} GB\")"
124136
]
125137
},
138+
{
139+
"cell_type": "markdown",
140+
"metadata": {},
141+
"source": [
142+
"### Option 2: Create Settings from Environment Variables\n",
143+
"\n",
144+
"`OetcSettings.from_env()` reads configuration from environment variables,\n",
145+
"with optional keyword overrides. This is the recommended approach for\n",
146+
"CI/CD pipelines and production deployments.\n",
147+
"\n",
148+
"| Environment Variable | Required | Description |\n",
149+
"|---|---|---|\n",
150+
"| `OETC_EMAIL` | Yes | Account email |\n",
151+
"| `OETC_PASSWORD` | Yes | Account password |\n",
152+
"| `OETC_NAME` | Yes | Job name |\n",
153+
"| `OETC_AUTH_URL` | Yes | Authentication server URL |\n",
154+
"| `OETC_ORCHESTRATOR_URL` | Yes | Orchestrator server URL |\n",
155+
"| `OETC_CPU_CORES` | No | CPU cores (default: 2) |\n",
156+
"| `OETC_DISK_SPACE_GB` | No | Disk space in GB (default: 10) |\n",
157+
"| `OETC_DELETE_WORKER_ON_ERROR` | No | Delete worker on error (default: false) |\n",
158+
"\n",
159+
"Keyword arguments take precedence over environment variables."
160+
]
161+
},
162+
{
163+
"cell_type": "code",
164+
"metadata": {},
165+
"outputs": [],
166+
"source": [
167+
"# Create settings from environment variables\n",
168+
"# All required env vars must be set: OETC_EMAIL, OETC_PASSWORD,\n",
169+
"# OETC_NAME, OETC_AUTH_URL, OETC_ORCHESTRATOR_URL\n",
170+
"settings = OetcSettings.from_env()\n",
171+
"\n",
172+
"# Or override specific values via keyword arguments\n",
173+
"settings = OetcSettings.from_env(\n",
174+
" cpu_cores=8,\n",
175+
" disk_space_gb=50,\n",
176+
")"
177+
],
178+
"execution_count": null
179+
},
126180
{
127181
"cell_type": "markdown",
128182
"metadata": {},
@@ -221,38 +275,49 @@
221275
"\n",
222276
"### Solver Options\n",
223277
"\n",
224-
"You can pass solver-specific options through the `solver_options` parameter:"
278+
"Solver name and options can be configured at two levels:\n",
279+
"\n",
280+
"1. **Settings level** \u2014 defaults stored in `OetcSettings.solver` and `OetcSettings.solver_options`\n",
281+
"2. **Call level** \u2014 passed via `m.solve(solver_name=..., **solver_options)`\n",
282+
"\n",
283+
"Call-level options **override** settings-level options. The two dicts are\n",
284+
"merged (call-time takes precedence), and the original settings are never\n",
285+
"mutated."
225286
]
226287
},
227288
{
228289
"cell_type": "code",
229-
"execution_count": null,
230290
"metadata": {},
231291
"outputs": [],
232292
"source": [
233-
"# Example with advanced solver options\n",
293+
"# Settings-level defaults\n",
234294
"advanced_settings = OetcSettings(\n",
235295
" credentials=credentials,\n",
236296
" name=\"advanced-linopy-job\",\n",
237297
" authentication_server_url=\"https://auth.oetcloud.com\",\n",
238298
" orchestrator_server_url=\"https://orchestrator.oetcloud.com\",\n",
239-
" solver=\"gurobi\", # Using Gurobi solver\n",
299+
" solver=\"gurobi\",\n",
240300
" solver_options={\n",
241-
" \"TimeLimit\": 600, # 10 minutes\n",
242-
" \"MIPGap\": 0.01, # 1% optimality gap\n",
243-
" \"Threads\": 4, # Use 4 threads\n",
244-
" \"OutputFlag\": 1, # Enable solver output\n",
301+
" \"TimeLimit\": 600,\n",
302+
" \"MIPGap\": 0.01,\n",
245303
" },\n",
246-
" cpu_cores=8, # More CPU cores for larger problems\n",
247-
" disk_space_gb=50, # More disk space\n",
304+
" cpu_cores=8,\n",
305+
" disk_space_gb=50,\n",
248306
")\n",
249307
"\n",
250-
"print(\"Advanced OETC settings:\")\n",
251-
"print(f\"Solver: {advanced_settings.solver}\")\n",
252-
"print(f\"Solver options: {advanced_settings.solver_options}\")\n",
253-
"print(f\"CPU cores: {advanced_settings.cpu_cores}\")\n",
254-
"print(f\"Disk space: {advanced_settings.disk_space_gb} GB\")"
255-
]
308+
"advanced_handler = OetcHandler(advanced_settings)\n",
309+
"\n",
310+
"# Call-level overrides: solver_name and solver_options are forwarded\n",
311+
"# to OETC and merged with the settings defaults.\n",
312+
"# Here MIPGap from settings (0.01) is kept, TimeLimit is overridden to 300.\n",
313+
"status, condition = m.solve(\n",
314+
" remote=advanced_handler,\n",
315+
" solver_name=\"gurobi\",\n",
316+
" TimeLimit=300,\n",
317+
" Threads=4,\n",
318+
")"
319+
],
320+
"execution_count": null
256321
},
257322
{
258323
"cell_type": "markdown",
@@ -356,6 +421,9 @@
356421
"nbconvert_exporter": "python",
357422
"pygments_lexer": "ipython3",
358423
"version": "3.12.3"
424+
},
425+
"nbsphinx": {
426+
"execute": "never"
359427
}
360428
},
361429
"nbformat": 4,

0 commit comments

Comments
 (0)