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
1 change: 1 addition & 0 deletions doc/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,7 @@ This package is published under MIT license.
piecewise-linear-constraints
piecewise-linear-constraints-tutorial
manipulating-models
iterative-resolving
testing-framework
transport-tutorial
infeasible-model
Expand Down
3 changes: 3 additions & 0 deletions doc/iterative-resolving.nblink
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"path": "../examples/iterative-resolving.ipynb"
}
2 changes: 2 additions & 0 deletions doc/release_notes.rst
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ Release Notes
Upcoming Version
----------------

* Add ``Solver.resolve()`` method for re-solving a modified native solver model without rebuilding the linopy model. Implemented for HiGHS, Gurobi, and Mosek.
* Add ``Model.apply_result()`` to map a solver ``Result`` back onto model variables and constraints, enabling iterative solve workflows.
* Harmonize coordinate alignment for operations with subset/superset objects:
- Multiplication and division fill missing coords with 0 (variable doesn't participate)
- Addition and subtraction of constants fill missing coords with 0 (identity element) and pin result to LHS coords
Expand Down
302 changes: 302 additions & 0 deletions examples/iterative-resolving.ipynb
Original file line number Diff line number Diff line change
@@ -0,0 +1,302 @@
{
"cells": [
{
"cell_type": "markdown",
"id": "d21785bf",
"metadata": {},
"source": [
"# Iterative Re-solving\n",
"\n",
"In many optimization workflows you need to solve a model, tweak parameters on the native solver object, and re-solve — without rebuilding the entire linopy model. This is common in:\n",
"\n",
"- Sensitivity analysis (varying objective coefficients or bounds)\n",
"- Decomposition algorithms (Benders, column generation)\n",
"- Rolling-horizon / receding-horizon schemes\n",
"\n",
"Linopy supports this via two methods:\n",
"\n",
"- **`Solver.resolve()`** — re-solves an existing native solver model and returns a `Result`\n",
"- **`Model.apply_result()`** — maps a `Result` back onto the linopy model's variables and constraints\n",
"\n",
"Currently `resolve()` is implemented for the **HiGHS**, **Gurobi**, and **Mosek** solvers."
]
},
{
"cell_type": "markdown",
"id": "f7f604f5",
"metadata": {},
"source": [
"## Setup\n",
"\n",
"We start with a simple two-variable LP."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "122ee88e",
"metadata": {
"execution": {
"iopub.execute_input": "2026-03-16T08:48:03.207441Z",
"iopub.status.busy": "2026-03-16T08:48:03.207185Z",
"iopub.status.idle": "2026-03-16T08:48:03.649145Z",
"shell.execute_reply": "2026-03-16T08:48:03.648818Z"
}
},
"outputs": [],
"source": [
"import numpy as np\n",
"\n",
"from linopy import Model\n",
"from linopy.solvers import Highs"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "7c05110c",
"metadata": {
"execution": {
"iopub.execute_input": "2026-03-16T08:48:03.650662Z",
"iopub.status.busy": "2026-03-16T08:48:03.650440Z",
"iopub.status.idle": "2026-03-16T08:48:03.744258Z",
"shell.execute_reply": "2026-03-16T08:48:03.743824Z"
}
},
"outputs": [],
"source": [
"m = Model()\n",
"x = m.add_variables(lower=0, upper=10, name=\"x\")\n",
"y = m.add_variables(lower=0, upper=10, name=\"y\")\n",
"m.add_constraints(x + y >= 8, name=\"demand\")\n",
"m.add_constraints(x + y <= 15, name=\"capacity\")\n",
"m.objective = 2 * x + 3 * y\n",
"m"
]
},
{
"cell_type": "markdown",
"id": "574be9bd",
"metadata": {},
"source": [
"## Initial Solve\n",
"\n",
"Solve the model as usual. After solving, linopy stores the native solver object in `m.solver_model`."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "548b7a4d",
"metadata": {
"execution": {
"iopub.execute_input": "2026-03-16T08:48:03.745807Z",
"iopub.status.busy": "2026-03-16T08:48:03.745531Z",
"iopub.status.idle": "2026-03-16T08:48:03.787345Z",
"shell.execute_reply": "2026-03-16T08:48:03.786876Z"
}
},
"outputs": [],
"source": [
"m.solve(solver_name=\"highs\")\n",
"print(f\"Objective: {m.objective.value}\")\n",
"print(f\"x = {m.solution['x'].values.item():.2f}\")\n",
"print(f\"y = {m.solution['y'].values.item():.2f}\")"
]
},
{
"cell_type": "markdown",
"id": "214cbe09",
"metadata": {},
"source": [
"## Modify the Native Solver Model\n",
"\n",
"Access the native HiGHS object and change the objective coefficients. Here we make `x` much more expensive, so the solver should prefer `y`."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "b662db35",
"metadata": {
"execution": {
"iopub.execute_input": "2026-03-16T08:48:03.789208Z",
"iopub.status.busy": "2026-03-16T08:48:03.788835Z",
"iopub.status.idle": "2026-03-16T08:48:03.801031Z",
"shell.execute_reply": "2026-03-16T08:48:03.800672Z"
}
},
"outputs": [],
"source": [
"h = m.solver_model\n",
"n_cols = h.getNumCol()\n",
"new_costs = np.array([10.0, 1.0], dtype=float)\n",
"h.changeColsCost(n_cols, np.arange(n_cols, dtype=np.int32), new_costs)"
]
},
{
"cell_type": "markdown",
"id": "9e55e60b",
"metadata": {},
"source": [
"## Re-solve and Apply\n",
"\n",
"Create a solver instance, call `resolve()`, then apply the result back to the linopy model."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "949ce62e",
"metadata": {
"execution": {
"iopub.execute_input": "2026-03-16T08:48:03.802549Z",
"iopub.status.busy": "2026-03-16T08:48:03.802443Z",
"iopub.status.idle": "2026-03-16T08:48:03.818907Z",
"shell.execute_reply": "2026-03-16T08:48:03.818509Z"
}
},
"outputs": [],
"source": [
"solver = Highs()\n",
"result = solver.resolve(h, sense=m.sense)\n",
"m.apply_result(result, solver_name=\"highs\")\n",
"\n",
"print(f\"Objective: {m.objective.value}\")\n",
"print(f\"x = {m.solution['x'].values.item():.2f}\")\n",
"print(f\"y = {m.solution['y'].values.item():.2f}\")"
]
},
{
"cell_type": "markdown",
"id": "5b2eab9a",
"metadata": {},
"source": [
"Since `x` is now 10× more expensive than `y`, the solver allocates as much as possible to `y`."
]
},
{
"cell_type": "markdown",
"id": "7fd50c4d",
"metadata": {},
"source": [
"## Iterating Over Parameter Sweeps\n",
"\n",
"The `resolve` / `apply_result` pattern is efficient for parameter sweeps because it avoids rebuilding the model from scratch each time."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "bb3ac524",
"metadata": {
"execution": {
"iopub.execute_input": "2026-03-16T08:48:03.820485Z",
"iopub.status.busy": "2026-03-16T08:48:03.820382Z",
"iopub.status.idle": "2026-03-16T08:48:03.862574Z",
"shell.execute_reply": "2026-03-16T08:48:03.862143Z"
}
},
"outputs": [],
"source": [
"import pandas as pd\n",
"\n",
"cost_x_values = [1.0, 2.0, 5.0, 10.0, 20.0]\n",
"results = []\n",
"\n",
"for cost_x in cost_x_values:\n",
" h.changeColsCost(\n",
" n_cols, np.arange(n_cols, dtype=np.int32), np.array([cost_x, 3.0], dtype=float)\n",
" )\n",
" result = solver.resolve(h, sense=m.sense)\n",
" m.apply_result(result, solver_name=\"highs\")\n",
" results.append(\n",
" {\n",
" \"cost_x\": cost_x,\n",
" \"objective\": m.objective.value,\n",
" \"x\": m.solution[\"x\"].values.item(),\n",
" \"y\": m.solution[\"y\"].values.item(),\n",
" }\n",
" )\n",
"\n",
"pd.DataFrame(results)"
]
},
{
"cell_type": "markdown",
"id": "6f84f855",
"metadata": {},
"source": [
"## Handling Infeasible Results\n",
"\n",
"When a re-solve yields an infeasible or otherwise non-optimal result, `apply_result` returns the status without setting variable solutions. You can also pass `solution=None` in a `Result` object."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "acc2e8bb",
"metadata": {
"execution": {
"iopub.execute_input": "2026-03-16T08:48:03.864315Z",
"iopub.status.busy": "2026-03-16T08:48:03.864083Z",
"iopub.status.idle": "2026-03-16T08:48:03.879731Z",
"shell.execute_reply": "2026-03-16T08:48:03.879245Z"
}
},
"outputs": [],
"source": [
"from linopy.constants import Result, Status, TerminationCondition\n",
"\n",
"status = Status.from_termination_condition(TerminationCondition.infeasible)\n",
"result = Result(status=status, solution=None)\n",
"\n",
"s, tc = m.apply_result(result)\n",
"print(f\"Status: {s}, Termination: {tc}\")"
]
},
{
"cell_type": "markdown",
"id": "898f4824",
"metadata": {},
"source": [
"## API Reference\n",
"\n",
"**`Solver.resolve(solver_model, sense=\"min\")`**\n",
"\n",
"Re-solves a native solver object and returns a `Result`. Implemented for `Highs`, `Gurobi`, and `Mosek`. Raises `NotImplementedError` for other solvers.\n",
"\n",
"**`Model.apply_result(result, solver_name=None)`**\n",
"\n",
"Applies a `Result` to the model:\n",
"1. Resets existing solutions\n",
"2. Sets status and termination condition\n",
"3. Maps primal values back to variables\n",
"4. Maps dual values back to constraints (if available)\n",
"\n",
"Returns a `(status, termination_condition)` tuple."
]
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.12.3"
}
},
"nbformat": 4,
"nbformat_minor": 5
}
Loading
Loading