forked from PyPSA/PyPSA
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathgeneration-investment-screening-curve.py
127 lines (91 loc) · 3.67 KB
/
generation-investment-screening-curve.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
# -*- coding: utf-8 -*-
## Show classic screening curve analysis for generation investment
#
# Compute the long-term equilibrium power plant investment for a given load duration curve (1000-1000z for z \in [0, 1]) and a given set of generator investment options.
#
# Available as a Jupyter notebook at https://pypsa.readthedocs.io/en/latest/examples/generation-investment-screening-curve.ipynb.
import numpy as np
import pandas as pd
import pypsa
# %matplotlib inline
# Generator marginal (m) and capital (c) costs in EUR/MWh - numbers chosen for simple answer
generators = {
"coal": {"m": 2, "c": 15},
"gas": {"m": 12, "c": 10},
"load-shedding": {"m": 1012, "c": 0},
}
# Screening curve intersections at 0.01 and 0.5
x = np.linspace(0, 1, 101)
df = pd.DataFrame(
{key: pd.Series(item["c"] + x * item["m"], x) for key, item in generators.items()}
)
df.plot(ylim=[0, 50], title="Screening Curve")
n = pypsa.Network()
num_snapshots = 1001
snapshots = np.linspace(0, 1, num_snapshots)
n.set_snapshots(snapshots)
n.snapshot_weightings = n.snapshot_weightings / num_snapshots
n.add("Bus", name="bus")
n.add("Load", name="load", bus="bus", p_set=1000 - 1000 * snapshots)
for gen in generators:
n.add(
"Generator",
name=gen,
bus="bus",
p_nom_extendable=True,
marginal_cost=float(generators[gen]["m"]),
capital_cost=float(generators[gen]["c"]),
)
n.loads_t.p_set.plot(title="Load Duration Curve")
n.lopf(solver_name="cbc")
print(n.objective)
# capacity set by total electricity required
# NB: no load shedding since all prices < 1e4
n.generators.p_nom_opt.round(2)
n.buses_t.marginal_price.plot(title="Price Duration Curve")
# The prices correspond either to VOLL (1012) for first 0.01 or the marginal costs (12 for 0.49 and 2 for 0.5)
# EXCEPT for (infinitesimally small) points at the screening curve intersections, which
# correspond to changing the load duration near the intersection, so that capacity changes
# This explains 7 = (12+10 - 15) (replacing coal with gas) and 22 = (12+10) (replacing load-shedding with gas)
# I have no idea what is causing \l = 0; it should be 2.
n.buses_t.marginal_price.round(2).sum(axis=1).value_counts()
n.generators_t.p.plot(ylim=[0, 600], title="Generation Dispatch")
# Demonstrate zero-profit condition
print("Total costs:")
print(
n.generators.p_nom_opt * n.generators.capital_cost
+ n.generators_t.p.multiply(n.snapshot_weightings.generators, axis=0).sum()
* n.generators.marginal_cost
)
print("\nTotal revenue:")
print(
n.generators_t.p.multiply(n.snapshot_weightings.generators, axis=0)
.multiply(n.buses_t.marginal_price["bus"], axis=0)
.sum()
)
## Without expansion optimisation
#
# Take the capacities from the above long-term equilibrium, then disallow expansion.
#
# Show that the resulting market prices are identical.
#
# This holds in this example, but does NOT necessarily hold and breaks down in some circumstances (for example, when there is a lot of storage and inter-temporal shifting).
n.generators.p_nom_extendable = False
n.generators.p_nom = n.generators.p_nom_opt
n.lopf(solver_name="glpk")
n.buses_t.marginal_price.plot(title="Price Duration Curve")
n.buses_t.marginal_price.sum(axis=1).value_counts()
# Demonstrate zero-profit condition
# Differences are due to singular times, see above, not a problem
print("Total costs:")
print(
n.generators.p_nom * n.generators.capital_cost
+ n.generators_t.p.multiply(n.snapshot_weightings.generators, axis=0).sum()
* n.generators.marginal_cost
)
print("Total revenue:")
print(
n.generators_t.p.multiply(n.snapshot_weightings.generators, axis=0)
.multiply(n.buses_t.marginal_price["bus"], axis=0)
.sum()
)