Note

You can download this example as a Jupyter notebook or start it in interactive mode.

Modifying Models#

Given a model that is already built and possibly optimized, the user might want to modify single constraint or variable bounds by means of correction or exploration of the feasible space.

In the following we show how single elements can be tweaked or rewritten. Let’s start with the simple model of the Getting Started section.

[1]:
import pandas as pd
import xarray as xr

import linopy
[2]:
m = linopy.Model()
time = pd.Index(range(10), name="time")

x = m.add_variables(
    lower=0,
    coords=[time],
    name="x",
)
y = m.add_variables(lower=0, coords=[time], name="y")

factor = pd.Series(time, index=time)

con1 = m.add_constraints(3 * x + 7 * y >= 10 * factor, name="con1")
con2 = m.add_constraints(5 * x + 2 * y >= 3 * factor, name="con2")

m.add_objective(x + 2 * y)
m.solve(solver_name="highs")

m.solve(solver_name="highs")
sol = m.solution.to_dataframe()
sol.plot(grid=True, ylabel="Optimal Value")
Running HiGHS 1.14.0 (git hash: 7df0786): Copyright (c) 2026 under MIT licence terms
LP linopy-problem-pjxkbjvt has 20 rows; 20 cols; 40 nonzeros
Coefficient ranges:
  Matrix  [2e+00, 7e+00]
  Cost    [1e+00, 2e+00]
  Bound   [0e+00, 0e+00]
  RHS     [3e+00, 9e+01]
Presolving model
18 rows, 18 cols, 36 nonzeros 0s
18 rows, 18 cols, 36 nonzeros 0s
Presolve reductions: rows 18(-2); columns 18(-2); nonzeros 36(-4)
Solving the presolved LP
Using dual simplex solver
  Iteration        Objective     Infeasibilities num(sum)
          0     0.0000000000e+00 Pr: 18(585) 0.0s
         18     1.2879310345e+02 Pr: 0(0) 0.0s

Performed postsolve
Solving the original LP from the solution after postsolve

Model name          : linopy-problem-pjxkbjvt
Model status        : Optimal
Simplex   iterations: 18
Objective value     :  1.2879310345e+02
P-D objective error :  0.0000000000e+00
HiGHS run time      :          0.00
Running HiGHS 1.14.0 (git hash: 7df0786): Copyright (c) 2026 under MIT licence terms
LP linopy-problem-llord1tb has 20 rows; 20 cols; 40 nonzeros
Coefficient ranges:
  Matrix  [2e+00, 7e+00]
  Cost    [1e+00, 2e+00]
  Bound   [0e+00, 0e+00]
  RHS     [3e+00, 9e+01]
Presolving model
18 rows, 18 cols, 36 nonzeros 0s
18 rows, 18 cols, 36 nonzeros 0s
Presolve reductions: rows 18(-2); columns 18(-2); nonzeros 36(-4)
Solving the presolved LP
Using dual simplex solver
  Iteration        Objective     Infeasibilities num(sum)
          0     0.0000000000e+00 Pr: 18(585) 0.0s
         18     1.2879310345e+02 Pr: 0(0) 0.0s

Performed postsolve
Solving the original LP from the solution after postsolve

Model name          : linopy-problem-llord1tb
Model status        : Optimal
Simplex   iterations: 18
Objective value     :  1.2879310345e+02
P-D objective error :  0.0000000000e+00
HiGHS run time      :          0.00
[2]:
<Axes: xlabel='time', ylabel='Optimal Value'>
_images/manipulating-models_2_2.svg

The figure above shows the optimal values of x(t) and y(t).

Varying lower and upper bounds#

Now, let’s say we want to set the lower bound of x(t) to 1. This would translate to:

[3]:
x.lower = 1

Note

The same could have been achieved by calling m.variables.x.lower = 1

Let’s solve it again!

[4]:
m.solve(solver_name="highs")
sol = m.solution.to_dataframe()
sol.plot(grid=True, ylabel="Optimal Value")
Running HiGHS 1.14.0 (git hash: 7df0786): Copyright (c) 2026 under MIT licence terms
LP linopy-problem-fzyteqra has 20 rows; 20 cols; 40 nonzeros
Coefficient ranges:
  Matrix  [2e+00, 7e+00]
  Cost    [1e+00, 2e+00]
  Bound   [1e+00, 1e+00]
  RHS     [3e+00, 9e+01]
Presolving model
17 rows, 18 cols, 34 nonzeros 0s
16 rows, 16 cols, 32 nonzeros 0s
Presolve reductions: rows 16(-4); columns 16(-4); nonzeros 32(-8)
Solving the presolved LP
Using dual simplex solver
  Iteration        Objective     Infeasibilities num(sum)
          0     1.2000015319e+01 Pr: 16(508) 0.0s
          8     1.3085714286e+02 Pr: 0(0) 0.0s

Performed postsolve
Solving the original LP from the solution after postsolve

Model name          : linopy-problem-fzyteqra
Model status        : Optimal
Simplex   iterations: 8
Objective value     :  1.3085714286e+02
P-D objective error :  0.0000000000e+00
HiGHS run time      :          0.00
[4]:
<Axes: xlabel='time', ylabel='Optimal Value'>
_images/manipulating-models_6_2.svg
[5]:
sol
[5]:
x y
time
0 1.0 0.000000
1 1.0 1.000000
2 1.0 2.428571
3 1.0 3.857143
4 1.0 5.285714
5 1.0 6.714286
6 1.0 8.142857
7 1.0 9.571429
8 1.0 11.000000
9 1.0 12.428571

We see that the new lower bound of x is binding across all time steps.

Of course the implementation is flexible over the dimensions, so we can pass non-scalar values:

[6]:
x.lower = xr.DataArray(range(10, 0, -1), coords=(time,))
[7]:
m.solve(solver_name="highs")
sol = m.solution.to_dataframe()
sol.plot(grid=True, ylabel="Optimal Value")
Running HiGHS 1.14.0 (git hash: 7df0786): Copyright (c) 2026 under MIT licence terms
LP linopy-problem-821gdn46 has 20 rows; 20 cols; 40 nonzeros
Coefficient ranges:
  Matrix  [2e+00, 7e+00]
  Cost    [1e+00, 2e+00]
  Bound   [1e+00, 1e+01]
  RHS     [3e+00, 9e+01]
Presolving model
10 rows, 14 cols, 20 nonzeros 0s
6 rows, 6 cols, 12 nonzeros 0s
Presolve reductions: rows 6(-14); columns 6(-14); nonzeros 12(-28)
Solving the presolved LP
Using dual simplex solver
  Iteration        Objective     Infeasibilities num(sum)
          0     8.7571439684e+01 Pr: 6(264) 0.0s
          3     1.5100000000e+02 Pr: 0(0) 0.0s

Performed postsolve
Solving the original LP from the solution after postsolve

Model name          : linopy-problem-821gdn46
Model status        : Optimal
Simplex   iterations: 3
Objective value     :  1.5100000000e+02
P-D objective error :  0.0000000000e+00
HiGHS run time      :          0.00
[7]:
<Axes: xlabel='time', ylabel='Optimal Value'>
_images/manipulating-models_10_2.svg

You can manipulate the upper bound of a variable in the same way.

Varying Constraints#

A similar functionality is implemented for constraints. Here we can modify the left-hand-side, the sign and the right-hand-side.

Assume we want to relax the right-hand-side of the first constraint con1 to 8 * factor. This would translate to:

[8]:
con1.rhs = 8 * factor

Note

The same could have been achieved by calling m.constraints.con1.rhs = 8 * factor

Let’s solve it again!

[9]:
m.solve(solver_name="highs")
sol = m.solution.to_dataframe()
sol.plot(grid=True, ylabel="Optimal Value")
Running HiGHS 1.14.0 (git hash: 7df0786): Copyright (c) 2026 under MIT licence terms
LP linopy-problem-wzfwbf3i has 20 rows; 20 cols; 40 nonzeros
Coefficient ranges:
  Matrix  [2e+00, 7e+00]
  Cost    [1e+00, 2e+00]
  Bound   [1e+00, 1e+01]
  RHS     [3e+00, 7e+01]
Presolving model
10 rows, 14 cols, 20 nonzeros 0s
6 rows, 6 cols, 12 nonzeros 0s
Presolve reductions: rows 6(-14); columns 6(-14); nonzeros 12(-28)
Solving the presolved LP
Using dual simplex solver
  Iteration        Objective     Infeasibilities num(sum)
          0     7.7285725398e+01 Pr: 6(216) 0.0s
          4     1.2707881773e+02 Pr: 0(0) 0.0s

Performed postsolve
Solving the original LP from the solution after postsolve

Model name          : linopy-problem-wzfwbf3i
Model status        : Optimal
Simplex   iterations: 4
Objective value     :  1.2707881773e+02
P-D objective error :  5.5694412943e-17
HiGHS run time      :          0.00
[9]:
<Axes: xlabel='time', ylabel='Optimal Value'>
_images/manipulating-models_15_2.svg

In contrast to previous figure, we now see that the optimal value of y does not reach values above 10 in the end.

In the same way, we can modify the left-hand-side. Assume we want to weight y with a coefficient of 8 in the constraints, this gives

[10]:
con1.lhs = 3 * x + 8 * y

Note: The same could have been achieved by calling

m.constraints['con1'].lhs = 3 * x + 8 * y

which leads to

[11]:
m.solve(solver_name="highs")
sol = m.solution.to_dataframe()
sol.plot(grid=True, ylabel="Optimal Value")
Running HiGHS 1.14.0 (git hash: 7df0786): Copyright (c) 2026 under MIT licence terms
LP linopy-problem-a7zm0uq9 has 20 rows; 20 cols; 40 nonzeros
Coefficient ranges:
  Matrix  [2e+00, 8e+00]
  Cost    [1e+00, 2e+00]
  Bound   [1e+00, 1e+01]
  RHS     [3e+00, 7e+01]
Presolving model
10 rows, 14 cols, 20 nonzeros 0s
6 rows, 6 cols, 12 nonzeros 0s
Presolve reductions: rows 6(-14); columns 6(-14); nonzeros 12(-28)
Solving the presolved LP
Using dual simplex solver
  Iteration        Objective     Infeasibilities num(sum)
          0     7.4500011112e+01 Pr: 6(216) 0.0s
          4     1.1827941176e+02 Pr: 0(0) 0.0s

Performed postsolve
Solving the original LP from the solution after postsolve

Model name          : linopy-problem-a7zm0uq9
Model status        : Optimal
Simplex   iterations: 4
Objective value     :  1.1827941176e+02
P-D objective error :  0.0000000000e+00
HiGHS run time      :          0.00
[11]:
<Axes: xlabel='time', ylabel='Optimal Value'>
_images/manipulating-models_20_2.svg

Varying the objective#

Varying the objective happens in the same way as for the left-hand-side of the constraint as it is a linear expression too. Note, when passing an unstacked linear expression, i.e. an expression with more than the _term dimension, linopy will automatically stack it.

So assume, we would like to modify the weight of y in the objective function, this translates to:

[12]:
m.objective = x + 3 * y
[13]:
m.solve(solver_name="highs")
sol = m.solution.to_dataframe()
sol.plot(grid=True, ylabel="Optimal Value")
Running HiGHS 1.14.0 (git hash: 7df0786): Copyright (c) 2026 under MIT licence terms
LP linopy-problem-oqrfw0zx has 20 rows; 20 cols; 40 nonzeros
Coefficient ranges:
  Matrix  [2e+00, 8e+00]
  Cost    [1e+00, 3e+00]
  Bound   [1e+00, 1e+01]
  RHS     [3e+00, 7e+01]
Presolving model
10 rows, 10 cols, 16 nonzeros 0s
6 rows, 6 cols, 12 nonzeros 0s
Presolve reductions: rows 6(-14); columns 6(-14); nonzeros 12(-28)
Solving the presolved LP
Using dual simplex solver
  Iteration        Objective     Infeasibilities num(sum)
          0     8.1000016669e+01 Pr: 6(216) 0.0s
          3     1.3900000000e+02 Pr: 0(0) 0.0s

Performed postsolve
Solving the original LP from the solution after postsolve

Model name          : linopy-problem-oqrfw0zx
Model status        : Optimal
Simplex   iterations: 3
Objective value     :  1.3900000000e+02
P-D objective error :  0.0000000000e+00
HiGHS run time      :          0.00
[13]:
<Axes: xlabel='time', ylabel='Optimal Value'>
_images/manipulating-models_23_2.svg

As a consequence, y stays at zero for all time steps.

[14]:
m.objective
[14]:
Objective:
----------
LinearExpression: +1 x[0] + 3 y[0] + 1 x[1] ... +3 y[8] + 1 x[9] + 3 y[9]
Sense: min
Value: 139.0

Fixing Variables and Extracting MILP Duals#

A common workflow in mixed-integer programming is to solve the MILP, then fix the integer/binary variables to their optimal values and re-solve as an LP to obtain dual values (shadow prices).

Let’s extend our model with a binary variable z that activates an additional capacity constraint on x.

[15]:
z = m.add_variables(binary=True, coords=[time], name="z")

# x can only exceed 5 when z is active: x <= 5 + 100 * z
m.add_constraints(x <= 5 + 100 * z, name="capacity")

# Penalize activation of z in the objective
m.objective = x + 3 * y + 10 * z

m.solve(solver_name="highs")
Running HiGHS 1.14.0 (git hash: 7df0786): Copyright (c) 2026 under MIT licence terms
MIP linopy-problem-wu2u882u has 30 rows; 30 cols; 60 nonzeros; 10 integer variables (10 binary)
Coefficient ranges:
  Matrix  [1e+00, 1e+02]
  Cost    [1e+00, 1e+01]
  Bound   [1e+00, 1e+01]
  RHS     [3e+00, 7e+01]
Presolving model
20 rows, 19 cols, 32 nonzeros 0s
15 rows, 19 cols, 30 nonzeros 0s
Presolve reductions: rows 15(-15); columns 19(-11); nonzeros 30(-30)

Solving MIP model with:
   15 rows
   19 cols (5 binary, 0 integer, 0 implied int., 14 continuous, 0 domain fixed)
   30 nonzeros

Src: B => Branching; C => Central rounding; F => Feasibility pump; H => Heuristic;
     I => Shifting; J => Feasibility jump; L => Sub-MIP; P => Empty MIP; R => Randomized rounding;
     S => Solve LP; T => Evaluate node; U => Unbounded; X => User solution; Y => HiGHS solution;
     Z => ZI Round; l => Trivial lower; p => Trivial point; u => Trivial upper; z => Trivial zero

        Nodes      |    B&B Tree     |            Objective Bounds              |  Dynamic Constraints |       Work
Src  Proc. InQueue |  Leaves   Expl. | BestBound       BestSol              Gap |   Cuts   InLp Confl. | LpIters     Time

         0       0         0   0.00%   105             inf                  inf        0      0      0         0     0.0s
 S       0       0         0   0.00%   105             239               56.07%        0      0      0         0     0.0s
         0       0         0   0.00%   195.8333333     239               18.06%        0      0      0        11     0.0s
 L       0       0         0   0.00%   197.5416667     197.5416667        0.00%        5      5      0        16     0.0s
         1       0         1 100.00%   197.5416667     197.5416667        0.00%        5      5      0        17     0.0s

Solving report
  Model             linopy-problem-wu2u882u
  Status            Optimal
  Primal bound      197.541666667
  Dual bound        197.541666667
  Gap               0% (tolerance: 0.01%)
  P-D integral      0.00129971041174
  Solution status   feasible
                    197.541666667 (objective)
                    0 (bound viol.)
                    0 (int. viol.)
                    0 (row viol.)
  Timing            0.01
  Max sub-MIP depth 1
  Nodes             1
  Repair LPs        0
  LP iterations     17
                    0 (strong br.)
                    5 (separation)
                    1 (heuristics)
[15]:
('ok', 'optimal')

Now fix the binary variable z to its optimal values and relax its integrality. This converts the model into an LP, which allows us to extract dual values.

[16]:
m.variables.binaries.fix()
m.variables.binaries.relax()
m.solve(solver_name="highs")

# Dual values are now available on the constraints
m.constraints["con1"].dual
Running HiGHS 1.14.0 (git hash: 7df0786): Copyright (c) 2026 under MIT licence terms
LP linopy-problem-4w1iphut has 40 rows; 30 cols; 70 nonzeros
Coefficient ranges:
  Matrix  [1e+00, 1e+02]
  Cost    [1e+00, 1e+01]
  Bound   [1e+00, 1e+01]
  RHS     [1e+00, 7e+01]
Presolving model
17 rows, 14 cols, 27 nonzeros 0s
6 rows, 8 cols, 12 nonzeros 0s
Presolve reductions: rows 6(-34); columns 8(-22); nonzeros 12(-58)
Solving the presolved LP
Using dual simplex solver
  Iteration        Objective     Infeasibilities num(sum)
          0     1.4512504460e+02 Pr: 6(180) 0.0s
          4     1.9754166667e+02 Pr: 0(0) 0.0s

Performed postsolve
Solving the original LP from the solution after postsolve

Model name          : linopy-problem-4w1iphut
Model status        : Optimal
Simplex   iterations: 4
Objective value     :  1.9754166667e+02
P-D objective error :  7.1756893155e-17
HiGHS run time      :          0.00
[16]:
<xarray.DataArray 'dual' (time: 10)> Size: 80B
array([-0.        , -0.        , -0.        ,  0.33333333,  0.33333333,
        0.375     ,  0.375     ,  0.375     ,  0.375     ,  0.375     ])
Coordinates:
  * time     (time) int64 80B 0 1 2 3 4 5 6 7 8 9

Calling unfix() on all variables removes the fix constraints and unrelax() restores the integrality of z.

[17]:
m.variables.unfix()
m.variables.unrelax()

# z is binary again
m.variables["z"].attrs["binary"]
[17]:
True