Security-Constrained Optimisation

Note

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

Security-Constrained Optimisation#

In this example, the dispatch of generators is optimised using the security-constrained linear OPF, to guaranteed that no branches are overloaded by certain branch outages.

[1]:
import numpy as np

import pypsa
[2]:
network = pypsa.examples.scigrid_de(from_master=True)
WARNING:pypsa.io:Importing network from PyPSA version v0.17.1 while current version is v0.34.0. Read the release notes at https://pypsa.readthedocs.io/en/latest/release_notes.html to prepare your network for import.
INFO:pypsa.io:Imported network scigrid-de.nc has buses, generators, lines, loads, storage_units, transformers

There are some infeasibilities without line extensions.

[3]:
for line_name in ["316", "527", "602"]:
    network.lines.loc[line_name, "s_nom"] = 1200

now = network.snapshots[0]

Performing security-constrained linear OPF

[4]:
branch_outages = network.lines.index[:15]
network.optimize.optimize_security_constrained(now, branch_outages=branch_outages)
WARNING:pypsa.consistency:The following transformers have zero r, which could break the linear load flow:
Index(['2', '5', '10', '12', '13', '15', '18', '20', '22', '24', '26', '30',
       '32', '37', '42', '46', '52', '56', '61', '68', '69', '74', '78', '86',
       '87', '94', '95', '96', '99', '100', '104', '105', '106', '107', '117',
       '120', '123', '124', '125', '128', '129', '138', '143', '156', '157',
       '159', '160', '165', '184', '191', '195', '201', '220', '231', '232',
       '233', '236', '247', '248', '250', '251', '252', '261', '263', '264',
       '267', '272', '279', '281', '282', '292', '303', '307', '308', '312',
       '315', '317', '322', '332', '334', '336', '338', '351', '353', '360',
       '362', '382', '384', '385', '391', '403', '404', '413', '421', '450',
       '458'],
      dtype='object', name='Transformer')
INFO:linopy.model: Solve problem using Highs solver
INFO:linopy.io: Writing time: 0.23s
INFO:linopy.constants: Optimization successful:
Status: ok
Termination condition: optimal
Solution: 2485 primals, 34397 duals
Objective: 3.48e+05
Solver model: available
Solver message: optimal

Running HiGHS 1.10.0 (git hash: fd86653): Copyright (c) 2025 HiGHS under MIT licence terms
WARNING: LP matrix packed vector contains 4 |values| in [9.11236e-10, 9.11239e-10] less than or equal to 1e-09: ignored
LP   linopy-problem-3vu6_xjp has 34397 rows; 2485 cols; 58467 nonzeros
Coefficient ranges:
  Matrix [1e-09, 2e+02]
  Cost   [3e+00, 1e+02]
  Bound  [0e+00, 0e+00]
  RHS    [1e-07, 7e+03]
Presolving model
15330 rows, 1653 cols, 33584 nonzeros  0s
12730 rows, 1423 cols, 28489 nonzeros  0s
6717 rows, 1269 cols, 16323 nonzeros  0s
Dependent equations search running on 522 equations with time limit of 1000.00s
Dependent equations search removed 0 rows and 0 nonzeros in 0.00s (limit = 1000.00s)
6716 rows, 1265 cols, 16318 nonzeros  0s
Presolve : Reductions: rows 6716(-27681); columns 1265(-1220); elements 16318(-42149)
Solving the presolved LP
Using EKK dual simplex solver - serial
  Iteration        Objective     Infeasibilities num(sum)
          0     0.0000000000e+00 Ph1: 0(0) 0s
        707     3.4788709255e+05 Pr: 0(0) 0s
Solving the original LP from the solution after postsolve
Model name          : linopy-problem-3vu6_xjp
Model status        : Optimal
Simplex   iterations: 707
Objective value     :  3.4788709255e+05
Relative P-D gap    :  1.9743486133e-14
HiGHS run time      :          0.13
Writing the solution to /tmp/linopy-solve-jx82e44s.sol
INFO:pypsa.optimization.optimize:The shadow-prices of the constraints Generator-fix-p-lower, Generator-fix-p-upper, Line-fix-s-lower, Line-fix-s-upper, Transformer-fix-s-lower, Transformer-fix-s-upper, StorageUnit-fix-p_dispatch-lower, StorageUnit-fix-p_dispatch-upper, StorageUnit-fix-p_store-lower, StorageUnit-fix-p_store-upper, StorageUnit-fix-state_of_charge-lower, StorageUnit-fix-state_of_charge-upper, Kirchhoff-Voltage-Law, StorageUnit-energy_balance, Transformer-fix-s-lower-security-for-Line-outage-in-<pypsa.networks.SubNetwork object at 0x711636f82510>, Transformer-fix-s-upper-security-for-Line-outage-in-<pypsa.networks.SubNetwork object at 0x711636f82510>, Line-fix-s-lower-security-for-Line-outage-in-<pypsa.networks.SubNetwork object at 0x711636f82510>, Line-fix-s-upper-security-for-Line-outage-in-<pypsa.networks.SubNetwork object at 0x711636f82510> were not assigned to the network.
[4]:
('ok', 'optimal')

For the PF, set the P to the optimised P.

[5]:
network.generators_t.p_set = network.generators_t.p_set.reindex(
    columns=network.generators.index
)
network.generators_t.p_set.loc[now] = network.generators_t.p.loc[now]

network.storage_units_t.p_set = network.storage_units_t.p_set.reindex(
    columns=network.storage_units.index
)
network.storage_units_t.p_set.loc[now] = network.storage_units_t.p.loc[now]

Check no lines are overloaded with the linear contingency analysis

[6]:
p0_test = network.lpf_contingency(now, branch_outages=branch_outages)
p0_test
INFO:pypsa.pf:Performing linear load-flow on AC sub-network <pypsa.networks.SubNetwork object at 0x711636dac690> for snapshot(s) DatetimeIndex(['2011-01-01'], dtype='datetime64[ns]', name='snapshot', freq=None)
WARNING:pypsa.contingency:No type given for 1, assuming it is a line
WARNING:pypsa.contingency:No type given for 2, assuming it is a line
WARNING:pypsa.contingency:No type given for 3, assuming it is a line
WARNING:pypsa.contingency:No type given for 4, assuming it is a line
WARNING:pypsa.contingency:No type given for 5, assuming it is a line
WARNING:pypsa.contingency:No type given for 6, assuming it is a line
WARNING:pypsa.contingency:No type given for 7, assuming it is a line
WARNING:pypsa.contingency:No type given for 8, assuming it is a line
WARNING:pypsa.contingency:No type given for 9, assuming it is a line
WARNING:pypsa.contingency:No type given for 10, assuming it is a line
WARNING:pypsa.contingency:No type given for 11, assuming it is a line
WARNING:pypsa.contingency:No type given for 12, assuming it is a line
WARNING:pypsa.contingency:No type given for 13, assuming it is a line
WARNING:pypsa.contingency:No type given for 14, assuming it is a line
WARNING:pypsa.contingency:No type given for 15, assuming it is a line
[6]:
base (Line, 1) (Line, 2) (Line, 3) (Line, 4) (Line, 5) (Line, 6) (Line, 7) (Line, 8) (Line, 9) (Line, 10) (Line, 11) (Line, 12) (Line, 13) (Line, 14) (Line, 15)
Transformer 2 -398.673565 -398.673565 -418.812178 -359.776340 -494.531601 -456.944280 -398.465090 -398.186350 -398.250993 -398.719318 -398.867096 -390.267056 -407.034903 -406.571219 -398.866773 -398.697291
5 883.055798 883.055798 898.249126 811.125658 988.177522 745.490070 883.034697 883.006484 883.013027 883.107968 883.277662 860.524526 886.291013 886.111602 883.277291 883.084016
10 -227.642971 -227.642971 -227.040362 -227.465346 -225.951441 -302.148212 -239.209400 -254.674176 -251.087684 -227.628687 -227.583794 -209.997522 42.837917 27.838193 -227.583893 -227.636780
12 -1211.646078 -1211.646078 -1211.821855 -1211.717450 -1212.306155 -1192.941427 -1191.974369 -1165.672509 -1171.772269 -1211.648631 -1211.656385 -1216.321289 -1479.256768 -1464.416213 -1211.656367 -1211.646921
13 41.861950 41.861950 41.500969 41.608205 39.441350 41.627203 41.832211 41.792449 41.801671 67.075991 5.594733 39.431473 43.269600 43.191538 5.655277 73.316135
... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ...
Line 855 65.129874 65.129874 65.010504 65.078881 64.536480 64.147069 65.127058 65.123292 65.124165 65.140617 65.173879 64.753233 65.277580 65.269389 65.173805 65.134037
856 93.141666 93.141666 92.945010 93.049300 92.092940 91.247887 93.137330 93.131532 93.132877 93.160471 93.218669 92.505188 93.376631 93.363601 93.218540 93.148934
857 359.231589 359.231589 357.976182 359.455061 359.413098 372.664330 359.179307 359.109404 359.125615 359.237746 359.258512 356.942348 361.344965 361.227766 359.258467 359.235643
858 33.680485 33.680485 33.624797 33.654329 33.383510 33.144210 33.679257 33.677615 33.677996 33.685810 33.702290 33.500249 33.747022 33.743332 33.702254 33.682543
859 182.371439 182.371439 181.969318 182.274410 181.001252 181.315363 182.360064 182.344856 182.348383 182.396657 182.474892 181.282509 182.909996 182.880130 182.474719 182.381371

948 rows × 16 columns

Check loading as per unit of s_nom in each contingency

[7]:
max_loading = (
    abs(p0_test.divide(network.passive_branches().s_nom, axis=0)).describe().loc["max"]
)
max_loading
[7]:
base          1.0
(Line, 1)     1.0
(Line, 2)     1.0
(Line, 3)     1.0
(Line, 4)     1.0
(Line, 5)     1.0
(Line, 6)     1.0
(Line, 7)     1.0
(Line, 8)     1.0
(Line, 9)     1.0
(Line, 10)    1.0
(Line, 11)    1.0
(Line, 12)    1.0
(Line, 13)    1.0
(Line, 14)    1.0
(Line, 15)    1.0
Name: max, dtype: float64
[8]:
np.allclose(max_loading, np.ones(len(max_loading)))
[8]:
True