Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 58 additions & 0 deletions .github/workflows/test-notebooks.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
name: Test Notebooks

on:
push:
branches: [ master ]
pull_request:
branches: [ '*' ]
schedule:
- cron: "0 5 * * TUE"

concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

jobs:
notebooks:
name: Test documentation notebooks
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v6
with:
fetch-depth: 0

- name: Set up Python 3.12
uses: actions/setup-python@v6
with:
python-version: "3.12"

- name: Install package and dependencies
run: |
python -m pip install uv
uv pip install --system -e ".[docs]"

- name: Execute notebooks
run: |
EXIT_CODE=0
for notebook in examples/*.ipynb; do
name=$(basename "$notebook")

# Skip notebooks that require credentials or special setup
case "$name" in
solve-on-oetc.ipynb|solve-on-remote.ipynb)
echo "Skipping $name (requires credentials or special setup)"
continue
;;
esac

echo "::group::Running $name"
if jupyter nbconvert --to notebook --execute --ExecutePreprocessor.timeout=600 "$notebook"; then
echo "✓ $name passed"
else
echo "::error::✗ $name failed"
EXIT_CODE=1
fi
echo "::endgroup::"
done
exit $EXIT_CODE
5 changes: 1 addition & 4 deletions doc/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -91,16 +91,13 @@

"""

nbsphinx_allow_errors = True
nbsphinx_allow_errors = False
nbsphinx_execute = "auto"
nbsphinx_execute_arguments = [
"--InlineBackend.figure_formats={'svg', 'pdf'}",
"--InlineBackend.rc={'figure.dpi': 96}",
]

# Exclude notebooks that require credentials or special setup
nbsphinx_execute_never = ["**/solve-on-oetc*"]

# -- Options for HTML output -------------------------------------------------

# The theme to use for HTML and HTML Help pages. See the documentation for
Expand Down
34 changes: 17 additions & 17 deletions examples/piecewise-linear-constraints.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
{
"cell_type": "markdown",
"metadata": {},
"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."
"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."
},
{
"cell_type": "code",
Expand Down Expand Up @@ -90,7 +90,7 @@
"cell_type": "markdown",
"metadata": {},
"source": [
"## 1. SOS2 formulation Gas turbine\n",
"## 1. SOS2 formulation \u2014 Gas turbine\n",
"\n",
"The gas turbine has a **convex** heat rate: efficient at moderate load,\n",
"increasingly fuel-hungry at high output. We use the **SOS2** formulation\n",
Expand Down Expand Up @@ -173,7 +173,7 @@
}
},
"source": [
"m1.solve()"
"m1.solve(reformulate_sos=\"auto\")"
],
"outputs": [],
"execution_count": null
Expand Down Expand Up @@ -224,11 +224,11 @@
"cell_type": "markdown",
"metadata": {},
"source": [
"## 2. Incremental formulation Coal plant\n",
"## 2. Incremental formulation \u2014 Coal plant\n",
"\n",
"The coal plant has a **monotonically increasing** heat rate. Since all\n",
"breakpoints are strictly monotonic, we can use the **incremental**\n",
"formulation which uses fill-fraction variables with binary indicators."
"formulation \u2014 which uses fill-fraction variables with binary indicators."
]
},
{
Expand Down Expand Up @@ -306,7 +306,7 @@
}
},
"source": [
"m2.solve();"
"m2.solve(reformulate_sos=\"auto\");"
],
"outputs": [],
"execution_count": null
Expand Down Expand Up @@ -357,10 +357,10 @@
"cell_type": "markdown",
"metadata": {},
"source": [
"## 3. Disjunctive formulation Diesel generator\n",
"## 3. Disjunctive formulation \u2014 Diesel generator\n",
"\n",
"The diesel generator has a **forbidden operating zone**: it must either\n",
"be off (0 MW) or run between 50–80 MW. Because of this gap, we use\n",
"be off (0 MW) or run between 50\u201380 MW. Because of this gap, we use\n",
"**disjunctive** piecewise constraints via `linopy.segments()` and add a\n",
"high-cost **backup** source to cover demand when the diesel is off or\n",
"at its maximum.\n",
Expand Down Expand Up @@ -446,7 +446,7 @@
}
},
"source": [
"m3.solve()"
"m3.solve(reformulate_sos=\"auto\")"
],
"outputs": [],
"execution_count": null
Expand Down Expand Up @@ -476,11 +476,11 @@
"cell_type": "markdown",
"metadata": {},
"source": [
"## 4. LP formulation Concave efficiency bound\n",
"## 4. LP formulation \u2014 Concave efficiency bound\n",
"\n",
"When the piecewise function is **concave** and we use a `>=` constraint\n",
"(i.e. `pw >= y`, meaning y is bounded above by pw), linopy can use a\n",
"pure **LP** formulation with tangent-line constraints no SOS2 or\n",
"pure **LP** formulation with tangent-line constraints \u2014 no SOS2 or\n",
"binary variables needed. This is the fastest to solve.\n",
"\n",
"For this formulation, the x-breakpoints must be in **strictly increasing**\n",
Expand Down Expand Up @@ -514,7 +514,7 @@
"power = m4.add_variables(name=\"power\", lower=0, upper=120, coords=[time])\n",
"fuel = m4.add_variables(name=\"fuel\", lower=0, coords=[time])\n",
"\n",
"# pw >= fuel means fuel <= concave_function(power) auto-selects LP method\n",
"# pw >= fuel means fuel <= concave_function(power) \u2192 auto-selects LP method\n",
"m4.add_piecewise_constraints(\n",
" linopy.piecewise(power, x_pts4, y_pts4) >= fuel,\n",
" name=\"pwl\",\n",
Expand Down Expand Up @@ -544,7 +544,7 @@
}
},
"source": [
"m4.solve()"
"m4.solve(reformulate_sos=\"auto\")"
],
"outputs": [],
"execution_count": null
Expand Down Expand Up @@ -595,7 +595,7 @@
"cell_type": "markdown",
"metadata": {},
"source": [
"## 5. Slopes mode Building breakpoints from slopes\n",
"## 5. Slopes mode \u2014 Building breakpoints from slopes\n",
"\n",
"Sometimes you know the **slope** of each segment rather than the y-values\n",
"at each breakpoint. The `breakpoints()` factory can compute y-values from\n",
Expand Down Expand Up @@ -628,7 +628,7 @@
},
{
"cell_type": "markdown",
"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.",
"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.",
"metadata": {}
},
{
Expand Down Expand Up @@ -666,7 +666,7 @@
},
{
"cell_type": "code",
"source": "m6.solve()",
"source": "m6.solve(reformulate_sos=\"auto\")",
"metadata": {
"ExecuteTime": {
"end_time": "2026-03-09T10:17:28.878112Z",
Expand Down Expand Up @@ -855,7 +855,7 @@
},
{
"cell_type": "markdown",
"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.",
"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.",
"metadata": {}
}
],
Expand Down
10 changes: 10 additions & 0 deletions examples/solve-on-oetc.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,13 @@
"All of these steps are handled automatically by linopy's `OetcHandler`."
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"> **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."
]
},
{
"cell_type": "markdown",
"metadata": {},
Expand Down Expand Up @@ -356,6 +363,9 @@
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.12.3"
},
"nbsphinx": {
"execute": "never"
}
},
"nbformat": 4,
Expand Down
21 changes: 17 additions & 4 deletions examples/solve-on-remote.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,22 @@
"cell_type": "markdown",
"id": "4db583af",
"metadata": {},
"source": ["# Remote Solving with SSH", "\n",
"source": [
"# Remote Solving with SSH",
"\n",
"This example demonstrates how linopy can solve optimization models on remote machines using SSH connections. This is one of two remote solving options available in linopy",
"\n",
"1. **SSH Remote Solving** (this example) - Connect to your own servers via SSH\\n2. **OETC Cloud Solving** - Use cloud-based optimization services (see `solve-on-oetc.ipynb`)",
"\n\n",
"## SSH Remote Solving\\n\\nSSH remote solving is ideal when you have:\\n* Access to dedicated servers with optimization solvers installed\\n* Full control over the computing environment\\n* Existing infrastructure for optimization workloads\\n\\n## What you need for SSH remote solving:\\n* A running installation of paramiko on your local machine (`pip install paramiko`)\\n* A remote server with a working installation of linopy (e.g., in a conda environment)\\n* SSH access to that machine\\n\\n## How SSH Remote Solving Works\\n\\nThe workflow consists of the following steps, most of which linopy handles automatically:\\n\\n1. Define a model on the local machine\\n2. Save the model on the remote machine via SSH\\n3. Load, solve and write out the model on the remote machine\\n4. Copy the solved model back to the local machine\\n5. Load the solved model on the local machine\\n\\nThe model initialization happens locally, while the actual solving happens remotely.\""]
"## SSH Remote Solving\\n\\nSSH remote solving is ideal when you have:\\n* Access to dedicated servers with optimization solvers installed\\n* Full control over the computing environment\\n* Existing infrastructure for optimization workloads\\n\\n## What you need for SSH remote solving:\\n* A running installation of paramiko on your local machine (`pip install paramiko`)\\n* A remote server with a working installation of linopy (e.g., in a conda environment)\\n* SSH access to that machine\\n\\n## How SSH Remote Solving Works\\n\\nThe workflow consists of the following steps, most of which linopy handles automatically:\\n\\n1. Define a model on the local machine\\n2. Save the model on the remote machine via SSH\\n3. Load, solve and write out the model on the remote machine\\n4. Copy the solved model back to the local machine\\n5. Load the solved model on the local machine\\n\\nThe model initialization happens locally, while the actual solving happens remotely.\""
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"> **Note:** This notebook requires SSH access to a remote server with a solver installed. It is not executed during the documentation build, so no cell outputs are shown. To run it yourself, configure SSH access and install a solver on the remote machine."
]
},
{
"cell_type": "markdown",
Expand Down Expand Up @@ -311,7 +321,7 @@
"\n",
".xr-section-summary-in + label:before {\n",
" display: inline-block;\n",
" content: '';\n",
" content: '\u25ba';\n",
" font-size: 11px;\n",
" width: 15px;\n",
" text-align: center;\n",
Expand All @@ -322,7 +332,7 @@
"}\n",
"\n",
".xr-section-summary-in:checked + label:before {\n",
" content: '';\n",
" content: '\u25bc';\n",
"}\n",
"\n",
".xr-section-summary-in:checked + label > span {\n",
Expand Down Expand Up @@ -610,6 +620,9 @@
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.11.3"
},
"nbsphinx": {
"execute": "never"
}
},
"nbformat": 4,
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ docs = [
"nbsphinx-link==1.3.0",
"docutils<0.21",
"numpy<2",
"gurobipy==11.0.2",
"gurobipy>=13.0.0",
"ipykernel==6.29.5",
"matplotlib==3.9.1",
"highspy>=1.7.1",
Expand Down
Loading