New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Introduce nomopyomo #99
Changes from 45 commits
cf787d7
340cab5
e259977
4522d92
d6eb31d
55d4a82
c3c194f
afa98b2
c0c2740
f0dffde
771dc94
a50f43c
9409c78
2ecee3e
0ff03aa
d6134ac
508a3bd
53ac07b
aca70c6
3093ebd
3a636ab
eab33d2
d2c9c7e
8ebe4cb
b85f685
39cfda4
03193db
dbcaedb
0dfa5ce
7df314f
e30f9d3
67c4875
cff7ec8
ef29cbf
bac787a
db5e6fb
c317f04
40533b8
7aaa1f0
be562a2
ef882ae
0bc4286
831a2a1
01706ac
135d2ad
43c4e1d
a8d8ba1
8cfce1f
2a8c339
7a989cb
a4f3eb3
1019b4a
8c834b0
e82e5ac
5a809c2
f058738
d99f931
4be6bef
aabca4f
5ee71d1
1b0b796
cf5cb39
1756375
b299ba9
15cfeb1
1f84cb0
62f5264
6acb6e8
4a4badb
1e6d736
0990cfc
fcec8bd
6453126
83f63f8
b78e7e5
16a814f
3b7c74d
b726cae
4dc1f51
5e6548d
156f25b
df08282
67be12b
841f74c
a553d68
98f53ff
e8be54b
3fd1a83
d0f096f
41a4762
882c030
4f4769d
2e9eb10
07f3990
2e3201d
90fdeb7
a57039d
1e23865
c0aaf8b
842ee8b
73d8e5c
5bdbcd2
c568ec8
0358473
056af01
a3ef8a6
69c9880
f0e533b
cbe6e81
4991457
ceb5406
9112473
b0f1d3a
7026e18
70ecac5
8f443cd
b381048
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,5 +1,6 @@ | ||
include pypsa/component_attrs/*.csv | ||
include pypsa/standard_types/*.csv | ||
include pypsa/components.csv | ||
include pypsa/variables.csv | ||
include README.rst LICENSE.txt | ||
include requirements.yml |
Large diffs are not rendered by default.
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -14,3 +14,4 @@ dependencies: | |
- cartopy>=0.16 | ||
- coincbc | ||
- glpk | ||
- gurobi::gurobi | ||
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -21,24 +21,18 @@ | |
|
||
# make the code as Python 3 compatible as possible | ||
from __future__ import division, absolute_import | ||
import six | ||
from six import iteritems, itervalues, iterkeys | ||
from six.moves import map | ||
from weakref import ref | ||
|
||
|
||
__author__ = "Tom Brown (FIAS), Jonas Hoersch (FIAS), David Schlachtberger (FIAS)" | ||
__copyright__ = "Copyright 2015-2017 Tom Brown (FIAS), Jonas Hoersch (FIAS), David Schlachtberger (FIAS), GNU GPL 3" | ||
|
||
import networkx as nx | ||
|
||
import numpy as np | ||
import pandas as pd | ||
import scipy as sp, scipy.sparse | ||
from scipy.sparse import csgraph | ||
from itertools import chain | ||
from collections import namedtuple | ||
from operator import itemgetter | ||
import os | ||
|
||
Comment on lines
30
to
37
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Wow that's a lot of unneeded imports. Didn't know. |
||
|
||
|
@@ -71,10 +65,11 @@ | |
|
||
from .graph import graph, incidence_matrix, adjacency_matrix | ||
|
||
import inspect | ||
|
||
import sys | ||
|
||
if sys.version_info.major >= 3: | ||
from .linopf import network_lopf as network_lopf_lowmem | ||
|
||
import logging | ||
logger = logging.getLogger(__name__) | ||
|
||
|
@@ -198,7 +193,7 @@ class Network(Basic): | |
|
||
pf = network_pf | ||
|
||
lopf = network_lopf | ||
# lopf = network_lopf | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. delete? |
||
|
||
opf = network_opf | ||
|
||
|
@@ -407,6 +402,102 @@ def set_snapshots(self,snapshots): | |
|
||
#NB: No need to rebind pnl to self, since haven't changed it | ||
|
||
def lopf(self, snapshots=None, solver_name="glpk", extra_functionality=None, | ||
solver_logfile=None, solver_options={}, keep_files=False, | ||
formulation="kirchhoff", extra_postprocessing=None, pyomo=True, | ||
**kwargs): | ||
""" | ||
Linear optimal power flow for a group of snapshots. | ||
|
||
Parameters | ||
---------- | ||
snapshots : list or index slice | ||
A list of snapshots to optimise, must be a subset of | ||
network.snapshots, defaults to network.snapshots | ||
solver_name : string | ||
Must be a solver name that pyomo recognises and that is | ||
installed, e.g. "glpk", "gurobi" | ||
pyomo : bool, default True | ||
Whether to use pyomo for building and solving the model, setting | ||
this to False saves a lot of memory and time. | ||
solver_logfile : None|string | ||
If not None, sets the logfile option of the solver. | ||
solver_options : dictionary | ||
A dictionary with additional options that get passed to the solver. | ||
(e.g. {'threads':2} tells gurobi to use only 2 cpus) | ||
keep_files : bool, default False | ||
Keep the files that pyomo constructs from OPF problem | ||
construction, e.g. .lp file - useful for debugging | ||
formulation : string | ||
Formulation of the linear power flow equations to use; must be | ||
one of ["angles","cycles","kirchhoff","ptdf"] | ||
extra_functionality : callable function | ||
This function must take two arguments | ||
`extra_functionality(network,snapshots)` and is called after | ||
the model building is complete, but before it is sent to the | ||
solver. It allows the user to | ||
add/change constraints and add/change the objective function. | ||
|
||
Other Parameters | ||
---------------- | ||
|
||
ptdf_tolerance : float | ||
Only when pyomo is True. | ||
Value below which PTDF entries are ignored | ||
free_memory : set, default {'pyomo'} | ||
Only when pyomo is True. | ||
Any subset of {'pypsa', 'pyomo'}. Allows to stash `pypsa` time-series | ||
data away while the solver runs (as a pickle to disk) and/or free | ||
`pyomo` data after the solution has been extracted. | ||
solver_io : string, default None | ||
Only when pyomo is True. | ||
Solver Input-Output option, e.g. "python" to use "gurobipy" for | ||
solver_name="gurobi" | ||
skip_pre : bool, default False | ||
Only when pyomo is True. | ||
Skip the preliminary steps of computing topology, calculating | ||
dependent values and finding bus controls. | ||
extra_postprocessing : callable function | ||
This function must take three arguments | ||
`extra_postprocessing(network,snapshots,duals)` and is called after | ||
the model has solved and the results are extracted. It allows the user | ||
to extract further information about the solution, such as additional | ||
shadow prices. | ||
warmstart : bool or string, default False | ||
Only when pyomo is False. | ||
Use this to warmstart the optimization. Pass a string which gives | ||
the path to the basis file. If set to True, a path to | ||
a basis file must be given in network.basis_fn. | ||
store_basis : bool, default True | ||
Only when pyomo is False. | ||
Whether to store the basis of the optimization results. If True, | ||
the path to the basis file is saved in network.basis_fn. Note that | ||
a basis can only be stored if simplex, dual-simplex, or barrier | ||
*with* crossover is used for solving. | ||
keep_references : bool, default False | ||
Only when pyomo is False. | ||
Keep the references of variable and constraint names withing the | ||
network, e.g. n.generators_t.p_varref - useful for constructing | ||
extra_functionality or debugging | ||
keep_shadowprices : bool or list of component names, default None | ||
Only when pyomo is False. | ||
Keep shadow prices for all constraints, if set to True. | ||
These are stored at e.g. n.generators_t.mu_upper for upper limit | ||
of p_nom. If a list of component names is passed, shadow | ||
prices of variables attached to those are extracted. If set to None, | ||
components default to ['Bus', 'Line', 'GlobalConstraint'] | ||
|
||
""" | ||
args = {'snapshots': snapshots, 'keep_files': keep_files, | ||
'solver_options': solver_options, 'formulation': formulation, | ||
'extra_functionality': extra_functionality, | ||
'solver_name': solver_name, 'solver_logfile': solver_logfile} | ||
args.update(kwargs) | ||
if pyomo: | ||
return network_lopf(self, **args) | ||
else: | ||
return network_lopf_lowmem(self, **args) | ||
|
||
|
||
|
||
def add(self, class_name, name, **kwargs): | ||
|
@@ -834,6 +925,8 @@ def determine_network_topology(self): | |
|
||
for sub in self.sub_networks.obj: | ||
find_cycles(sub) | ||
sub.find_bus_controls() | ||
|
||
|
||
def iterate_components(self, components=None, skip_empty=True): | ||
if components is None: | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -22,19 +22,15 @@ | |
# make the code as Python 3 compatible as possible | ||
from __future__ import division | ||
from __future__ import absolute_import | ||
from six import iteritems, string_types | ||
from six import iteritems | ||
|
||
|
||
__author__ = "Tom Brown (FIAS), Jonas Hoersch (FIAS)" | ||
__copyright__ = "Copyright 2015-2017 Tom Brown (FIAS), Jonas Hoersch (FIAS), GNU GPL 3" | ||
|
||
|
||
|
||
|
||
|
||
#weak references are necessary to make sure the key-value pair are | ||
#destroyed if the key object goes out of scope | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. delete if relevant imports are removed? |
||
from weakref import WeakKeyDictionary | ||
|
||
from collections import OrderedDict | ||
from itertools import repeat | ||
|
@@ -303,3 +299,73 @@ def zsum(s, *args, **kwargs): | |
Meant to be set as pd.Series.zsum = zsum. | ||
""" | ||
return 0 if s.empty else s.sum(*args, **kwargs) | ||
|
||
#Perhaps this should rather go into components.py | ||
nominal_attrs = {'Generator': 'p_nom', | ||
'Line': 's_nom', | ||
'Transformer': 's_nom', | ||
'Link': 'p_nom', | ||
'Store': 'e_nom', | ||
'StorageUnit': 'p_nom'} | ||
|
||
def expand_series(ser, columns): | ||
""" | ||
Helper function to quickly expand a series to a dataframe with according | ||
column axis and every single column being the equal to the given series. | ||
""" | ||
return ser.to_frame(columns[0]).reindex(columns=columns).ffill(axis=1) | ||
|
||
|
||
def get_extendable_i(n, c): | ||
""" | ||
Getter function. Get the index of extendable elements of a given component. | ||
""" | ||
return n.df(c)[lambda ds: | ||
ds[nominal_attrs[c] + '_extendable']].index | ||
|
||
def get_non_extendable_i(n, c): | ||
""" | ||
Getter function. Get the index of non-extendable elements of a given | ||
component. | ||
""" | ||
return n.df(c)[lambda ds: | ||
~ds[nominal_attrs[c] + '_extendable']].index | ||
|
||
def get_bounds_pu(n, c, sns, index=slice(None), attr=None): | ||
""" | ||
Getter function to retrieve the per unit bounds of a given compoent for | ||
given snapshots and possible subset of elements (e.g. non-extendables). | ||
Depending on the attr you can further specify the bounds of the variable | ||
you are looking at, e.g. p_store for storage units. | ||
|
||
Parameters | ||
---------- | ||
n : pypsa.Network | ||
c : string | ||
Component name, e.g. "Generator", "Line". | ||
sns : pandas.Index/pandas.DateTimeIndex | ||
set of snapshots for the bounds | ||
index : pd.Index, default None | ||
Subset of the component elements. If None (default) bounds of all | ||
elements are returned. | ||
attr : string, default None | ||
attribute name for the bounds, e.g. "p", "s", "p_store" | ||
|
||
""" | ||
min_pu_str = nominal_attrs[c].replace('nom', 'min_pu') | ||
max_pu_str = nominal_attrs[c].replace('nom', 'max_pu') | ||
|
||
max_pu = get_switchable_as_dense(n, c, max_pu_str, sns) | ||
if c in n.passive_branch_components: | ||
min_pu = - max_pu | ||
elif c == 'StorageUnit': | ||
min_pu = pd.DataFrame(0, max_pu.index, max_pu.columns) | ||
if attr == 'p_store': | ||
max_pu = - get_switchable_as_dense(n, c, min_pu_str, sns) | ||
if attr == 'state_of_charge': | ||
max_pu = expand_series(n.df(c).max_hours, sns).T | ||
min_pu = pd.DataFrame(0, *max_pu.axes) | ||
else: | ||
min_pu = get_switchable_as_dense(n, c, min_pu_str, sns) | ||
return min_pu[index], max_pu[index] | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Does it make sense to include gurobi here as a dependency? I currently tend to say no. Any counterarguments?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
gurobipy is needed to solve the lp problem without pyomo and as for as I know, does not come automatically with pyomo (which is also in the dependency)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Jep, that's true. gurobi doesn't come with pyomo. But as I understand the script it's possible to chose between cbc and gurobi, isn't it?
I would suggest to remove gurobi as a dependency, as it's a commercial solver and the users of pypsa should decide on their own to buy and install gurobi or not.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes it is, supported for now are
I totally agree on the point of letting the user decide which solver to use. But is does not contradict. Installing gurobipy means only installing the interface, not the solver itself. This counts for the other solvers as well, none of them is installed pypsa. Like this the code can guarantee, that as soon as the solver is installed, the optimization runs.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
but if you guys don't agree, I don't mind taking it out :)