├── .gitignore
├── .travis.yml
├── Dockerfile
├── LICENSE
├── README.md
├── cvxpower
├── __init__.py
├── devices.py
├── devices_test.py
├── network.py
└── network_test.py
├── docs
├── Makefile
├── _static
│ └── dynamic_load_results.jpg
├── conf.py
├── devices.rst
├── examples.rst
├── get_started.rst
├── index.rst
├── network.rst
└── notebooks
│ ├── dynamic.rst
│ ├── dynamic_files
│ ├── dynamic_10_1.png
│ ├── dynamic_10_2.png
│ ├── dynamic_11_1.png
│ ├── dynamic_11_2.png
│ ├── dynamic_4_1.png
│ ├── dynamic_5_1.png
│ ├── dynamic_6_1.png
│ ├── dynamic_6_2.png
│ ├── dynamic_7_1.png
│ ├── dynamic_7_2.png
│ ├── dynamic_8_1.png
│ └── dynamic_9_1.png
│ ├── microgrid.rst
│ ├── smarthome.rst
│ ├── static.rst
│ ├── static_files
│ └── static_14_1.png
│ ├── three_bus.png
│ ├── windfarm.rst
│ └── windfarm_files
│ ├── windfarm_10_1.png
│ ├── windfarm_10_2.png
│ ├── windfarm_11_1.png
│ ├── windfarm_11_2.png
│ ├── windfarm_12_1.png
│ ├── windfarm_13_1.png
│ ├── windfarm_15_1.png
│ ├── windfarm_17_1.png
│ ├── windfarm_17_2.png
│ ├── windfarm_18_1.png
│ ├── windfarm_3_1.png
│ ├── windfarm_5_1.png
│ └── windfarm_8_1.png
├── examples
├── CaliforniaSolarEnergy.ipynb
├── HistoricalEMSHourlyLoad_2014-2016.xlsx
├── HomeEnergy.ipynb
├── ThreeBusExample.ipynb
├── WindForecast.ipynb
├── dynamic.ipynb
├── gen_bid_curve.py
├── ieee14cdf.txt
├── nrel_wind
│ ├── 20182-2007.csv
│ ├── 20182-2008.csv
│ ├── 20182-2009.csv
│ ├── 20182-2010.csv
│ ├── 20182-2011.csv
│ └── 20182-2012.csv
├── p_wind.pickle
├── residual_params.pickle
├── sigma_epsilon.pickle
├── static.ipynb
├── three_bus.png
├── wind_baseline.pickle
├── wind_power_test.pickle
├── wind_power_train.pickle
└── windfarm.ipynb
├── setup.py
└── tools
├── generate_notebooks.sh
├── push_docker.sh
└── run_ci.sh
/.gitignore:
--------------------------------------------------------------------------------
1 | # notebook
2 | .ipynb_checkpoints
3 |
4 | # python
5 | *.pyc
6 | *.egg-info
7 | .eggs
8 |
9 | # sphinx
10 | _build
11 |
12 | #OSX
13 | *.DS_Store
14 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | sudo: required
2 |
3 | language: python
4 |
5 | services:
6 | - docker
7 |
8 | before_install:
9 | - docker pull cvxgrp/dem
10 |
11 | script:
12 | - docker run -v $TRAVIS_BUILD_DIR:/build/dem cvxgrp/dem /build/dem/tools/run_ci.sh
13 |
14 | notifications:
15 | email: false
16 |
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM debian:latest
2 |
3 | # Debian packages
4 | RUN apt-get update && apt-get install -y \
5 | autoconf \
6 | autotools-dev \
7 | build-essential \
8 | bzip2 \
9 | cmake \
10 | curl \
11 | g++ \
12 | gfortran \
13 | git \
14 | libc-dev \
15 | libopenblas-dev \
16 | libquadmath0 \
17 | libtool \
18 | make \
19 | parallel \
20 | pkg-config \
21 | unzip \
22 | timelimit \
23 | wget \
24 | zip && apt-get clean
25 |
26 | # Python 2
27 | RUN apt-get install -y \
28 | python-dev \
29 | python-pip
30 | RUN python2 -m pip install -U pip
31 | RUN python2 -m pip install -U numpy scipy
32 | RUN python2 -m pip install -U cvxpy
33 |
34 | # Python 3
35 | RUN apt-get install -y \
36 | python3-dev \
37 | python3-pip
38 | RUN python3 -m pip install -U pip
39 | RUN python3 -m pip install -U numpy scipy
40 | RUN python3 -m pip install -U cvxpy
41 |
42 | CMD ["bash"]
43 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Apache License
2 | Version 2.0, January 2004
3 | http://www.apache.org/licenses/
4 |
5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6 |
7 | 1. Definitions.
8 |
9 | "License" shall mean the terms and conditions for use, reproduction,
10 | and distribution as defined by Sections 1 through 9 of this document.
11 |
12 | "Licensor" shall mean the copyright owner or entity authorized by
13 | the copyright owner that is granting the License.
14 |
15 | "Legal Entity" shall mean the union of the acting entity and all
16 | other entities that control, are controlled by, or are under common
17 | control with that entity. For the purposes of this definition,
18 | "control" means (i) the power, direct or indirect, to cause the
19 | direction or management of such entity, whether by contract or
20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
21 | outstanding shares, or (iii) beneficial ownership of such entity.
22 |
23 | "You" (or "Your") shall mean an individual or Legal Entity
24 | exercising permissions granted by this License.
25 |
26 | "Source" form shall mean the preferred form for making modifications,
27 | including but not limited to software source code, documentation
28 | source, and configuration files.
29 |
30 | "Object" form shall mean any form resulting from mechanical
31 | transformation or translation of a Source form, including but
32 | not limited to compiled object code, generated documentation,
33 | and conversions to other media types.
34 |
35 | "Work" shall mean the work of authorship, whether in Source or
36 | Object form, made available under the License, as indicated by a
37 | copyright notice that is included in or attached to the work
38 | (an example is provided in the Appendix below).
39 |
40 | "Derivative Works" shall mean any work, whether in Source or Object
41 | form, that is based on (or derived from) the Work and for which the
42 | editorial revisions, annotations, elaborations, or other modifications
43 | represent, as a whole, an original work of authorship. For the purposes
44 | of this License, Derivative Works shall not include works that remain
45 | separable from, or merely link (or bind by name) to the interfaces of,
46 | the Work and Derivative Works thereof.
47 |
48 | "Contribution" shall mean any work of authorship, including
49 | the original version of the Work and any modifications or additions
50 | to that Work or Derivative Works thereof, that is intentionally
51 | submitted to Licensor for inclusion in the Work by the copyright owner
52 | or by an individual or Legal Entity authorized to submit on behalf of
53 | the copyright owner. For the purposes of this definition, "submitted"
54 | means any form of electronic, verbal, or written communication sent
55 | to the Licensor or its representatives, including but not limited to
56 | communication on electronic mailing lists, source code control systems,
57 | and issue tracking systems that are managed by, or on behalf of, the
58 | Licensor for the purpose of discussing and improving the Work, but
59 | excluding communication that is conspicuously marked or otherwise
60 | designated in writing by the copyright owner as "Not a Contribution."
61 |
62 | "Contributor" shall mean Licensor and any individual or Legal Entity
63 | on behalf of whom a Contribution has been received by Licensor and
64 | subsequently incorporated within the Work.
65 |
66 | 2. Grant of Copyright License. Subject to the terms and conditions of
67 | this License, each Contributor hereby grants to You a perpetual,
68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69 | copyright license to reproduce, prepare Derivative Works of,
70 | publicly display, publicly perform, sublicense, and distribute the
71 | Work and such Derivative Works in Source or Object form.
72 |
73 | 3. Grant of Patent License. Subject to the terms and conditions of
74 | this License, each Contributor hereby grants to You a perpetual,
75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76 | (except as stated in this section) patent license to make, have made,
77 | use, offer to sell, sell, import, and otherwise transfer the Work,
78 | where such license applies only to those patent claims licensable
79 | by such Contributor that are necessarily infringed by their
80 | Contribution(s) alone or by combination of their Contribution(s)
81 | with the Work to which such Contribution(s) was submitted. If You
82 | institute patent litigation against any entity (including a
83 | cross-claim or counterclaim in a lawsuit) alleging that the Work
84 | or a Contribution incorporated within the Work constitutes direct
85 | or contributory patent infringement, then any patent licenses
86 | granted to You under this License for that Work shall terminate
87 | as of the date such litigation is filed.
88 |
89 | 4. Redistribution. You may reproduce and distribute copies of the
90 | Work or Derivative Works thereof in any medium, with or without
91 | modifications, and in Source or Object form, provided that You
92 | meet the following conditions:
93 |
94 | (a) You must give any other recipients of the Work or
95 | Derivative Works a copy of this License; and
96 |
97 | (b) You must cause any modified files to carry prominent notices
98 | stating that You changed the files; and
99 |
100 | (c) You must retain, in the Source form of any Derivative Works
101 | that You distribute, all copyright, patent, trademark, and
102 | attribution notices from the Source form of the Work,
103 | excluding those notices that do not pertain to any part of
104 | the Derivative Works; and
105 |
106 | (d) If the Work includes a "NOTICE" text file as part of its
107 | distribution, then any Derivative Works that You distribute must
108 | include a readable copy of the attribution notices contained
109 | within such NOTICE file, excluding those notices that do not
110 | pertain to any part of the Derivative Works, in at least one
111 | of the following places: within a NOTICE text file distributed
112 | as part of the Derivative Works; within the Source form or
113 | documentation, if provided along with the Derivative Works; or,
114 | within a display generated by the Derivative Works, if and
115 | wherever such third-party notices normally appear. The contents
116 | of the NOTICE file are for informational purposes only and
117 | do not modify the License. You may add Your own attribution
118 | notices within Derivative Works that You distribute, alongside
119 | or as an addendum to the NOTICE text from the Work, provided
120 | that such additional attribution notices cannot be construed
121 | as modifying the License.
122 |
123 | You may add Your own copyright statement to Your modifications and
124 | may provide additional or different license terms and conditions
125 | for use, reproduction, or distribution of Your modifications, or
126 | for any such Derivative Works as a whole, provided Your use,
127 | reproduction, and distribution of the Work otherwise complies with
128 | the conditions stated in this License.
129 |
130 | 5. Submission of Contributions. Unless You explicitly state otherwise,
131 | any Contribution intentionally submitted for inclusion in the Work
132 | by You to the Licensor shall be under the terms and conditions of
133 | this License, without any additional terms or conditions.
134 | Notwithstanding the above, nothing herein shall supersede or modify
135 | the terms of any separate license agreement you may have executed
136 | with Licensor regarding such Contributions.
137 |
138 | 6. Trademarks. This License does not grant permission to use the trade
139 | names, trademarks, service marks, or product names of the Licensor,
140 | except as required for reasonable and customary use in describing the
141 | origin of the Work and reproducing the content of the NOTICE file.
142 |
143 | 7. Disclaimer of Warranty. Unless required by applicable law or
144 | agreed to in writing, Licensor provides the Work (and each
145 | Contributor provides its Contributions) on an "AS IS" BASIS,
146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147 | implied, including, without limitation, any warranties or conditions
148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149 | PARTICULAR PURPOSE. You are solely responsible for determining the
150 | appropriateness of using or redistributing the Work and assume any
151 | risks associated with Your exercise of permissions under this License.
152 |
153 | 8. Limitation of Liability. In no event and under no legal theory,
154 | whether in tort (including negligence), contract, or otherwise,
155 | unless required by applicable law (such as deliberate and grossly
156 | negligent acts) or agreed to in writing, shall any Contributor be
157 | liable to You for damages, including any direct, indirect, special,
158 | incidental, or consequential damages of any character arising as a
159 | result of this License or out of the use or inability to use the
160 | Work (including but not limited to damages for loss of goodwill,
161 | work stoppage, computer failure or malfunction, or any and all
162 | other commercial damages or losses), even if such Contributor
163 | has been advised of the possibility of such damages.
164 |
165 | 9. Accepting Warranty or Additional Liability. While redistributing
166 | the Work or Derivative Works thereof, You may choose to offer,
167 | and charge a fee for, acceptance of support, warranty, indemnity,
168 | or other liability obligations and/or rights consistent with this
169 | License. However, in accepting such obligations, You may act only
170 | on Your own behalf and on Your sole responsibility, not on behalf
171 | of any other Contributor, and only if You agree to indemnify,
172 | defend, and hold each Contributor harmless for any liability
173 | incurred by, or claims asserted against, such Contributor by reason
174 | of your accepting any such warranty or additional liability.
175 |
176 | END OF TERMS AND CONDITIONS
177 |
178 | APPENDIX: How to apply the Apache License to your work.
179 |
180 | To apply the Apache License to your work, attach the following
181 | boilerplate notice, with the fields enclosed by brackets "{}"
182 | replaced with your own identifying information. (Don't include
183 | the brackets!) The text should be enclosed in the appropriate
184 | comment syntax for the file format. We also recommend that a
185 | file or class name and description of purpose be included on the
186 | same "printed page" as the copyright notice for easier
187 | identification within third-party archives.
188 |
189 | Copyright 2017 Steven Diamond
190 |
191 | Licensed under the Apache License, Version 2.0 (the "License");
192 | you may not use this file except in compliance with the License.
193 | You may obtain a copy of the License at
194 |
195 | http://www.apache.org/licenses/LICENSE-2.0
196 |
197 | Unless required by applicable law or agreed to in writing, software
198 | distributed under the License is distributed on an "AS IS" BASIS,
199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200 | See the License for the specific language governing permissions and
201 | limitations under the License.
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # `cvxpower`
2 |
3 | A library for static, dynamic, uncertain, and robust optimization of power networks, implementing the models from our [paper](https://web.stanford.edu/~boyd/papers/dyn_ener_man.html)
4 |
5 | ```
6 | @incollection{MBBW:18,
7 | author={N. Moehle and E. Busseti and S. Boyd and M. Wytock},
8 | title={Dynamic Energy Management},
9 | booktitle={Large Scale Optimization in Supply Chains and Smart Manufacturing},
10 | pages={69--126},
11 | year={2019},
12 | publisher={Springer}
13 | }
14 | ```
15 |
16 |
17 | See the [examples](https://github.com/cvxgrp/cvxpower/tree/master/examples) for basic usage, or the (outdated) [documentation](http://energy-management.readthedocs.io/).
18 |
--------------------------------------------------------------------------------
/cvxpower/__init__.py:
--------------------------------------------------------------------------------
1 | """
2 | Copyright (C) Matt Wytock, Enzo Busseti, Nicholas Moehle, 2016-2019.
3 |
4 | This program is free software: you can redistribute it and/or modify
5 | it under the terms of the GNU General Public License as published by
6 | the Free Software Foundation, either version 3 of the License, or
7 | (at your option) any later version.
8 | This program is distributed in the hope that it will be useful,
9 | but WITHOUT ANY WARRANTY; without even the implied warranty of
10 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 | GNU General Public License for more details.
12 | You should have received a copy of the GNU General Public License
13 | along with this program. If not, see .
14 |
15 | Code written by Nicholas Moehle before January 2019 is licensed
16 | under the Apache License, Version 2.0 (the "License");
17 | you may not use this file except in compliance with the License.
18 | You may obtain a copy of the License at
19 |
20 | http://www.apache.org/licenses/LICENSE-2.0
21 |
22 | Unless required by applicable law or agreed to in writing, software
23 | distributed under the License is distributed on an "AS IS" BASIS,
24 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
25 | See the License for the specific language governing permissions and
26 | limitations under the License.
27 | """
28 |
29 | from .network import *
30 | from .devices import *
31 | from cvxpy import Parameter
32 |
--------------------------------------------------------------------------------
/cvxpower/devices.py:
--------------------------------------------------------------------------------
1 | """
2 | Copyright (C) Matt Wytock, Enzo Busseti, Nicholas Moehle, 2016-2019.
3 |
4 | This program is free software: you can redistribute it and/or modify
5 | it under the terms of the GNU General Public License as published by
6 | the Free Software Foundation, either version 3 of the License, or
7 | (at your option) any later version.
8 | This program is distributed in the hope that it will be useful,
9 | but WITHOUT ANY WARRANTY; without even the implied warranty of
10 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 | GNU General Public License for more details.
12 | You should have received a copy of the GNU General Public License
13 | along with this program. If not, see .
14 |
15 | Code written by Nicholas Moehle before January 2019 is licensed
16 | under the Apache License, Version 2.0 (the "License");
17 | you may not use this file except in compliance with the License.
18 | You may obtain a copy of the License at
19 |
20 | http://www.apache.org/licenses/LICENSE-2.0
21 |
22 | Unless required by applicable law or agreed to in writing, software
23 | distributed under the License is distributed on an "AS IS" BASIS,
24 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
25 | See the License for the specific language governing permissions and
26 | limitations under the License.
27 | """
28 |
29 |
30 | __doc__ = r"""Device models for dynamic energy management.
31 |
32 | Each device has one or more terminals each of which has associated with it a
33 | vector representing power consumption or generation. Formally, we denote this
34 | power schedule as :math:`p = (p(1), \ldots, p(T)) \in \mathbb{R}^T` and devices
35 | specify the objective function and constraints imposed by each device over these
36 | power flows.
37 | """
38 |
39 | import cvxpy as cvx
40 | import numpy as np
41 |
42 | from .network import Device, Terminal
43 |
44 |
45 | class Generator(Device):
46 | r"""Generator with quadratic cost and optional ramp constraints.
47 |
48 | A generator has range and ramp rate constraints defining the operating
49 | region, for :math:`\tau = 1,\ldots,T`
50 |
51 | .. math::
52 |
53 | P^{\min} \le -p(\tau) \le P^{\max} \\
54 | R^{\min} \le -(p(\tau) - p(\tau - 1)) \le R^{\max}
55 |
56 | ..
57 |
58 | where :math:`p(0)` is defined to be :math:`p^\mathrm{init}` is the initial
59 | power produced by the generator. If :math:`R^{\min}` or :math:`R^{\max}` are
60 | unspecified, the minimum or maximum ramp rate is unconstrained, respectively.
61 |
62 | The cost function is separable across time periods
63 |
64 | .. math::
65 |
66 | \sum_{\tau=1}^T \phi(-p(\tau))
67 |
68 | ..
69 |
70 | and quadratic, :math:`\phi(x) = \alpha x^2 + \beta x`, parameterized by
71 | :math:`\alpha, \beta \in \mathbb{R}`.
72 |
73 | :param power_min: Minimum power generation, :math:`P^\min`
74 | :param power_max: Maximum power generation, :math:`P^\max`
75 | :param alpha: Quadratic term in cost function, :math:`\alpha`
76 | :param beta: Linear term in cost function, :math:`\beta`
77 | :param ramp_min: (optional) Minimum ramp rate, :math:`R^\min`
78 | :param ramp_max: (optional) Maximum ramp rate, :math:`R^\max`
79 | :param power_init: (optional) Initial power generation for ramp constraints, :math:`p^\mathrm{init}`
80 | :param name: (optional) Display name for generator
81 | :type power_min: float or sequence of float
82 | :type power_max: float or sequence of float
83 | :type alpha: float or sequence of float
84 | :type beta: float or sequence of float
85 | :type ramp_min: float or sequence of float
86 | :type ramp_max: float or sequence of float
87 | :type power_init: float
88 | :type name: string
89 | """
90 |
91 | def __init__(
92 | self,
93 | power_min=None,
94 | power_max=None,
95 | ramp_min=None,
96 | ramp_max=None,
97 | power_init=0,
98 | alpha=0,
99 | beta=0,
100 | gamma=0,
101 | name=None,
102 | len_interval=1,
103 | ):
104 | super(Generator, self).__init__([Terminal()], name)
105 | self.power_min = power_min
106 | self.power_max = power_max
107 | self.ramp_min = ramp_min
108 | self.ramp_max = ramp_max
109 | self.power_init = power_init
110 | self.alpha = alpha
111 | self.beta = beta
112 | self.gamma = gamma
113 | self.len_interval = len_interval # in hours
114 |
115 | @property
116 | def cost(self):
117 | p = self.terminals[0].power_var
118 | return self.alpha * cvx.square(p) - self.beta * p + self.gamma
119 |
120 | @property
121 | def constraints(self):
122 | p = -self.terminals[0].power_var
123 |
124 | constraints = []
125 | if self.power_min is not None:
126 | constraints += [p >= self.power_min]
127 | if self.power_max is not None:
128 | constraints += [p <= self.power_max]
129 |
130 | # TODO(mwytock): Add ramp constraints
131 |
132 | return constraints
133 |
134 |
135 | class FixedLoad(Device):
136 | """Fixed load.
137 |
138 | A fixed load has a specified profile :math:`l \in \mathbb{R}^T`,
139 | enforced with constraint
140 |
141 | .. math::
142 | p = l.
143 | ..
144 |
145 | :param power: Load profile, :math:`l`
146 | :param name: (optional) Display name for load
147 | :type power: float or sequence of float
148 | :type name: string
149 | """
150 |
151 | def __init__(self, power=None, name=None):
152 | super(FixedLoad, self).__init__([Terminal()], name)
153 | self.power = power
154 |
155 | @property
156 | def constraints(self):
157 | if self.terminals[0].power_var.shape[1] == 1:
158 | p = (
159 | self.power[:, 0]
160 | if (not np.isscalar(self.power) and self.power.ndim == 2)
161 | else self.power
162 | )
163 | return [self.terminals[0].power_var[:, 0] == p]
164 | else:
165 | return [self.terminals[0].power_var == self.power]
166 |
167 |
168 | class ThermalLoad(Device):
169 | r"""Thermal load.
170 |
171 | A thermal load consists of a heat store (e.g. building, refrigerator), with
172 | a temperature profile :math:`\theta \in \mathbb{R}^T` which must be
173 | maintained within :math:`[\theta^\min,\theta^\max]`. The temperature evolves
174 | as
175 |
176 | .. math::
177 | \theta(\tau + 1) = \theta(\tau) + (\mu/c)(\theta^\mathrm{amb}(\tau) -
178 | \theta(\tau)) - (\eta/c)p(\tau)
179 | ..
180 |
181 | for :math:`\tau = 1,\ldots,T-1` and :math:`\theta(1) =
182 | \theta^{\mathrm{init}}`, where :math:`\mu` is the ambient conduction
183 | coefficient, :math:`\eta` is the heating/cooling efficiency, :math:`c` is
184 | the heat capacity of the heat store, :math:`\theta \in \mathbb{R}^T` is the
185 | ambient temperature profile and :math:`\theta^{\mathrm{init}}` is the
186 | initial temperature of the heat store.
187 |
188 | The power consumption of is constrained by
189 |
190 | .. math::
191 | 0 \le p \le P^\max.
192 | ..
193 |
194 | :param temp_init: Initial tempeature, :math:`\theta^\mathrm{init}`
195 | :param temp_min: Minimum temperature, :math:`\theta^\min`
196 | :param temp_max: Maximum temperature, :math:`\theta^\max`
197 | :param temp_amb: Ambient temperature, :math:`\theta^\mathrm{amb}`
198 | :param power_max: Maximum power consumption, :math:`P^\max`
199 | :param amb_conduct_coeff: Ambient conduction coefficient, :math:`\mu`
200 | :param efficiency: Heating/cooling efficiency, :math:`\eta`
201 | :param capacity: Heat capacity of the heat store, :math:`c`
202 | :type temp_init: float
203 | :type temp_min: float or sequence of floats
204 | :type temp_max: float or sequence of floats
205 | :type temp_amb: float or sequence of floats
206 | :type power_max: float or sequence of floats
207 | :type amb_conduct_coeff: float or sequence of floats
208 | :type efficiency: float or sequence of floats
209 | :type capacity: float or sequence of floats
210 | """
211 |
212 | def __init__(
213 | self,
214 | temp_init=None,
215 | temp_min=None,
216 | temp_max=None,
217 | temp_amb=None,
218 | power_max=None,
219 | amb_conduct_coeff=None,
220 | efficiency=None,
221 | capacity=None,
222 | name=None,
223 | ):
224 | super(ThermalLoad, self).__init__([Terminal()], name)
225 | self.temp_init = temp_init
226 | self.temp_min = temp_min
227 | self.temp_max = temp_max
228 | self.temp_amb = temp_amb
229 | self.power_max = power_max
230 | self.amb_conduct_coeff = amb_conduct_coeff
231 | self.efficiency = efficiency
232 | self.capacity = capacity
233 |
234 | @property
235 | def constraints(self):
236 | alpha = self.amb_conduct_coeff / self.capacity
237 | beta = self.efficiency / self.capacity
238 | N = self.terminals[0].power_var.shape[0]
239 | self.temp = cvx.Variable(shape=(N, 1))
240 |
241 | constrs = [
242 | self.terminals[0].power_var <= self.power_max,
243 | self.terminals[0].power_var >= 0,
244 | ]
245 |
246 | if self.temp_max is not None:
247 | constrs += [self.temp <= self.temp_max]
248 | if self.temp_min is not None:
249 | constrs += [self.temp >= self.temp_min]
250 |
251 | for i in range(N):
252 | temp_prev = self.temp[i - 1] if i else self.temp_init
253 | constrs += [
254 | self.temp[i]
255 | == (
256 | temp_prev
257 | + alpha * (self.temp_amb[i] - temp_prev)
258 | - beta * self.terminals[0].power_var[i]
259 | )
260 | ]
261 |
262 | return constrs
263 |
264 |
265 | class CurtailableLoad(Device):
266 | r"""Curtailable load.
267 |
268 | A curtailable load penalizes the shortfall between a desired load profile
269 | :math:`l \in \mathbb{R}^T` and delivered power with the linear penalty
270 |
271 | .. math::
272 |
273 | \alpha \sum_{\tau=1}^T (l(\tau) - p(\tau))_+
274 |
275 | ..
276 |
277 | where :math:`(z)_+ = \max(0, z)` and :math:`\alpha > 0` is curtailment
278 | penalty. Conceptually, the linear curtailment penalty represents the price
279 | paid for reducing load.
280 |
281 | :param power: Desired load profile, :math:`l`
282 | :param alpha: Linear curtailment penalty, :math:`\alpha`
283 | :param name: (optional) Display name for load
284 | :type power: float or sequence of float
285 | :type alpha: float or sequence of float
286 | :type name: string
287 | """
288 |
289 | def __init__(self, power=None, alpha=None, name=None):
290 | super(CurtailableLoad, self).__init__([Terminal()], name)
291 | self.power = power
292 | self.alpha = alpha
293 |
294 | @property
295 | def cost(self):
296 | return self.alpha * cvx.pos(self.power - self.terminals[0].power_var)
297 |
298 |
299 | class DeferrableLoad(Device):
300 | r"""Deferrable load.
301 |
302 | A deferrable load must consume a certain amount of energy :math:`E` over a
303 | flexible time horizon :math:`\tau = A,\ldots,D`. This is characterized by
304 | the constraint
305 |
306 | .. math::
307 | \sum_{\tau=A}^{D} p(\tau) \ge E
308 | ..
309 |
310 | In addition, the power consumption in any interval is bounded by
311 |
312 | .. math::
313 | p \le P^\max
314 | ..
315 |
316 | :param energy: Desired energy consumption, :math:`E`
317 | :param power_max: Maximum power consumption, :math:`P^\max`
318 | :param time_start: Starting interval, inclusive, :math:`A`
319 | :param time_end: Ending interval, inclusive, :math:`D`
320 | :param name: (optional) Display name for load
321 | :type energy: float
322 | :type power_max: float or sequence of float
323 | :type time_start: int
324 | :type time_end: int
325 | :type name: string
326 | """
327 |
328 | def __init__(
329 | self,
330 | energy=0,
331 | power_max=None,
332 | time_start=0,
333 | time_end=None,
334 | name=None,
335 | len_interval=1.0,
336 | ):
337 | super(DeferrableLoad, self).__init__([Terminal()], name)
338 | self.time_start = time_start
339 | self.time_end = time_end
340 | self.energy = energy
341 | self.power_max = power_max
342 | self.len_interval = len_interval
343 |
344 | @property
345 | def constraints(self):
346 | idx = slice(self.time_start, self.time_end)
347 | return [
348 | cvx.sum(self.terminals[0].power_var[idx]) * self.len_interval
349 | == self.energy,
350 | self.terminals[0].power_var >= 0,
351 | self.terminals[0].power_var <= self.power_max,
352 | ]
353 |
354 |
355 | class TransmissionLine(Device):
356 | """Transmission line.
357 |
358 | A lossless transmission line has two terminals with power schedules
359 | :math:`p_1` and :math:`p_2`. Conservation of energy across the
360 | line is enforced with the constraint
361 |
362 | .. math::
363 | p_1 + p_2 = 0,
364 | ..
365 |
366 | and a maximum capacity of :math:`P^\max` with
367 |
368 | .. math::
369 | |p_1| \le P^\max.
370 | ..
371 |
372 | :param power_max: Maximum capacity of the transmission line
373 | :param name: (optional) Display name for transmission line
374 | :type power_max: float or sequence of floats
375 | :type name: string
376 | """
377 |
378 | def __init__(self, alpha=0.0, power_max=None, name=None):
379 | super(TransmissionLine, self).__init__([Terminal(), Terminal()], name)
380 | self.power_max = power_max
381 | self.alpha = alpha
382 | assert self.alpha >= 0
383 |
384 | @property
385 | def constraints(self):
386 | p1 = self.terminals[0].power_var
387 | p2 = self.terminals[1].power_var
388 |
389 | constrs = []
390 | if self.alpha > 0:
391 | constrs += [p1 + p2 >= self.alpha * cvx.square((p1 - p2) / 2)]
392 | if self.power_max is not None:
393 | constrs += [2 * self.alpha * self.power_max ** 2 >= p1 + p2]
394 | else:
395 | constrs += [p1 + p2 == 0]
396 | if self.power_max is not None:
397 | constrs += [cvx.abs((p1 - p2) / 2) <= self.power_max]
398 |
399 | return constrs
400 |
401 |
402 | class Storage(Device):
403 | r"""Storage device.
404 |
405 | A storage device either takes or delivers power with charging and
406 | discharging rates specified by the constraints
407 |
408 | .. math::
409 | -D^\max \le p \le C^\max
410 | ..
411 |
412 | where :math:`C^\max` and :math:`D^\max` are the maximum charging and
413 | discharging rates. The charge level of the battery is given by
414 |
415 | .. math::
416 | q(\tau) = q^\mathrm{init} + \sum_{t=1}^\tau p(t), \quad \tau = 1, \ldots, T,
417 | ..
418 |
419 | which is constrained according to the physical limits of the battery
420 |
421 | .. math::
422 | 0 \le q \le Q^\max.
423 | ..
424 |
425 | :param discharge_max: Maximum discharge rate, :math:`D^\max`
426 | :param charge_max: Maximum charge rate, :math:`C^\max`
427 | :param energy_init: Initial charge, :math:`q^\mathrm{init}`
428 | :param energy_max: Maximum battery capacity, :math:`Q^\max`
429 | :param name: (optional) Display name of storage device
430 | :type discharge_max: float or sequence of floats
431 | :type charge_max: float or sequence of floats
432 | :type energy_init: float
433 | :type energy_max: float or sequence of floats
434 | :type name: string
435 | """
436 |
437 | def __init__(
438 | self,
439 | discharge_max=0,
440 | charge_max=None,
441 | energy_init=0,
442 | energy_final=None,
443 | energy_max=None,
444 | name=None,
445 | len_interval=1.0,
446 | final_energy_price=None,
447 | ):
448 | super(Storage, self).__init__([Terminal()], name)
449 | self.discharge_max = discharge_max
450 | self.charge_max = charge_max
451 | self.energy_init = energy_init
452 | self.energy_max = energy_max
453 | self.energy_final = energy_final
454 | self.len_interval = len_interval # in hours
455 | self.final_energy_price = final_energy_price
456 | self.energy = None
457 |
458 | @property
459 | def cost(self):
460 | T, S = self.terminals[0].power_var.shape
461 | if self.final_energy_price is not None:
462 | if self.energy is None:
463 | self.energy = cvx.Variable(self.terminals[0].power_var.shape)
464 | cost = np.zeros((T - 1, S))
465 | final_cost = cvx.reshape(
466 | self.energy[-1, :] * self.final_energy_price[0, 0], (1, S)
467 | )
468 | cost = cvx.vstack([cost, final_cost])
469 | else:
470 | cost = np.zeros(T, S)
471 | return cost
472 |
473 | @property
474 | def constraints(self):
475 | P = self.terminals[0].power_var
476 | if self.energy is None:
477 | self.energy = cvx.Variable(self.terminals[0].power_var.shape)
478 | e_init = cvx.reshape(self.energy_init, ())
479 | constr = [
480 | cvx.diff(self.energy) == P[1:, :] * self.len_interval,
481 | self.energy[0, :] - e_init - P[0, :] * self.len_interval == 0,
482 | self.terminals[0].power_var >= -self.discharge_max,
483 | self.terminals[0].power_var <= self.charge_max,
484 | self.energy <= self.energy_max,
485 | self.energy >= 0,
486 | ]
487 | if self.energy_final is not None:
488 | constr += [self.energy[-1] >= self.energy_final]
489 | return constr
490 |
--------------------------------------------------------------------------------
/cvxpower/devices_test.py:
--------------------------------------------------------------------------------
1 | """
2 | Copyright (C) Matt Wytock, Enzo Busseti, Nicholas Moehle, 2016-2019.
3 |
4 | This program is free software: you can redistribute it and/or modify
5 | it under the terms of the GNU General Public License as published by
6 | the Free Software Foundation, either version 3 of the License, or
7 | (at your option) any later version.
8 | This program is distributed in the hope that it will be useful,
9 | but WITHOUT ANY WARRANTY; without even the implied warranty of
10 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 | GNU General Public License for more details.
12 | You should have received a copy of the GNU General Public License
13 | along with this program. If not, see .
14 |
15 | Code written by Nicholas Moehle before January 2019 is licensed
16 | under the Apache License, Version 2.0 (the "License");
17 | you may not use this file except in compliance with the License.
18 | You may obtain a copy of the License at
19 |
20 | http://www.apache.org/licenses/LICENSE-2.0
21 |
22 | Unless required by applicable law or agreed to in writing, software
23 | distributed under the License is distributed on an "AS IS" BASIS,
24 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
25 | See the License for the specific language governing permissions and
26 | limitations under the License.
27 | """
28 |
29 | import cvxpy as cvx
30 | import numpy as np
31 | import unittest
32 |
33 | from cvxpower import *
34 |
35 |
36 | class StaticTest(unittest.TestCase):
37 | def test_hello_world(self):
38 | load = FixedLoad(power=100)
39 | gen = Generator(power_max=1000, alpha=0.1, beta=100)
40 | net = Net([load.terminals[0], gen.terminals[0]])
41 | network = Group([load, gen], [net])
42 | network.optimize(solver="ECOS")
43 | np.testing.assert_allclose(load.terminals[0].power, 100)
44 | np.testing.assert_allclose(gen.terminals[0].power, -100)
45 | np.testing.assert_allclose(net.price, 120, rtol=1e-2)
46 | np.testing.assert_allclose(load.terminals[0].payment, 12000, rtol=1e-2)
47 | np.testing.assert_allclose(gen.terminals[0].payment, -12000, rtol=1e-2)
48 |
49 | def test_curtailable_load(self):
50 | load = CurtailableLoad(power=1000, alpha=150)
51 | gen = Generator(power_max=1000, alpha=1, beta=100)
52 | net = Net([load.terminals[0], gen.terminals[0]])
53 | network = Group([load, gen], [net])
54 | network.optimize(solver="ECOS")
55 |
56 | np.testing.assert_allclose(load.terminals[0].power, 25.00, rtol=1e-2)
57 | np.testing.assert_allclose(gen.terminals[0].power, -25, rtol=1e-2)
58 | np.testing.assert_allclose(net.price, 150, rtol=1e-2)
59 |
60 | def test_two_generators_with_transmission(self):
61 | load = FixedLoad(power=100)
62 | gen1 = Generator(power_max=1000, alpha=0.01, beta=100, name="Gen1")
63 | gen2 = Generator(power_max=100, alpha=0.1, beta=0.1, name="Gen2")
64 | line = TransmissionLine(power_max=50)
65 |
66 | net1 = Net([load.terminals[0], gen1.terminals[0], line.terminals[0]])
67 | net2 = Net([gen2.terminals[0], line.terminals[1]])
68 |
69 | network = Group([load, gen1, gen2, line], [net1, net2])
70 | network.optimize(solver="ECOS")
71 |
72 | np.testing.assert_allclose(load.terminals[0].power, 100, rtol=1e-2)
73 | np.testing.assert_allclose(gen1.terminals[0].power, -50, rtol=1e-2)
74 | np.testing.assert_allclose(gen2.terminals[0].power, -50, rtol=1e-2)
75 | np.testing.assert_allclose(line.terminals[0].power, -50, rtol=1e-2)
76 | np.testing.assert_allclose(line.terminals[1].power, 50, rtol=1e-2)
77 |
78 | np.testing.assert_allclose(net1.price, 101, rtol=1e-2)
79 | np.testing.assert_allclose(net2.price, 10.1, rtol=1e-2)
80 |
81 | def test_three_buses(self):
82 | load1 = FixedLoad(power=50, name="Load1")
83 | load2 = FixedLoad(power=100, name="Load2")
84 | gen1 = Generator(power_max=1000, alpha=0.01, beta=100, name="Gen1")
85 | gen2 = Generator(power_max=100, alpha=0.1, beta=0.1, name="Gen2")
86 | line1 = TransmissionLine(power_max=50)
87 | line2 = TransmissionLine(power_max=10)
88 | line3 = TransmissionLine(power_max=55)
89 |
90 | net1 = Net(
91 | [
92 | load1.terminals[0],
93 | gen1.terminals[0],
94 | line1.terminals[0],
95 | line2.terminals[0],
96 | ]
97 | )
98 | net2 = Net([load2.terminals[0], line1.terminals[1], line3.terminals[0]])
99 | net3 = Net([gen2.terminals[0], line2.terminals[1], line3.terminals[1]])
100 |
101 | network = Group(
102 | [load1, load2, gen1, gen2, line1, line2, line3], [net1, net2, net3]
103 | )
104 | network.optimize(solver="ECOS")
105 |
106 | np.testing.assert_allclose(load1.terminals[0].power, 50, rtol=1e-2)
107 | np.testing.assert_allclose(load2.terminals[0].power, 100, rtol=1e-2)
108 | np.testing.assert_allclose(gen1.terminals[0].power, -85, rtol=1e-2)
109 | np.testing.assert_allclose(gen2.terminals[0].power, -65, rtol=1e-2)
110 | np.testing.assert_allclose(line1.terminals[0].power, 45, rtol=1e-2)
111 | np.testing.assert_allclose(line1.terminals[1].power, -45, rtol=1e-2)
112 | np.testing.assert_allclose(line2.terminals[0].power, -10, rtol=1e-2)
113 | np.testing.assert_allclose(line2.terminals[1].power, 10, rtol=1e-2)
114 | np.testing.assert_allclose(line3.terminals[0].power, -55, rtol=1e-2)
115 | np.testing.assert_allclose(line3.terminals[1].power, 55, rtol=1e-2)
116 |
117 | np.testing.assert_allclose(net1.price, 101.7, rtol=1e-2)
118 | np.testing.assert_allclose(net2.price, 101.7, rtol=1e-2)
119 | np.testing.assert_allclose(net3.price, 13.1, rtol=1e-2)
120 |
121 | def test_group(self):
122 | solar = Generator(power_max=10, alpha=0, beta=0, name="Solar")
123 | load = FixedLoad(power=13)
124 | line = TransmissionLine(power_max=25)
125 | net = Net([load.terminals[0], solar.terminals[0], line.terminals[0]])
126 | home = Group([solar, load, line], [net], [line.terminals[1]], name="Home")
127 |
128 | grid = Generator(power_max=1e6, alpha=0.05, beta=100, name="Grid")
129 | meter = Net([line.terminals[1], grid.terminals[0]], name="Meter")
130 |
131 | network = Group([home, grid], [meter])
132 | network.init_problem()
133 | network.problem.solve(solver="ECOS")
134 |
135 | np.testing.assert_allclose(home.terminals[0].power, 3)
136 | np.testing.assert_allclose(grid.terminals[0].power, -3)
137 |
138 | np.testing.assert_allclose(net.price, 100.3, rtol=1e-2)
139 |
140 | def test_vary_parameters(self):
141 | load1 = FixedLoad(power=50, name="Load1")
142 | load2 = FixedLoad(power=100, name="Load2")
143 | gen1 = Generator(power_max=100, alpha=1, beta=10, name="Gen1")
144 | gen2 = Generator(power_max=1000, alpha=0.01, beta=0, name="Gen2")
145 | line1 = TransmissionLine(power_max=100)
146 | line2 = TransmissionLine(power_max=10)
147 | line3 = TransmissionLine(power_max=Parameter(1))
148 |
149 | net1 = Net(
150 | [
151 | load1.terminals[0],
152 | gen1.terminals[0],
153 | line1.terminals[0],
154 | line2.terminals[0],
155 | ]
156 | )
157 | net2 = Net([load2.terminals[0], line1.terminals[1], line3.terminals[0]])
158 | net3 = Net([gen2.terminals[0], line2.terminals[1], line3.terminals[1]])
159 | network = Group(
160 | [load1, load2, gen1, gen2, line1, line2, line3], [net1, net2, net3]
161 | )
162 | network.init_problem()
163 | prob = network.problem
164 |
165 | line3.power_max.value = [50]
166 | prob.solve(solver="ECOS")
167 | np.testing.assert_allclose(load1.terminals[0].power, 50, rtol=1e-2)
168 | np.testing.assert_allclose(load2.terminals[0].power, 100, rtol=1e-2)
169 | np.testing.assert_allclose(gen1.terminals[0].power, -90, rtol=1e-2)
170 | np.testing.assert_allclose(gen2.terminals[0].power, -60, rtol=1e-2)
171 | np.testing.assert_allclose(line1.terminals[0].power, 50, rtol=1e-2)
172 | np.testing.assert_allclose(line1.terminals[1].power, -50, rtol=1e-2)
173 | np.testing.assert_allclose(line2.terminals[0].power, -10, rtol=1e-2)
174 | np.testing.assert_allclose(line2.terminals[1].power, 10, rtol=1e-2)
175 | np.testing.assert_allclose(line3.terminals[0].power, -50, rtol=1e-2)
176 | np.testing.assert_allclose(line3.terminals[1].power, 50, rtol=1e-2)
177 |
178 | np.testing.assert_allclose(net1.price, 190.0136, rtol=1e-2)
179 | np.testing.assert_allclose(net2.price, 190.0136, rtol=1e-2)
180 | np.testing.assert_allclose(net3.price, 1.2000, rtol=1e-2)
181 |
182 | line3.power_max.value = [100]
183 | prob.solve(solver="ECOS")
184 | np.testing.assert_allclose(load1.terminals[0].power, 50, rtol=1e-2)
185 | np.testing.assert_allclose(load2.terminals[0].power, 100, rtol=1e-2)
186 | np.testing.assert_allclose(gen1.terminals[0].power, -40, rtol=1e-2)
187 | np.testing.assert_allclose(gen2.terminals[0].power, -110, rtol=1e-2)
188 | np.testing.assert_allclose(line1.terminals[0].power, 0, atol=1e-4)
189 | np.testing.assert_allclose(line1.terminals[1].power, 0, atol=1e-4)
190 | np.testing.assert_allclose(line2.terminals[0].power, -10, rtol=1e-2)
191 | np.testing.assert_allclose(line2.terminals[1].power, 10, rtol=1e-2)
192 | np.testing.assert_allclose(line3.terminals[0].power, -100, rtol=1e-2)
193 | np.testing.assert_allclose(line3.terminals[1].power, 100, rtol=1e-2)
194 |
195 | np.testing.assert_allclose(net1.price, 89.9965, rtol=1e-2)
196 | np.testing.assert_allclose(net2.price, 89.9965, rtol=1e-2)
197 | np.testing.assert_allclose(net3.price, 2.2009, rtol=1e-2)
198 |
199 |
200 | T = 10
201 | p_load = (np.sin(np.pi * np.arange(T) / T) + 1e-2).reshape(-1, 1)
202 |
203 |
204 | class DynamicTest(unittest.TestCase):
205 | def test_dynamic_load(self):
206 | load = FixedLoad(power=p_load)
207 | gen = Generator(power_max=2, power_min=-0.01, alpha=100, beta=100)
208 | net = Net([load.terminals[0], gen.terminals[0]])
209 | network = Group([load, gen], [net])
210 |
211 | network.init_problem(time_horizon=T)
212 | network.problem.solve(solver="ECOS")
213 | np.testing.assert_allclose(load.terminals[0].power, p_load, atol=1e-4)
214 | np.testing.assert_allclose(gen.terminals[0].power, -p_load, atol=1e-4)
215 | np.testing.assert_allclose(net.price, p_load * 200 + 100, rtol=1e-2)
216 |
217 | def test_storage(self):
218 | load = FixedLoad(power=p_load)
219 | gen = Generator(power_max=2, alpha=100, beta=100)
220 | storage = Storage(charge_max=0.1, discharge_max=0.1, energy_max=0.5)
221 |
222 | net = Net([load.terminals[0], gen.terminals[0], storage.terminals[0]])
223 | network = Group([load, gen, storage], [net])
224 | network.init_problem(time_horizon=T)
225 | network.problem.solve(solver="ECOS")
226 |
227 | def test_deferrable_load(self):
228 | load = FixedLoad(power=p_load)
229 | gen = Generator(power_max=2, alpha=100, beta=100)
230 | deferrable = DeferrableLoad(time_start=5, energy=0.5, power_max=0.1)
231 |
232 | net = Net([load.terminals[0], gen.terminals[0], deferrable.terminals[0]])
233 | network = Group([load, gen, deferrable], [net])
234 | network.init_problem(time_horizon=T)
235 | network.problem.solve(solver="ECOS")
236 |
237 | def test_thermal_load(self):
238 | temp_amb = (np.sin(np.pi * np.arange(T) / T) + 1e-2).reshape(
239 | -1, 1
240 | ) ** 2 * 50 + 50
241 |
242 | load = FixedLoad(power=p_load)
243 | gen = Generator(power_max=2, alpha=100, beta=100)
244 | thermal = ThermalLoad(
245 | temp_init=60,
246 | temp_amb=temp_amb,
247 | temp_max=90,
248 | power_max=0.1,
249 | amb_conduct_coeff=0.1,
250 | efficiency=0.95,
251 | capacity=1,
252 | )
253 |
254 | net = Net([load.terminals[0], gen.terminals[0], thermal.terminals[0]])
255 | network = Group([load, gen, thermal], [net])
256 | network.init_problem(time_horizon=T)
257 | network.problem.solve(solver="ECOS")
258 |
259 |
260 | T = 10
261 | K = 2
262 | p_load0 = (np.sin(np.pi * np.arange(T) / T) + 1e-2).reshape(-1, 1)
263 | p_load1 = np.abs((np.cos(np.pi * np.arange(T) / T) + 1e-2)).reshape(-1, 1)
264 | p_load_robust = np.hstack([p_load0, p_load1])
265 | p_load_robust[0, 1] = p_load_robust[0, 0]
266 |
267 |
268 | class ScenarioTest(unittest.TestCase):
269 | def test_dynamic_load(self):
270 | load = FixedLoad(power=p_load_robust)
271 | gen = Generator(power_max=2, power_min=-0.01, alpha=100, beta=100)
272 | net = Net([load.terminals[0], gen.terminals[0]])
273 | network = Group([load, gen], [net])
274 |
275 | network.init_problem(time_horizon=T, num_scenarios=K)
276 | network.problem.solve(solver="ECOS")
277 | np.testing.assert_allclose(load.terminals[0].power, p_load_robust, atol=1e-4)
278 | np.testing.assert_allclose(gen.terminals[0].power, -p_load_robust, atol=1e-4)
279 | np.testing.assert_allclose(
280 | net.price[:, 0], p_load_robust[:, 0] * 200 + 100, rtol=1e-2
281 | )
282 |
283 | def test_storage(self):
284 | load = FixedLoad(power=p_load_robust)
285 | gen = Generator(power_max=2, alpha=100, beta=100)
286 | storage = Storage(charge_max=0.1, discharge_max=0.1, energy_max=0.5)
287 |
288 | net = Net([load.terminals[0], gen.terminals[0], storage.terminals[0]])
289 | network = Group([load, gen, storage], [net])
290 | network.init_problem(time_horizon=T, num_scenarios=K)
291 | network.problem.solve(solver="ECOS")
292 |
293 | def test_deferrable_load(self):
294 | load = FixedLoad(power=p_load_robust)
295 | gen = Generator(power_max=2, alpha=100, beta=100)
296 | deferrable = DeferrableLoad(time_start=5, energy=0.5, power_max=0.1)
297 |
298 | net = Net([load.terminals[0], gen.terminals[0], deferrable.terminals[0]])
299 | network = Group([load, gen, deferrable], [net])
300 | network.init_problem(time_horizon=T, num_scenarios=K)
301 | network.problem.solve(solver="ECOS")
302 |
303 | def test_thermal_load(self):
304 | temp_amb = (np.sin(np.pi * np.arange(T) / T) + 1e-2).reshape(
305 | -1, 1
306 | ) ** 2 * 50 + 50
307 |
308 | load = FixedLoad(power=p_load_robust)
309 | gen = Generator(power_max=2, alpha=100, beta=100)
310 | thermal = ThermalLoad(
311 | temp_init=60,
312 | temp_amb=temp_amb,
313 | temp_max=90,
314 | power_max=0.1,
315 | amb_conduct_coeff=0.1,
316 | efficiency=0.95,
317 | capacity=1,
318 | )
319 |
320 | net = Net([load.terminals[0], gen.terminals[0], thermal.terminals[0]])
321 | network = Group([load, gen, thermal], [net])
322 | network.init_problem(time_horizon=T, num_scenarios=K)
323 | network.problem.solve(solver="ECOS")
324 |
325 |
326 | #
327 | # TODO(mwytock): MPC test cases
328 | #
329 |
330 | if __name__ == "__main__":
331 | unittest.main()
332 |
--------------------------------------------------------------------------------
/cvxpower/network.py:
--------------------------------------------------------------------------------
1 | """
2 | Copyright (C) Matt Wytock, Enzo Busseti, Nicholas Moehle, 2016-2019.
3 |
4 | This program is free software: you can redistribute it and/or modify
5 | it under the terms of the GNU General Public License as published by
6 | the Free Software Foundation, either version 3 of the License, or
7 | (at your option) any later version.
8 | This program is distributed in the hope that it will be useful,
9 | but WITHOUT ANY WARRANTY; without even the implied warranty of
10 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 | GNU General Public License for more details.
12 | You should have received a copy of the GNU General Public License
13 | along with this program. If not, see .
14 |
15 | Code written by Nicholas Moehle before January 2019 is licensed
16 | under the Apache License, Version 2.0 (the "License");
17 | you may not use this file except in compliance with the License.
18 | You may obtain a copy of the License at
19 |
20 | http://www.apache.org/licenses/LICENSE-2.0
21 |
22 | Unless required by applicable law or agreed to in writing, software
23 | distributed under the License is distributed on an "AS IS" BASIS,
24 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
25 | See the License for the specific language governing permissions and
26 | limitations under the License.
27 | """
28 |
29 |
30 | __doc__ = """Network of power devices."""
31 |
32 | import cvxpy as cvx
33 | import numpy as np
34 | import tqdm
35 |
36 |
37 | def _get_all_terminals(device):
38 | """Gets all terminals, including those nested within child devices."""
39 | terms = device.terminals
40 | if hasattr(device, "internal_terminal"):
41 | terms += [device.internal_terminal]
42 | if hasattr(device, "devices"):
43 | terms += [t for d in device.devices for t in _get_all_terminals(d)]
44 | return terms
45 |
46 |
47 | class Terminal(object):
48 | """Device terminal."""
49 |
50 | @property
51 | def power_var(self):
52 | return self._power
53 |
54 | @property
55 | def power(self):
56 | """Power consumed (positive value) or produced (negative value) at this
57 | terminal."""
58 | return self._power.value
59 |
60 | def _init_problem(self, time_horizon, num_scenarios):
61 | self._power = cvx.Variable(shape=(time_horizon, num_scenarios))
62 |
63 | def _set_payments(self, price):
64 | if price is not None and self._power.value is not None:
65 | self.payment = price * self._power.value
66 | else:
67 | self.payment = None
68 |
69 |
70 | class Net(object):
71 | r"""Connection point for device terminals.
72 |
73 | A net defines the power balance constraint ensuring that power sums to zero
74 | across all connected terminals at each time point.
75 |
76 | :param terminals: The terminals connected to this net
77 | :param name: (optional) Display name of net
78 | :type terminals: list of :class:`Terminal`
79 | :type name: string
80 | """
81 |
82 | def __init__(self, terminals, name=None):
83 | self.name = "Net" if name is None else name
84 | self.terminals = terminals
85 |
86 | def _init_problem(self, time_horizon, num_scenarios):
87 | self.num_scenarios = num_scenarios
88 | # self.constraints = [sum(t._power[:, k] for t in self.terminals) == 0
89 | # for k in range(num_scenarios)]
90 | self.constraints = [sum(t._power for t in self.terminals) / num_scenarios == 0]
91 |
92 | self.problem = cvx.Problem(cvx.Minimize(0), self.constraints)
93 |
94 | def _set_payments(self):
95 | for t in self.terminals:
96 | t._set_payments(self.price)
97 |
98 | @property
99 | def results(self):
100 | return Results(price={self: self.price})
101 |
102 | @property
103 | def price(self):
104 | """Price associated with this net."""
105 | return self.constraints[0].dual_value
106 | # print([c.dual_value for c in self.constraints])
107 | # raise
108 | # if (len(self.constraints) == 1 and
109 | # np.size(self.constraints[0].dual_value)) == 1:
110 | # return self.constraints[0].dual_value
111 | # TODO(enzo) hardcoded 1/K probability
112 | # return np.sum(constr.dual_value
113 | # for constr in self.constraints)
114 | # if self.num_scenarios > 1:
115 | # return np.matrix(np.sum([constr.dual_value[0]
116 | # for constr in self.constraints], 0))
117 | # return np.hstack(constr.dual_value.reshape(-1, 1)
118 | # for constr in self.constraints)
119 |
120 |
121 | class Device(object):
122 | """Base class for network device.
123 |
124 | Subclasses are expected to override :attr:`constraints` and/or
125 | :attr:`cost` to define the device-specific cost function.
126 |
127 | :param terminals: The terminals of the device
128 | :param name: (optional) Display name of device
129 | :type terminals: list of :class:`Terminal`
130 | :type name: string
131 | """
132 |
133 | def __init__(self, terminals, name=None):
134 | self.name = type(self).__name__ if name is None else name
135 | self.terminals = terminals
136 | self.problem = None
137 |
138 | @property
139 | def cost(self):
140 | """Device objective, to be overriden by subclasses.
141 |
142 | :rtype: cvxpy expression of size :math:`T \times K`
143 | """
144 | return np.zeros((1, 1))
145 |
146 | @property
147 | def constraints(self):
148 | """Device constraints, to be overriden by subclasses.
149 |
150 | :rtype: list of cvxpy constraints
151 | """
152 | return []
153 |
154 | @property
155 | def results(self):
156 | """Network optimization results.
157 |
158 | :rtype: :class:`Results`
159 | """
160 | status = self.problem.status if self.problem else None
161 | return Results(
162 | power={(self, i): t.power for i, t in enumerate(self.terminals)},
163 | payments={(self, i): t.payment for i, t in enumerate(self.terminals)},
164 | status=status,
165 | )
166 |
167 | def _init_problem(self, time_horizon, num_scenarios):
168 | self.problem = cvx.Problem(
169 | cvx.Minimize(cvx.sum(self.cost) / num_scenarios),
170 | # TODO(enzo) we should weight by probs
171 | self.constraints
172 | + [
173 | terminal._power[0, k] == terminal._power[0, 0]
174 | for terminal in self.terminals
175 | for k in range(
176 | 1, terminal._power.shape[1] if len(terminal._power.shape) > 1 else 0
177 | )
178 | ],
179 | )
180 |
181 | def init_problem(self, time_horizon=1, num_scenarios=1):
182 | """Initialize the network optimization problem.
183 |
184 | :param time_horizon: The time horizon :math:`T` to optimize over.
185 | :param num_scenarios: The number of scenarios for robust MPC.
186 | :type time_horizon: int
187 | :type num_scenarios: int
188 | """
189 | for terminal in _get_all_terminals(self):
190 | terminal._init_problem(time_horizon, num_scenarios)
191 |
192 | self._init_problem(time_horizon, num_scenarios)
193 |
194 | def optimize(self, time_horizon=1, num_scenarios=1, **kwargs):
195 | self.init_problem(time_horizon, num_scenarios)
196 | self.problem.solve(**kwargs)
197 | return self.results
198 |
199 |
200 | class Group(Device):
201 | """A single device composed of multiple devices and nets.
202 |
203 |
204 | The `Group` device allows for creating new devices composed of existing base
205 | devices or other groups.
206 |
207 | :param devices: Internal devices to be included.
208 | :param nets: Internal nets to be included.
209 | :param terminals: (optional) Terminals for new device.
210 | :param name: (optional) Display name of group device
211 | :type devices: list of :class:`Device`
212 | :type nets: list of :class:`Net`
213 | :type terminals: list of :class:`Terminal`
214 | :type name: string
215 | """
216 |
217 | def __init__(self, devices, nets, terminals=[], name=None):
218 | super(Group, self).__init__(terminals, name)
219 | self.devices = devices
220 | self.nets = nets
221 |
222 | @property
223 | def results(self):
224 | for n in self.nets:
225 | n._set_payments()
226 | results = sum(x.results for x in self.devices + self.nets)
227 | results.status = self.problem.status if self.problem else None
228 | return results
229 |
230 | def _init_problem(self, time_horizon, num_scenarios):
231 | for device in self.devices:
232 | device._init_problem(time_horizon, num_scenarios)
233 |
234 | for net in self.nets:
235 | net._init_problem(time_horizon, num_scenarios)
236 |
237 | self.problem = sum(x.problem for x in self.devices + self.nets)
238 |
239 | # def optimize(self, **kwargs):
240 | # super(Group, self).optimize(**kwargs)
241 | # for n in self.nets:
242 | # n._set_payments
243 | # raise
244 |
245 |
246 | class Results(object):
247 | """Network optimization results."""
248 |
249 | def __init__(self, power=None, payments=None, price=None, status=None):
250 | self.power = power if power else {}
251 | self.payments = payments if payments else {}
252 | self.price = price if price else {}
253 | self.status = status
254 |
255 | def __radd__(self, other):
256 | return self.__add__(other)
257 |
258 | def __add__(self, other):
259 | if other == 0:
260 | return self
261 | power = self.power.copy()
262 | payments = self.payments.copy()
263 | price = self.price.copy()
264 |
265 | power.update(other.power)
266 | payments.update(other.payments)
267 | price.update(other.price)
268 | status = self.status if self.status is not None else other.status
269 | return Results(power, payments, price, status)
270 |
271 | def __str__(self):
272 | return self.summary()
273 |
274 | def __repr__(self):
275 | return self.summary()
276 |
277 | def summary(self):
278 | """Summary of results. Only works for single period optimization.
279 |
280 | :rtype: str
281 | """
282 |
283 | retval = "Status: " + self.status if self.status else "none"
284 | if not self.status in {cvx.OPTIMAL, cvx.OPTIMAL_INACCURATE}:
285 | return retval
286 |
287 | retval += "\n"
288 | retval += "%-20s %10s\n" % ("Terminal", "Power")
289 | retval += "%-20s %10s\n" % ("--------", "-----")
290 | averages = False
291 | for device_terminal, value in self.power.items():
292 | label = "%s[%d]" % (device_terminal[0].name, device_terminal[1])
293 | if isinstance(value, np.ndarray):
294 | value = np.mean(value)
295 | averages = True
296 | retval += "%-20s %10.2f\n" % (label, value)
297 |
298 | retval += "\n"
299 | retval += "%-20s %10s\n" % ("Net", "Price")
300 | retval += "%-20s %10s\n" % ("---", "-----")
301 | for net, value in self.price.items():
302 | if isinstance(value, np.ndarray):
303 | value = np.mean(value)
304 | retval += "%-20s %10.4f\n" % (net.name, value)
305 |
306 | retval += "\n"
307 | retval += "%-20s %10s\n" % ("Device", "Payment")
308 | retval += "%-20s %10s\n" % ("------", "-------")
309 | device_payments = {d[0][0]: 0 for d in self.payments.items()}
310 | for device_terminal, value in self.payments.items():
311 | if isinstance(value, np.ndarray):
312 | value = np.sum(value)
313 | device_payments[device_terminal[0]] += value
314 | for d in device_payments.keys():
315 | retval += "%-20s %10.2f\n" % (d.name, device_payments[d])
316 |
317 | if averages:
318 | retval += "\nPower and price are averages over the time horizon. Payment is total.\n"
319 |
320 | return retval
321 |
322 | def plot(self, index=None, **kwargs): # , print_terminals=True):
323 | """Plot results."""
324 | import matplotlib.pyplot as plt
325 |
326 | fig, ax = plt.subplots(nrows=2, ncols=1, **kwargs)
327 |
328 | ax[0].set_ylabel("power")
329 | for device_terminal, value in self.power.items():
330 | label = "%s[%d]" % (device_terminal[0].name, device_terminal[1])
331 | if index is None:
332 | ax[0].plot(value, label=label)
333 | else:
334 | ax[0].plot(index, value, label=label)
335 | ax[0].legend(loc="best")
336 |
337 | ax[1].set_ylabel("price")
338 | for net, value in self.price.items():
339 | if index is None:
340 | ax[1].plot(value, label=net.name)
341 | else:
342 | ax[1].plot(index, value, label=net.name)
343 | ax[1].legend(loc="best")
344 |
345 | return ax
346 |
347 |
348 | def _update_mpc_results(t, time_steps, results_t, results_mpc):
349 | for key, val in results_t.power.items():
350 | results_mpc.power.setdefault(key, np.empty(time_steps))[t] = val[0, 0]
351 | for key, val in results_t.price.items():
352 | results_mpc.price.setdefault(key, np.empty(time_steps))[t] = val[0, 0]
353 | for key, val in results_t.payments.items():
354 | results_mpc.payments.setdefault(key, np.empty(time_steps))[t] = val[0, 0]
355 |
356 |
357 | class OptimizationError(Exception):
358 | """Error due to infeasibility or numerical problems during optimization."""
359 |
360 | pass
361 |
362 |
363 | def run_mpc(device, time_steps, predict, execute, **kwargs):
364 | """Execute model predictive control.
365 |
366 | This method executes the model predictive control loop, roughly:
367 |
368 | .. code:: python
369 |
370 | for t in time_steps:
371 | predict(t)
372 | device.problem.solve()
373 | execute(t)
374 | ..
375 |
376 | It is the responsibility of the provided `predict` and `execute` functions
377 | to update the device models with the desired predictions and execute the
378 | actions as appropriate.
379 |
380 | :param device: Device (or network of devices) to optimize
381 | :param time_steps: Time steps to optimize over
382 | :param predict: Prediction step
383 | :param execute: Execution step
384 | :type device: :class:`Device`
385 | :type time_steps: sequence
386 | :type predict: single argument function
387 | :type execute: single argument function
388 | :returns: Model predictive control results
389 | :rtype: :class:`Results`
390 | :raise: :class:`OptimizationError`
391 |
392 | """
393 | total_cost = 0.0
394 | results = Results()
395 | # T_MPC = device
396 | for t in tqdm.trange(time_steps):
397 | predict(t)
398 |
399 | # device.init_problem(time_horizon=1)
400 | device.problem.solve(**kwargs)
401 | if device.problem.status != cvx.OPTIMAL:
402 | # temporary
403 | raise OptimizationError(
404 | "failed at iteration %d, %s" % (t, device.problem.status)
405 | )
406 | stage_cost = sum([device.cost[0, 0] for device in device.devices]).value
407 | # print('at time %s, adding cost %f' % (t, stage_cost))
408 | total_cost += stage_cost
409 | execute(t)
410 | _update_mpc_results(t, time_steps, device.results, results)
411 | return total_cost, results
412 |
--------------------------------------------------------------------------------
/cvxpower/network_test.py:
--------------------------------------------------------------------------------
1 | """
2 | Copyright (C) Matt Wytock, Enzo Busseti, Nicholas Moehle, 2016-2019.
3 |
4 | This program is free software: you can redistribute it and/or modify
5 | it under the terms of the GNU General Public License as published by
6 | the Free Software Foundation, either version 3 of the License, or
7 | (at your option) any later version.
8 | This program is distributed in the hope that it will be useful,
9 | but WITHOUT ANY WARRANTY; without even the implied warranty of
10 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 | GNU General Public License for more details.
12 | You should have received a copy of the GNU General Public License
13 | along with this program. If not, see .
14 |
15 | Code written by Nicholas Moehle before January 2019 is licensed
16 | under the Apache License, Version 2.0 (the "License");
17 | you may not use this file except in compliance with the License.
18 | You may obtain a copy of the License at
19 |
20 | http://www.apache.org/licenses/LICENSE-2.0
21 |
22 | Unless required by applicable law or agreed to in writing, software
23 | distributed under the License is distributed on an "AS IS" BASIS,
24 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
25 | See the License for the specific language governing permissions and
26 | limitations under the License.
27 | """
28 |
29 |
30 | import unittest
31 |
32 | from cvxpower import *
33 |
34 |
35 | class ResultsTest(unittest.TestCase):
36 | def test_summary_normal(self):
37 | load = FixedLoad(power=10)
38 | gen = Generator(power_max=10)
39 | net = Net([load.terminals[0], gen.terminals[0]])
40 | network = Group([load, gen], [net])
41 | network.init_problem()
42 | network.problem.solve(solver="ECOS")
43 | assert "optimal" in network.results.summary()
44 |
45 | def test_summary_infeasible(self):
46 | load = FixedLoad(power=10)
47 | gen = Generator(power_max=1)
48 | net = Net([load.terminals[0], gen.terminals[0]])
49 | network = Group([load, gen], [net])
50 | network.init_problem()
51 | network.problem.solve(solver="ECOS")
52 | assert "infeasible" in network.results.summary()
53 |
54 |
55 | class DeviceTest(unittest.TestCase):
56 | def test_optimize(self):
57 | load = FixedLoad(power=1)
58 | gen = Generator(power_max=10)
59 | net = Net([load.terminals[0], gen.terminals[0]])
60 | network = Group([load, gen], [net])
61 | network.optimize(solver="ECOS")
62 |
63 |
64 | if __name__ == "__main__":
65 | unittest.main()
66 |
--------------------------------------------------------------------------------
/docs/Makefile:
--------------------------------------------------------------------------------
1 | # Makefile for Sphinx documentation
2 | #
3 |
4 | # You can set these variables from the command line.
5 | SPHINXOPTS =
6 | SPHINXBUILD = sphinx-build
7 | PAPER =
8 | BUILDDIR = _build
9 |
10 | # User-friendly check for sphinx-build
11 | ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1)
12 | $(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/)
13 | endif
14 |
15 | # Internal variables.
16 | PAPEROPT_a4 = -D latex_paper_size=a4
17 | PAPEROPT_letter = -D latex_paper_size=letter
18 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
19 | # the i18n builder cannot share the environment and doctrees with the others
20 | I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
21 |
22 | .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest coverage gettext
23 |
24 | help:
25 | @echo "Please use \`make ' where is one of"
26 | @echo " html to make standalone HTML files"
27 | @echo " dirhtml to make HTML files named index.html in directories"
28 | @echo " singlehtml to make a single large HTML file"
29 | @echo " pickle to make pickle files"
30 | @echo " json to make JSON files"
31 | @echo " htmlhelp to make HTML files and a HTML help project"
32 | @echo " qthelp to make HTML files and a qthelp project"
33 | @echo " applehelp to make an Apple Help Book"
34 | @echo " devhelp to make HTML files and a Devhelp project"
35 | @echo " epub to make an epub"
36 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter"
37 | @echo " latexpdf to make LaTeX files and run them through pdflatex"
38 | @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx"
39 | @echo " text to make text files"
40 | @echo " man to make manual pages"
41 | @echo " texinfo to make Texinfo files"
42 | @echo " info to make Texinfo files and run them through makeinfo"
43 | @echo " gettext to make PO message catalogs"
44 | @echo " changes to make an overview of all changed/added/deprecated items"
45 | @echo " xml to make Docutils-native XML files"
46 | @echo " pseudoxml to make pseudoxml-XML files for display purposes"
47 | @echo " linkcheck to check all external links for integrity"
48 | @echo " doctest to run all doctests embedded in the documentation (if enabled)"
49 | @echo " coverage to run coverage check of the documentation (if enabled)"
50 |
51 | clean:
52 | rm -rf $(BUILDDIR)/*
53 |
54 | html:
55 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html
56 | @echo
57 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html."
58 |
59 | dirhtml:
60 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml
61 | @echo
62 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml."
63 |
64 | singlehtml:
65 | $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml
66 | @echo
67 | @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml."
68 |
69 | pickle:
70 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle
71 | @echo
72 | @echo "Build finished; now you can process the pickle files."
73 |
74 | json:
75 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json
76 | @echo
77 | @echo "Build finished; now you can process the JSON files."
78 |
79 | htmlhelp:
80 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp
81 | @echo
82 | @echo "Build finished; now you can run HTML Help Workshop with the" \
83 | ".hhp project file in $(BUILDDIR)/htmlhelp."
84 |
85 | qthelp:
86 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp
87 | @echo
88 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \
89 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:"
90 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/DynamicEnergyManagement.qhcp"
91 | @echo "To view the help file:"
92 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/DynamicEnergyManagement.qhc"
93 |
94 | applehelp:
95 | $(SPHINXBUILD) -b applehelp $(ALLSPHINXOPTS) $(BUILDDIR)/applehelp
96 | @echo
97 | @echo "Build finished. The help book is in $(BUILDDIR)/applehelp."
98 | @echo "N.B. You won't be able to view it unless you put it in" \
99 | "~/Library/Documentation/Help or install it in your application" \
100 | "bundle."
101 |
102 | devhelp:
103 | $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp
104 | @echo
105 | @echo "Build finished."
106 | @echo "To view the help file:"
107 | @echo "# mkdir -p $$HOME/.local/share/devhelp/DynamicEnergyManagement"
108 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/DynamicEnergyManagement"
109 | @echo "# devhelp"
110 |
111 | epub:
112 | $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub
113 | @echo
114 | @echo "Build finished. The epub file is in $(BUILDDIR)/epub."
115 |
116 | latex:
117 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
118 | @echo
119 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex."
120 | @echo "Run \`make' in that directory to run these through (pdf)latex" \
121 | "(use \`make latexpdf' here to do that automatically)."
122 |
123 | latexpdf:
124 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
125 | @echo "Running LaTeX files through pdflatex..."
126 | $(MAKE) -C $(BUILDDIR)/latex all-pdf
127 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex."
128 |
129 | latexpdfja:
130 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
131 | @echo "Running LaTeX files through platex and dvipdfmx..."
132 | $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja
133 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex."
134 |
135 | text:
136 | $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text
137 | @echo
138 | @echo "Build finished. The text files are in $(BUILDDIR)/text."
139 |
140 | man:
141 | $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man
142 | @echo
143 | @echo "Build finished. The manual pages are in $(BUILDDIR)/man."
144 |
145 | texinfo:
146 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
147 | @echo
148 | @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo."
149 | @echo "Run \`make' in that directory to run these through makeinfo" \
150 | "(use \`make info' here to do that automatically)."
151 |
152 | info:
153 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
154 | @echo "Running Texinfo files through makeinfo..."
155 | make -C $(BUILDDIR)/texinfo info
156 | @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo."
157 |
158 | gettext:
159 | $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale
160 | @echo
161 | @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale."
162 |
163 | changes:
164 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes
165 | @echo
166 | @echo "The overview file is in $(BUILDDIR)/changes."
167 |
168 | linkcheck:
169 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck
170 | @echo
171 | @echo "Link check complete; look for any errors in the above output " \
172 | "or in $(BUILDDIR)/linkcheck/output.txt."
173 |
174 | doctest:
175 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest
176 | @echo "Testing of doctests in the sources finished, look at the " \
177 | "results in $(BUILDDIR)/doctest/output.txt."
178 |
179 | coverage:
180 | $(SPHINXBUILD) -b coverage $(ALLSPHINXOPTS) $(BUILDDIR)/coverage
181 | @echo "Testing of coverage in the sources finished, look at the " \
182 | "results in $(BUILDDIR)/coverage/python.txt."
183 |
184 | xml:
185 | $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml
186 | @echo
187 | @echo "Build finished. The XML files are in $(BUILDDIR)/xml."
188 |
189 | pseudoxml:
190 | $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml
191 | @echo
192 | @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml."
193 |
--------------------------------------------------------------------------------
/docs/_static/dynamic_load_results.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cvxgrp/cvxpower/11c48a8d41eff897c05e716903f44908a77e6759/docs/_static/dynamic_load_results.jpg
--------------------------------------------------------------------------------
/docs/conf.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | #
3 | # Dynamic Energy Management documentation build configuration file, created by
4 | # sphinx-quickstart on Mon Aug 1 10:07:28 2016.
5 | #
6 | # This file is execfile()d with the current directory set to its
7 | # containing dir.
8 | #
9 | # Note that not all possible configuration values are present in this
10 | # autogenerated file.
11 | #
12 | # All configuration values have a default; values that are commented out
13 | # serve to show the default.
14 |
15 | import mock
16 | import os
17 | import shlex
18 | import sys
19 |
20 | # If extensions (or modules to document with autodoc) are in another directory,
21 | # add these directories to sys.path here. If the directory is relative to the
22 | # documentation root, use os.path.abspath to make it absolute, like shown here.
23 | sys.path.insert(0, os.path.abspath(".."))
24 |
25 | # Mock out dependencies for readthedocs
26 | MOCK_MODULES = ["cvxpy", "matplotlib", "matplotlib.pyplot", "numpy", "tqdm"]
27 | for mod_name in MOCK_MODULES:
28 | sys.modules[mod_name] = mock.Mock()
29 |
30 | # -- General configuration ------------------------------------------------
31 |
32 | # If your documentation needs a minimal Sphinx version, state it here.
33 | # needs_sphinx = '1.0'
34 |
35 | # Add any Sphinx extension module names here, as strings. They can be
36 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
37 | # ones.
38 | extensions = [
39 | "sphinx.ext.autodoc",
40 | "sphinx.ext.mathjax",
41 | ]
42 |
43 | # Add any paths that contain templates here, relative to this directory.
44 | templates_path = ["_templates"]
45 |
46 | # The suffix(es) of source filenames.
47 | # You can specify multiple suffix as a list of string:
48 | # source_suffix = ['.rst', '.md']
49 | source_suffix = ".rst"
50 |
51 | # The encoding of source files.
52 | # source_encoding = 'utf-8-sig'
53 |
54 | # The master toctree document.
55 | master_doc = "index"
56 |
57 | # General information about the project.
58 | project = u"DEM"
59 | copyright = u"2016, Matt Wytock, Stephen Boyd"
60 | author = u"Matt Wytock, Stephen Boyd"
61 |
62 | # The version info for the project you're documenting, acts as replacement for
63 | # |version| and |release|, also used in various other places throughout the
64 | # built documents.
65 | #
66 | # The short X.Y version.
67 | version = u"0.0.1"
68 | # The full version, including alpha/beta/rc tags.
69 | release = u"0.0.1"
70 |
71 | # The language for content autogenerated by Sphinx. Refer to documentation
72 | # for a list of supported languages.
73 | #
74 | # This is also used if you do content translation via gettext catalogs.
75 | # Usually you set "language" from the command line for these cases.
76 | language = None
77 |
78 | # There are two options for replacing |today|: either, you set today to some
79 | # non-false value, then it is used:
80 | # today = ''
81 | # Else, today_fmt is used as the format for a strftime call.
82 | # today_fmt = '%B %d, %Y'
83 |
84 | # List of patterns, relative to source directory, that match files and
85 | # directories to ignore when looking for source files.
86 | exclude_patterns = ["_build"]
87 |
88 | # The reST default role (used for this markup: `text`) to use for all
89 | # documents.
90 | # default_role = None
91 |
92 | # If true, '()' will be appended to :func: etc. cross-reference text.
93 | # add_function_parentheses = True
94 |
95 | # If true, the current module name will be prepended to all description
96 | # unit titles (such as .. function::).
97 | # add_module_names = True
98 |
99 | # If true, sectionauthor and moduleauthor directives will be shown in the
100 | # output. They are ignored by default.
101 | # show_authors = False
102 |
103 | # The name of the Pygments (syntax highlighting) style to use.
104 | pygments_style = "sphinx"
105 |
106 | # A list of ignored prefixes for module index sorting.
107 | # modindex_common_prefix = []
108 |
109 | # If true, keep warnings as "system message" paragraphs in the built documents.
110 | # keep_warnings = False
111 |
112 | # If true, `todo` and `todoList` produce output, else they produce nothing.
113 | todo_include_todos = False
114 |
115 |
116 | # -- Options for HTML output ----------------------------------------------
117 |
118 | # The theme to use for HTML and HTML Help pages. See the documentation for
119 | # a list of builtin themes.
120 | html_theme = "bizstyle"
121 |
122 | # Theme options are theme-specific and customize the look and feel of a theme
123 | # further. For a list of options available for each theme, see the
124 | # documentation.
125 | html_theme_options = {
126 | # 'github_banner': 'true',
127 | # 'github_repo': 'dem',
128 | # 'github_type': 'star',
129 | # 'github_user': 'mwytock',
130 | }
131 |
132 | # Add any paths that contain custom themes here, relative to this directory.
133 | # html_theme_path = []
134 |
135 | # The name for this set of Sphinx documents. If None, it defaults to
136 | # " v documentation".
137 | # html_title = None
138 |
139 | # A shorter title for the navigation bar. Default is the same as html_title.
140 | # html_short_title = None
141 |
142 | # The name of an image file (relative to this directory) to place at the top
143 | # of the sidebar.
144 | # html_logo = None
145 |
146 | # The name of an image file (within the static path) to use as favicon of the
147 | # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32
148 | # pixels large.
149 | # html_favicon = None
150 |
151 | # Add any paths that contain custom static files (such as style sheets) here,
152 | # relative to this directory. They are copied after the builtin static files,
153 | # so a file named "default.css" will overwrite the builtin "default.css".
154 | html_static_path = ["_static"]
155 |
156 | # Add any extra paths that contain custom files (such as robots.txt or
157 | # .htaccess) here, relative to this directory. These files are copied
158 | # directly to the root of the documentation.
159 | # html_extra_path = []
160 |
161 | # If not '', a 'Last updated on:' timestamp is inserted at every page bottom,
162 | # using the given strftime format.
163 | # html_last_updated_fmt = '%b %d, %Y'
164 |
165 | # If true, SmartyPants will be used to convert quotes and dashes to
166 | # typographically correct entities.
167 | # html_use_smartypants = True
168 |
169 | # Custom sidebar templates, maps document names to template names.
170 | # html_sidebars = {
171 | #'**': [
172 | # 'about.html',
173 | # 'navigation.html',
174 | # ]
175 | # }
176 |
177 | # Additional templates that should be rendered to pages, maps page names to
178 | # template names.
179 | # html_additional_pages = {}
180 |
181 | # If false, no module index is generated.
182 | # html_domain_indices = True
183 |
184 | # If false, no index is generated.
185 | # html_use_index = True
186 |
187 | # If true, the index is split into individual pages for each letter.
188 | # html_split_index = False
189 |
190 | # If true, links to the reST sources are added to the pages.
191 | # html_show_sourcelink = True
192 |
193 | # If true, "Created using Sphinx" is shown in the HTML footer. Default is True.
194 | # html_show_sphinx = True
195 |
196 | # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True.
197 | # html_show_copyright = True
198 |
199 | # If true, an OpenSearch description file will be output, and all pages will
200 | # contain a tag referring to it. The value of this option must be the
201 | # base URL from which the finished HTML is served.
202 | # html_use_opensearch = ''
203 |
204 | # This is the file name suffix for HTML files (e.g. ".xhtml").
205 | # html_file_suffix = None
206 |
207 | # Language to be used for generating the HTML full-text search index.
208 | # Sphinx supports the following languages:
209 | # 'da', 'de', 'en', 'es', 'fi', 'fr', 'hu', 'it', 'ja'
210 | # 'nl', 'no', 'pt', 'ro', 'ru', 'sv', 'tr'
211 | # html_search_language = 'en'
212 |
213 | # A dictionary with options for the search language support, empty by default.
214 | # Now only 'ja' uses this config value
215 | # html_search_options = {'type': 'default'}
216 |
217 | # The name of a javascript file (relative to the configuration directory) that
218 | # implements a search results scorer. If empty, the default will be used.
219 | # html_search_scorer = 'scorer.js'
220 |
221 | # Output file base name for HTML help builder.
222 | htmlhelp_basename = "DynamicEnergyManagementdoc"
223 |
224 | # -- Options for LaTeX output ---------------------------------------------
225 |
226 | latex_elements = {
227 | # The paper size ('letterpaper' or 'a4paper').
228 | #'papersize': 'letterpaper',
229 | # The font size ('10pt', '11pt' or '12pt').
230 | #'pointsize': '10pt',
231 | # Additional stuff for the LaTeX preamble.
232 | #'preamble': '',
233 | # Latex figure (float) alignment
234 | #'figure_align': 'htbp',
235 | }
236 |
237 | # Grouping the document tree into LaTeX files. List of tuples
238 | # (source start file, target name, title,
239 | # author, documentclass [howto, manual, or own class]).
240 | latex_documents = [
241 | (
242 | master_doc,
243 | "DynamicEnergyManagement.tex",
244 | u"Dynamic Energy Management Documentation",
245 | u"Matt Wytock, Stephen Boyd",
246 | "manual",
247 | ),
248 | ]
249 |
250 | # The name of an image file (relative to this directory) to place at the top of
251 | # the title page.
252 | # latex_logo = None
253 |
254 | # For "manual" documents, if this is true, then toplevel headings are parts,
255 | # not chapters.
256 | # latex_use_parts = False
257 |
258 | # If true, show page references after internal links.
259 | # latex_show_pagerefs = False
260 |
261 | # If true, show URL addresses after external links.
262 | # latex_show_urls = False
263 |
264 | # Documents to append as an appendix to all manuals.
265 | # latex_appendices = []
266 |
267 | # If false, no module index is generated.
268 | # latex_domain_indices = True
269 |
270 |
271 | # -- Options for manual page output ---------------------------------------
272 |
273 | # One entry per manual page. List of tuples
274 | # (source start file, name, description, authors, manual section).
275 | man_pages = [
276 | (
277 | master_doc,
278 | "dynamicenergymanagement",
279 | u"Dynamic Energy Management Documentation",
280 | [author],
281 | 1,
282 | )
283 | ]
284 |
285 | # If true, show URL addresses after external links.
286 | # man_show_urls = False
287 |
288 |
289 | # -- Options for Texinfo output -------------------------------------------
290 |
291 | # Grouping the document tree into Texinfo files. List of tuples
292 | # (source start file, target name, title, author,
293 | # dir menu entry, description, category)
294 | texinfo_documents = [
295 | (
296 | master_doc,
297 | "DynamicEnergyManagement",
298 | u"Dynamic Energy Management Documentation",
299 | author,
300 | "DynamicEnergyManagement",
301 | "One line description of project.",
302 | "Miscellaneous",
303 | ),
304 | ]
305 |
306 | # Documents to append as an appendix to all manuals.
307 | # texinfo_appendices = []
308 |
309 | # If false, no module index is generated.
310 | # texinfo_domain_indices = True
311 |
312 | # How to display URL addresses: 'footnote', 'no', or 'inline'.
313 | # texinfo_show_urls = 'footnote'
314 |
315 | # If true, do not generate a @detailmenu in the "Top" node's menu.
316 | # texinfo_no_detailmenu = False
317 |
318 | autodoc_default_flags = [
319 | "members",
320 | "show-inheritance",
321 | ]
322 |
323 | autodoc_member_order = "bysource"
324 |
--------------------------------------------------------------------------------
/docs/devices.rst:
--------------------------------------------------------------------------------
1 |
2 | Devices
3 | =======
4 |
5 | .. automodule:: dem.devices
6 |
7 |
8 | Generators
9 | ----------
10 |
11 | .. autoclass:: dem.devices.Generator
12 |
13 | Loads
14 | -----
15 |
16 | .. autoclass:: dem.devices.FixedLoad
17 | .. autoclass:: dem.devices.DeferrableLoad
18 | .. autoclass:: dem.devices.CurtailableLoad
19 | .. autoclass:: dem.devices.ThermalLoad
20 |
21 | Storage devices
22 | ---------------
23 |
24 | .. autoclass:: dem.devices.Storage
25 |
26 |
27 | Transmission lines
28 | ------------------
29 |
30 | .. autoclass:: dem.devices.TransmissionLine
31 |
--------------------------------------------------------------------------------
/docs/examples.rst:
--------------------------------------------------------------------------------
1 | Examples
2 | ========
3 |
4 | .. toctree::
5 | :maxdepth: 2
6 |
7 | notebooks/static
8 | notebooks/dynamic
9 | notebooks/windfarm
10 | notebooks/microgrid
11 | notebooks/smarthome
12 |
--------------------------------------------------------------------------------
/docs/get_started.rst:
--------------------------------------------------------------------------------
1 | .. Dynamic Energy Management documentation master file, created by
2 | sphinx-quickstart on Mon Aug 1 10:07:28 2016.
3 | You can adapt this file completely to your liking, but it should at least
4 | contain the root `toctree` directive.
5 |
6 | Get Started
7 | ===========
8 |
9 | Introduction
10 | ------------
11 |
12 | Installation
13 | ------------
14 |
15 | Basic Usage
16 | -----------
17 |
18 | .. code:: python
19 |
20 | import numpy as np
21 | from dem import *
22 |
23 | T = 100
24 | p_load = (np.sin(np.pi*np.arange(T)/T)+1)*0.5
25 |
26 | load = FixedLoad(p=p_load)
27 | gen = Generator(p_max=2, p_min=-0.1, alpha=100, beta=100)
28 | net = Net([load.terminals[0], gen.terminals[0]])
29 | network = Group([load, gen], [net])
30 |
31 | network.init_problem(time_horizon=T)
32 | network.problem.solve()
33 | network.results.plot()
34 | ..
35 |
36 | .. image:: _static/dynamic_load_results.jpg
37 |
--------------------------------------------------------------------------------
/docs/index.rst:
--------------------------------------------------------------------------------
1 | .. Dynamic Energy Management documentation master file, created by
2 | sphinx-quickstart on Mon Aug 1 10:07:28 2016.
3 | You can adapt this file completely to your liking, but it should at least
4 | contain the root `toctree` directive.
5 |
6 | Dynamic Energy Management
7 | =========================
8 |
9 | .. toctree::
10 | :maxdepth: 2
11 |
12 | get_started
13 | network
14 | devices
15 | examples
16 |
--------------------------------------------------------------------------------
/docs/network.rst:
--------------------------------------------------------------------------------
1 | Network Model
2 | =============
3 |
4 | .. automodule:: dem.network
5 | :members:
6 |
--------------------------------------------------------------------------------
/docs/notebooks/dynamic.rst:
--------------------------------------------------------------------------------
1 |
2 | Dynamic power flow
3 | ==================
4 |
5 | .. code:: python
6 |
7 | %matplotlib inline
8 | import matplotlib.pyplot as plt
9 | import matplotlib
10 | import numpy as np
11 |
12 | from dem import *
13 |
14 | matplotlib.rc("figure", figsize=(16,6))
15 | matplotlib.rc("lines", linewidth=2)
16 |
17 | T = 100
18 | p_load = np.sin(np.pi*np.arange(T)/T)
19 |
20 | Basic examples
21 | --------------
22 |
23 | Time-varying load
24 | ~~~~~~~~~~~~~~~~~
25 |
26 | .. code:: python
27 |
28 | load = FixedLoad(power=p_load)
29 | gen = Generator(power_max=2, power_min=-0.1, alpha=100, beta=100)
30 | net = Net([load.terminals[0], gen.terminals[0]])
31 | network = Group([load, gen], [net])
32 |
33 | network.init_problem(time_horizon=T)
34 | network.problem.solve()
35 | network.results.plot()
36 |
37 |
38 |
39 |
40 | .. parsed-literal::
41 |
42 | array([,
43 | ], dtype=object)
44 |
45 |
46 |
47 |
48 | .. image:: dynamic_files/dynamic_4_1.png
49 |
50 |
51 | Storage
52 | ~~~~~~~
53 |
54 | .. code:: python
55 |
56 | load = FixedLoad(power=p_load)
57 | gen = Generator(power_max=2, alpha=100, beta=100)
58 | storage = Storage(discharge_max=0.4, charge_max=0.1, energy_max=2)
59 | net = Net([load.terminals[0], gen.terminals[0], storage.terminals[0]])
60 | network = Group([load, gen, storage], [net])
61 |
62 | network.init_problem(time_horizon=T)
63 | network.problem.solve()
64 | network.results.plot()
65 |
66 | fig, ax = plt.subplots(nrows=1, ncols=1, figsize=(16,3))
67 | ax.plot(storage.energy.value)
68 | ax.set_ylabel("stored energy")
69 |
70 |
71 |
72 |
73 | .. parsed-literal::
74 |
75 |
76 |
77 |
78 |
79 |
80 | .. image:: dynamic_files/dynamic_6_1.png
81 |
82 |
83 |
84 | .. image:: dynamic_files/dynamic_6_2.png
85 |
86 |
87 | Deferrable load
88 | ~~~~~~~~~~~~~~~
89 |
90 | .. code:: python
91 |
92 | load = FixedLoad(power=p_load)
93 | gen = Generator(power_max=2, alpha=100, beta=100)
94 | deferrable = DeferrableLoad(time_start=50, energy=20, power_max=0.8)
95 | net = Net([load.terminals[0], gen.terminals[0], deferrable.terminals[0]])
96 | network = Group([load, gen, deferrable], [net])
97 |
98 | network.init_problem(time_horizon=T)
99 | network.problem.solve()
100 | network.results.plot()
101 |
102 |
103 |
104 |
105 | .. parsed-literal::
106 |
107 | array([,
108 | ], dtype=object)
109 |
110 |
111 |
112 |
113 | .. image:: dynamic_files/dynamic_8_1.png
114 |
115 |
116 | Thermal load
117 | ~~~~~~~~~~~~
118 |
119 | .. code:: python
120 |
121 | temp_amb = (np.sin(np.pi*np.arange(T)/T) + 1e-2).reshape(-1,1)**2*50+50
122 |
123 | load = FixedLoad(power=p_load)
124 | gen = Generator(power_max=2, alpha=100, beta=100)
125 | thermal = ThermalLoad(
126 | temp_init=60, temp_amb=temp_amb, temp_min=None, temp_max=80,
127 | power_max=2, amb_conduct_coeff=0.1, efficiency=3, capacity=1)
128 | net = Net([load.terminals[0], gen.terminals[0], thermal.terminals[0]])
129 | network = Group([load, gen, thermal], [net])
130 |
131 | network.init_problem(time_horizon=T)
132 | network.problem.solve()
133 | network.results.plot()
134 |
135 | fig, ax = plt.subplots(nrows=1, ncols=1, figsize=(16,3))
136 | ax.plot(temp_amb, label="Ambient")
137 | ax.plot(thermal.temp.value, label="Internal")
138 | ax.set_ylabel("temperature")
139 | ax.legend()
140 |
141 |
142 |
143 |
144 | .. parsed-literal::
145 |
146 |
147 |
148 |
149 |
150 |
151 | .. image:: dynamic_files/dynamic_10_1.png
152 |
153 |
154 |
155 | .. image:: dynamic_files/dynamic_10_2.png
156 |
157 |
--------------------------------------------------------------------------------
/docs/notebooks/dynamic_files/dynamic_10_1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cvxgrp/cvxpower/11c48a8d41eff897c05e716903f44908a77e6759/docs/notebooks/dynamic_files/dynamic_10_1.png
--------------------------------------------------------------------------------
/docs/notebooks/dynamic_files/dynamic_10_2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cvxgrp/cvxpower/11c48a8d41eff897c05e716903f44908a77e6759/docs/notebooks/dynamic_files/dynamic_10_2.png
--------------------------------------------------------------------------------
/docs/notebooks/dynamic_files/dynamic_11_1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cvxgrp/cvxpower/11c48a8d41eff897c05e716903f44908a77e6759/docs/notebooks/dynamic_files/dynamic_11_1.png
--------------------------------------------------------------------------------
/docs/notebooks/dynamic_files/dynamic_11_2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cvxgrp/cvxpower/11c48a8d41eff897c05e716903f44908a77e6759/docs/notebooks/dynamic_files/dynamic_11_2.png
--------------------------------------------------------------------------------
/docs/notebooks/dynamic_files/dynamic_4_1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cvxgrp/cvxpower/11c48a8d41eff897c05e716903f44908a77e6759/docs/notebooks/dynamic_files/dynamic_4_1.png
--------------------------------------------------------------------------------
/docs/notebooks/dynamic_files/dynamic_5_1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cvxgrp/cvxpower/11c48a8d41eff897c05e716903f44908a77e6759/docs/notebooks/dynamic_files/dynamic_5_1.png
--------------------------------------------------------------------------------
/docs/notebooks/dynamic_files/dynamic_6_1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cvxgrp/cvxpower/11c48a8d41eff897c05e716903f44908a77e6759/docs/notebooks/dynamic_files/dynamic_6_1.png
--------------------------------------------------------------------------------
/docs/notebooks/dynamic_files/dynamic_6_2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cvxgrp/cvxpower/11c48a8d41eff897c05e716903f44908a77e6759/docs/notebooks/dynamic_files/dynamic_6_2.png
--------------------------------------------------------------------------------
/docs/notebooks/dynamic_files/dynamic_7_1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cvxgrp/cvxpower/11c48a8d41eff897c05e716903f44908a77e6759/docs/notebooks/dynamic_files/dynamic_7_1.png
--------------------------------------------------------------------------------
/docs/notebooks/dynamic_files/dynamic_7_2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cvxgrp/cvxpower/11c48a8d41eff897c05e716903f44908a77e6759/docs/notebooks/dynamic_files/dynamic_7_2.png
--------------------------------------------------------------------------------
/docs/notebooks/dynamic_files/dynamic_8_1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cvxgrp/cvxpower/11c48a8d41eff897c05e716903f44908a77e6759/docs/notebooks/dynamic_files/dynamic_8_1.png
--------------------------------------------------------------------------------
/docs/notebooks/dynamic_files/dynamic_9_1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cvxgrp/cvxpower/11c48a8d41eff897c05e716903f44908a77e6759/docs/notebooks/dynamic_files/dynamic_9_1.png
--------------------------------------------------------------------------------
/docs/notebooks/microgrid.rst:
--------------------------------------------------------------------------------
1 |
2 | Community microgrid
3 | ===================
4 |
5 | Coming soon!
6 |
--------------------------------------------------------------------------------
/docs/notebooks/smarthome.rst:
--------------------------------------------------------------------------------
1 |
2 | Smart home
3 | ==========
4 |
5 | Coming soon!
6 |
--------------------------------------------------------------------------------
/docs/notebooks/static.rst:
--------------------------------------------------------------------------------
1 |
2 | Static power flow
3 | =================
4 |
5 | .. code:: python
6 |
7 | %matplotlib inline
8 | import matplotlib.pyplot as plt
9 | import matplotlib
10 | import numpy as np
11 |
12 | from dem import *
13 |
14 | matplotlib.rc("figure", figsize=(7,7))
15 | matplotlib.rc("lines", linewidth=2)
16 |
17 | Basic examples
18 | --------------
19 |
20 | Hello world
21 | ~~~~~~~~~~~
22 |
23 | .. code:: python
24 |
25 | load = FixedLoad(power=100)
26 | gen = Generator(power_max=1000, alpha=0.01, beta=100)
27 | net = Net([load.terminals[0], gen.terminals[0]])
28 | network = Group([load, gen], [net])
29 |
30 | network.init_problem()
31 | network.problem.solve()
32 | print network.results.summary()
33 |
34 |
35 | .. parsed-literal::
36 |
37 | Terminal Power
38 | -------- -----
39 | FixedLoad[0] 100.0000
40 | Generator[0] -100.0000
41 |
42 | Net Price
43 | --- -----
44 | Net 102.0001
45 |
46 |
47 |
48 | Curtailable load
49 | ~~~~~~~~~~~~~~~~
50 |
51 | .. code:: python
52 |
53 | load = CurtailableLoad(power=1000, alpha=150)
54 | gen = Generator(power_max=1000, alpha=1, beta=100)
55 | net = Net([load.terminals[0], gen.terminals[0]])
56 | network = Group([load, gen], [net])
57 |
58 | network.init_problem()
59 | network.problem.solve()
60 | print network.results.summary()
61 |
62 |
63 | .. parsed-literal::
64 |
65 | Terminal Power
66 | -------- -----
67 | CurtailableLoad[0] 25.0026
68 | Generator[0] -25.0026
69 |
70 | Net Price
71 | --- -----
72 | Net 150.0000
73 |
74 |
75 |
76 | Two generators, transmission line
77 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
78 |
79 | .. code:: python
80 |
81 | load = FixedLoad(power=100)
82 | gen1 = Generator(power_max=1000, alpha=0.01, beta=100, name="Gen1")
83 | gen2 = Generator(power_max=100, alpha=0.1, beta=0.1, name="Gen2")
84 | line = TransmissionLine(power_max=50)
85 |
86 | net1 = Net([load.terminals[0], gen1.terminals[0], line.terminals[0]])
87 | net2 = Net([gen2.terminals[0], line.terminals[1]])
88 | network = Group([load, gen1, gen2, line], [net1, net2])
89 |
90 | network.init_problem()
91 | network.problem.solve()
92 | print network.results.summary()
93 |
94 |
95 | .. parsed-literal::
96 |
97 | Terminal Power
98 | -------- -----
99 | FixedLoad[0] 100.0000
100 | TransmissionLine[0] -50.0000
101 | TransmissionLine[1] 50.0000
102 | Gen2[0] -50.0000
103 | Gen1[0] -50.0000
104 |
105 | Net Price
106 | --- -----
107 | Net 101.0000
108 | Net 10.1000
109 |
110 |
111 |
112 | Three buses
113 | ~~~~~~~~~~~
114 |
115 | Figure 2.1 from Kraning, et al. without the battery.
116 |
117 | .. figure:: ./three_bus.png
118 | :alt: Three bus example
119 |
120 | Three bus example
121 |
122 | .. code:: python
123 |
124 | load1 = FixedLoad(power=50, name="Load1")
125 | load2 = FixedLoad(power=100, name="Load2")
126 | gen1 = Generator(power_max=1000, alpha=0.01, beta=100, name="Gen1")
127 | gen2 = Generator(power_max=100, alpha=0.1, beta=0.1, name="Gen2")
128 | line1 = TransmissionLine(power_max=50)
129 | line2 = TransmissionLine(power_max=10)
130 | line3 = TransmissionLine(power_max=50)
131 |
132 | net1 = Net([load1.terminals[0], gen1.terminals[0], line1.terminals[0], line2.terminals[0]])
133 | net2 = Net([load2.terminals[0], line1.terminals[1], line3.terminals[0]])
134 | net3 = Net([gen2.terminals[0], line2.terminals[1], line3.terminals[1]])
135 | network = Group([load1, load2, gen1, gen2, line1, line2, line3], [net1, net2, net3])
136 |
137 | network.init_problem()
138 | network.problem.solve()
139 | print network.results.summary()
140 |
141 |
142 | .. parsed-literal::
143 |
144 | Terminal Power
145 | -------- -----
146 | TransmissionLine[1] 50.0000
147 | TransmissionLine[0] -10.0000
148 | Gen2[0] -60.0000
149 | Gen1[0] -90.0000
150 | TransmissionLine[0] 50.0000
151 | TransmissionLine[1] -50.0000
152 | Load2[0] 100.0000
153 | Load1[0] 50.0000
154 | TransmissionLine[1] 10.0000
155 | TransmissionLine[0] -50.0000
156 |
157 | Net Price
158 | --- -----
159 | Net 101.8008
160 | Net 196.1907
161 | Net 12.0975
162 |
163 |
164 |
165 | Grouping devices
166 | ----------------
167 |
168 | We can wrap up several devices and nets into a single device
169 |
170 | .. code:: python
171 |
172 | solar = Generator(power_max=10, alpha=0, beta=0, name="Solar")
173 | load = FixedLoad(power=13)
174 | line = TransmissionLine(power_max=25)
175 | net = Net([load.terminals[0], solar.terminals[0], line.terminals[0]])
176 | home = Group([solar, load, line], [net], [line.terminals[1]], name="Home")
177 |
178 | grid = Generator(power_max=1e6, alpha=0.05, beta=100, name="Grid")
179 | meter = Net([line.terminals[1], grid.terminals[0]], name="Meter")
180 | network = Group([home, grid], [meter])
181 |
182 | network.init_problem()
183 | network.problem.solve()
184 | print network.results.summary()
185 |
186 |
187 | .. parsed-literal::
188 |
189 | Terminal Power
190 | -------- -----
191 | FixedLoad[0] 13.0000
192 | TransmissionLine[1] 3.0000
193 | Solar[0] -10.0000
194 | Grid[0] -3.0000
195 | TransmissionLine[0] -3.0000
196 |
197 | Net Price
198 | --- -----
199 | Net 100.3000
200 | Meter 100.3000
201 |
202 |
203 |
204 | Varying parameters
205 | ------------------
206 |
207 | We can modify a single parameter and reoptimize, which is useful for
208 | sweeping over a parameter range.
209 |
210 | .. code:: python
211 |
212 | load1 = FixedLoad(power=50, name="Load1")
213 | load2 = FixedLoad(power=100, name="Load2")
214 | gen1 = Generator(power_max=100, alpha=1, beta=10, name="Gen1")
215 | gen2 = Generator(power_max=1000, alpha=0.01, beta=0, name="Gen2")
216 | line1 = TransmissionLine(power_max=100)
217 | line2 = TransmissionLine(power_max=10)
218 | line3 = TransmissionLine(power_max=Parameter(1))
219 |
220 | net1 = Net([load1.terminals[0], gen1.terminals[0], line1.terminals[0], line2.terminals[0]])
221 | net2 = Net([load2.terminals[0], line1.terminals[1], line3.terminals[0]])
222 | net3 = Net([gen2.terminals[0], line2.terminals[1], line3.terminals[1]])
223 | network = Group([load1, load2, gen1, gen2, line1, line2, line3], [net1, net2, net3])
224 |
225 | network.init_problem()
226 | xs = np.linspace(0, 150, 100)
227 | prices = np.empty((len(xs), 3))
228 | for i, x in enumerate(xs):
229 | line3.power_max.value = x
230 | network.problem.solve()
231 | prices[i,:] = [net.price for net in network.nets]
232 |
233 | plt.plot(xs, prices)
234 | plt.xlabel("Line capacity")
235 | plt.ylabel("Price")
236 | plt.legend(["Bus 1", "Bus 2", "Bus 3"])
237 |
238 |
239 |
240 |
241 | .. parsed-literal::
242 |
243 |
244 |
245 |
246 |
247 |
248 | .. image:: static_files/static_14_1.png
249 |
250 |
--------------------------------------------------------------------------------
/docs/notebooks/static_files/static_14_1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cvxgrp/cvxpower/11c48a8d41eff897c05e716903f44908a77e6759/docs/notebooks/static_files/static_14_1.png
--------------------------------------------------------------------------------
/docs/notebooks/three_bus.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cvxgrp/cvxpower/11c48a8d41eff897c05e716903f44908a77e6759/docs/notebooks/three_bus.png
--------------------------------------------------------------------------------
/docs/notebooks/windfarm.rst:
--------------------------------------------------------------------------------
1 |
2 | Wind farm integration
3 | =====================
4 |
5 | An example of a wind farm offering firm power by estimating the expected
6 | wind resource and then using a MPC to jointly optimize control of a
7 | small gas turbine and storage.
8 |
9 | .. code:: python
10 |
11 | %matplotlib inline
12 | import datetime
13 | import pandas as pd
14 | import matplotlib
15 | import numpy as np
16 | import cvxpy as cvx
17 | from matplotlib import pyplot as plt
18 |
19 | from dem import *
20 |
21 | matplotlib.rc("figure", figsize=(16,6))
22 | matplotlib.rc("lines", linewidth=2)
23 |
24 | Wind resource
25 | -------------
26 |
27 | Data is from `NREL wind integration
28 | dataset `__,
29 | site 20182.
30 |
31 | .. code:: python
32 |
33 | def read_wind_csv(filename):
34 | df = pd.read_csv(
35 | filename,
36 | skiprows=3,
37 | parse_dates=[[0,1,2,3,4]],
38 | date_parser=lambda *cols: datetime.datetime(*map(int, cols)),
39 | index_col=0)
40 | df.index.name = "time"
41 | return df
42 |
43 | wind = pd.concat([
44 | read_wind_csv("nrel_wind/20182-2011.csv"),
45 | read_wind_csv("nrel_wind/20182-2012.csv")])
46 | p_wind = wind["power (MW)"].resample("15min").mean()
47 | p_wind["2012-01"].plot()
48 |
49 |
50 |
51 |
52 | .. parsed-literal::
53 |
54 |
55 |
56 |
57 |
58 |
59 | .. image:: windfarm_files/windfarm_3_1.png
60 |
61 |
62 | Power blocks
63 | ~~~~~~~~~~~~
64 |
65 | The amount of power offered for sale
66 |
67 | .. code:: python
68 |
69 | def interval_of_day(dt):
70 | return dt.hour*4 + dt.minute/15
71 |
72 | # compute target output
73 | p_wind_by_interval = p_wind.groupby(interval_of_day(p_wind.index)).mean()
74 | p_wind_mean = pd.Series([p_wind_by_interval[interval_of_day(x)] for x in p_wind.index], index=p_wind.index)
75 |
76 | p_wind_by_interval.plot()
77 | plt.ylim([0,16])
78 |
79 |
80 |
81 |
82 | .. parsed-literal::
83 |
84 | (0, 16)
85 |
86 |
87 |
88 |
89 | .. image:: windfarm_files/windfarm_5_1.png
90 |
91 |
92 | MPC
93 | ---
94 |
95 | Autoregressive model
96 | ~~~~~~~~~~~~~~~~~~~~
97 |
98 | .. code:: python
99 |
100 | from sklearn import linear_model
101 |
102 | H = 4*6
103 | p_wind_residual = p_wind - p_wind_mean
104 | X = np.hstack([p_wind_residual.shift(x).fillna(0).reshape(-1,1) for x in xrange(1,H+1)])
105 | lr = linear_model.RidgeCV()
106 | lr.fit(X, p_wind_residual)
107 |
108 | def predict_wind(t, T):
109 | r = np.zeros(T)
110 | x = X[t,:]
111 | for i in xrange(T):
112 | tau = t+i
113 | r[i] = lr.predict(x.reshape(1,-1))
114 | x = np.hstack((r[i], x[:-1]))
115 | return np.maximum(p_wind_mean[t:t+T] + r, 0)
116 |
117 | t = 4*24*2
118 | T = 4*24
119 | compare = pd.DataFrame(index=p_wind.index)
120 | compare["p_wind"] = p_wind
121 | compare["p_wind_pred"] = pd.Series(predict_wind(t, T), index=p_wind.index[t:t+T])
122 | compare["p_out"] = p_wind_mean
123 | compare["2011-01-02":"2011-01-03"].plot()
124 |
125 |
126 |
127 |
128 | .. parsed-literal::
129 |
130 |
131 |
132 |
133 |
134 |
135 | .. image:: windfarm_files/windfarm_8_1.png
136 |
137 |
138 | .. code:: python
139 |
140 | T = 4*24
141 | out = FixedLoad(power=Parameter(T+1), name="Output")
142 | wind_gen = Generator(alpha=0, beta=0, power_min=0, power_max=Parameter(T+1), name="Wind")
143 | gas_gen = Generator(alpha=0.02, beta=1, power_min=0.01, power_max=1, name="Gas")
144 | storage = Storage(discharge_max=1, charge_max=1, energy_max=12*4, energy_init=Parameter(1, value=6*4))
145 | net = Net([wind_gen.terminals[0],
146 | gas_gen.terminals[0],
147 | storage.terminals[0],
148 | out.terminals[0]])
149 | network = Group([wind_gen, gas_gen, storage, out], [net])
150 | network.init_problem(time_horizon=T+1)
151 |
152 | def predict(t):
153 | out.power.value = p_wind_mean[t:t+T+1].as_matrix()/16
154 | wind_gen.power_max.value = np.hstack((p_wind[t], predict_wind(t+1,T)))/16
155 |
156 | def execute(t):
157 | energy_stored[t] = storage.energy.value[0]
158 | storage.energy_init.value = storage.energy.value[0]
159 |
160 | N = 4*24*7
161 | energy_stored = np.empty(N)
162 | results = run_mpc(network, N, predict, execute)
163 |
164 |
165 | .. parsed-literal::
166 |
167 | 100%|██████████| 672/672 [00:29<00:00, 22.65it/s]
168 |
169 |
170 | .. code:: python
171 |
172 | # plot the results
173 | results.plot()
174 |
175 | # plot energy stored in battery
176 | fig, ax = plt.subplots(nrows=1, ncols=1, figsize=(16,3))
177 | ax.plot(energy_stored)
178 | ax.set_ylabel("energy stored")
179 |
180 |
181 |
182 |
183 | .. parsed-literal::
184 |
185 |
186 |
187 |
188 |
189 |
190 | .. image:: windfarm_files/windfarm_10_1.png
191 |
192 |
193 |
194 | .. image:: windfarm_files/windfarm_10_2.png
195 |
196 |
197 | Wind curtailment
198 | ~~~~~~~~~~~~~~~~
199 |
200 | Current model is not too smart about using all the available wind power,
201 | at times it doesn't even charge the battery when there is extra power
202 | available...
203 |
204 | .. code:: python
205 |
206 | plt.plot(xrange(N), p_wind[:N]/16, label="p_wind_max")
207 | plt.plot(-results.power[(wind_gen, 0)], label="p_wind")
208 | plt.legend()
209 |
210 |
211 |
212 |
213 | .. parsed-literal::
214 |
215 |
216 |
217 |
218 |
219 |
220 | .. image:: windfarm_files/windfarm_12_1.png
221 |
222 |
223 | Robust MPC
224 | ----------
225 |
226 | Probabilistic predictions
227 | ~~~~~~~~~~~~~~~~~~~~~~~~~
228 |
229 | .. code:: python
230 |
231 | from sklearn import linear_model
232 |
233 | H = 4*6
234 | p_wind_residual = p_wind - p_wind_mean
235 | X = np.hstack([p_wind_residual.shift(x).fillna(0).reshape(-1,1) for x in xrange(1,H+1)])
236 | lr = linear_model.RidgeCV()
237 | lr.fit(X, p_wind_residual)
238 | sigma = np.std(lr.predict(X) - p_wind_residual)
239 |
240 | def predict_wind_probs(t, T, K):
241 | np.random.seed(0)
242 | R = np.empty((T,K))
243 | Xp = np.tile(X[t,:], (K,1)) # K x H prediction matrix
244 | for i in xrange(T):
245 | tau = t+i
246 | R[i,:] = lr.predict(Xp) + sigma*np.random.randn(K)
247 | Xp = np.hstack((R[i:i+1,:].T, Xp[:,:-1]))
248 | return np.minimum(np.maximum(np.tile(p_wind_mean[t:t+T], (K,1)).T + R, 0), 16)
249 |
250 | # plot an example of predictions at a particular time
251 | idx = slice("2011-01-02", "2011-01-03")
252 | t = 4*24*2
253 | T = 4*24
254 | K = 10
255 | wind_probs = predict_wind_probs(t, T, K)
256 | ax0 = plt.plot(p_wind[idx].index, p_wind[idx])
257 | ax1 = plt.plot(p_wind.index[t:t+T], wind_probs, color="b", alpha=0.3)
258 | ax2 = plt.plot(p_wind_mean[idx].index, p_wind_mean[idx])
259 | plt.legend(handles=[ax0[0], ax1[0], ax2[0]], labels=["p_wind", "p_wind_pred", "p_out"])
260 |
261 |
262 |
263 |
264 | .. parsed-literal::
265 |
266 |
267 |
268 |
269 |
270 |
271 | .. image:: windfarm_files/windfarm_15_1.png
272 |
273 |
274 | .. code:: python
275 |
276 | T = 24*4*2 # 48 hours, 15 minute intervals
277 | K = 10 # 10 scenarios
278 |
279 | out = FixedLoad(power=Parameter(T+1,K), name="Output")
280 | wind_gen = Generator(alpha=0, beta=0, power_min=0, power_max=Parameter(T+1,K), name="Wind")
281 | gas_gen = Generator(alpha=0.02, beta=1, power_min=0.01, power_max=1, name="Gas")
282 | storage = Storage(discharge_max=1, charge_max=1, energy_max=12*4, energy_init=Parameter(1, value=6*4))
283 | net = Net([wind_gen.terminals[0],
284 | gas_gen.terminals[0],
285 | storage.terminals[0],
286 | out.terminals[0]])
287 | network = Group([wind_gen, gas_gen, storage, out], [net])
288 | network.init_problem(time_horizon=T+1, num_scenarios=K)
289 |
290 | def predict(t):
291 | out.power.value = np.tile(p_wind_mean[t:t+T+1], (K,1)).T / 16
292 | wind_gen.power_max.value = np.empty((T+1, K))
293 | wind_gen.power_max.value[0,:] = p_wind[t] / 16
294 | wind_gen.power_max.value[1:,:] = predict_wind_probs(t+1,T,K) / 16
295 |
296 | def execute(t):
297 | energy_stored[t] = storage.energy.value[0,0]
298 | storage.energy_init.value = energy_stored[t]
299 |
300 | N = 7*24*4 # 7 days
301 | energy_stored = np.empty(N)
302 | results = run_mpc(network, N, predict, execute, solver=cvx.MOSEK)
303 |
304 |
305 | .. parsed-literal::
306 |
307 | 100%|██████████| 672/672 [1:21:08<00:00, 5.88s/it]
308 |
309 |
310 | .. code:: python
311 |
312 | # plot the results
313 | results.plot()
314 |
315 | # plot energy stored in battery
316 | fig, ax = plt.subplots(nrows=1, ncols=1, figsize=(16,3))
317 | ax.plot(energy_stored)
318 | ax.set_ylabel("energy stored")
319 |
320 |
321 |
322 |
323 | .. parsed-literal::
324 |
325 |
326 |
327 |
328 |
329 |
330 | .. image:: windfarm_files/windfarm_17_1.png
331 |
332 |
333 |
334 | .. image:: windfarm_files/windfarm_17_2.png
335 |
336 |
337 | .. code:: python
338 |
339 | plt.plot(xrange(N), p_wind[:N]/16, label="p_wind_max")
340 | plt.plot(-results.power[(wind_gen, 0)], label="p_wind")
341 | plt.legend()
342 |
343 |
344 |
345 |
346 | .. parsed-literal::
347 |
348 |
349 |
350 |
351 |
352 |
353 | .. image:: windfarm_files/windfarm_18_1.png
354 |
355 |
--------------------------------------------------------------------------------
/docs/notebooks/windfarm_files/windfarm_10_1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cvxgrp/cvxpower/11c48a8d41eff897c05e716903f44908a77e6759/docs/notebooks/windfarm_files/windfarm_10_1.png
--------------------------------------------------------------------------------
/docs/notebooks/windfarm_files/windfarm_10_2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cvxgrp/cvxpower/11c48a8d41eff897c05e716903f44908a77e6759/docs/notebooks/windfarm_files/windfarm_10_2.png
--------------------------------------------------------------------------------
/docs/notebooks/windfarm_files/windfarm_11_1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cvxgrp/cvxpower/11c48a8d41eff897c05e716903f44908a77e6759/docs/notebooks/windfarm_files/windfarm_11_1.png
--------------------------------------------------------------------------------
/docs/notebooks/windfarm_files/windfarm_11_2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cvxgrp/cvxpower/11c48a8d41eff897c05e716903f44908a77e6759/docs/notebooks/windfarm_files/windfarm_11_2.png
--------------------------------------------------------------------------------
/docs/notebooks/windfarm_files/windfarm_12_1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cvxgrp/cvxpower/11c48a8d41eff897c05e716903f44908a77e6759/docs/notebooks/windfarm_files/windfarm_12_1.png
--------------------------------------------------------------------------------
/docs/notebooks/windfarm_files/windfarm_13_1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cvxgrp/cvxpower/11c48a8d41eff897c05e716903f44908a77e6759/docs/notebooks/windfarm_files/windfarm_13_1.png
--------------------------------------------------------------------------------
/docs/notebooks/windfarm_files/windfarm_15_1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cvxgrp/cvxpower/11c48a8d41eff897c05e716903f44908a77e6759/docs/notebooks/windfarm_files/windfarm_15_1.png
--------------------------------------------------------------------------------
/docs/notebooks/windfarm_files/windfarm_17_1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cvxgrp/cvxpower/11c48a8d41eff897c05e716903f44908a77e6759/docs/notebooks/windfarm_files/windfarm_17_1.png
--------------------------------------------------------------------------------
/docs/notebooks/windfarm_files/windfarm_17_2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cvxgrp/cvxpower/11c48a8d41eff897c05e716903f44908a77e6759/docs/notebooks/windfarm_files/windfarm_17_2.png
--------------------------------------------------------------------------------
/docs/notebooks/windfarm_files/windfarm_18_1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cvxgrp/cvxpower/11c48a8d41eff897c05e716903f44908a77e6759/docs/notebooks/windfarm_files/windfarm_18_1.png
--------------------------------------------------------------------------------
/docs/notebooks/windfarm_files/windfarm_3_1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cvxgrp/cvxpower/11c48a8d41eff897c05e716903f44908a77e6759/docs/notebooks/windfarm_files/windfarm_3_1.png
--------------------------------------------------------------------------------
/docs/notebooks/windfarm_files/windfarm_5_1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cvxgrp/cvxpower/11c48a8d41eff897c05e716903f44908a77e6759/docs/notebooks/windfarm_files/windfarm_5_1.png
--------------------------------------------------------------------------------
/docs/notebooks/windfarm_files/windfarm_8_1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cvxgrp/cvxpower/11c48a8d41eff897c05e716903f44908a77e6759/docs/notebooks/windfarm_files/windfarm_8_1.png
--------------------------------------------------------------------------------
/examples/HistoricalEMSHourlyLoad_2014-2016.xlsx:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cvxgrp/cvxpower/11c48a8d41eff897c05e716903f44908a77e6759/examples/HistoricalEMSHourlyLoad_2014-2016.xlsx
--------------------------------------------------------------------------------
/examples/ThreeBusExample.ipynb:
--------------------------------------------------------------------------------
1 | {
2 | "cells": [
3 | {
4 | "cell_type": "code",
5 | "execution_count": 1,
6 | "metadata": {},
7 | "outputs": [],
8 | "source": [
9 | "import os\n",
10 | "import sys\n",
11 | "sys.path.insert(0, os.path.abspath('..'))\n",
12 | "from cvxpower import *\n",
13 | "\n",
14 | "%matplotlib inline\n",
15 | "import matplotlib.pyplot as plt\n",
16 | "import numpy as np\n",
17 | "#from config import *"
18 | ]
19 | },
20 | {
21 | "cell_type": "code",
22 | "execution_count": 2,
23 | "metadata": {},
24 | "outputs": [
25 | {
26 | "name": "stdout",
27 | "output_type": "stream",
28 | "text": [
29 | "Status: optimal\n",
30 | "Terminal Power\n",
31 | "-------- -----\n",
32 | "load1[0] 50.00\n",
33 | "load2[0] 100.00\n",
34 | "gen1[0] -90.00\n",
35 | "gen2[0] -60.00\n",
36 | "line1[0] 50.00\n",
37 | "line1[1] -50.00\n",
38 | "line2[0] -10.00\n",
39 | "line2[1] 10.00\n",
40 | "line3[0] -50.00\n",
41 | "line3[1] 50.00\n",
42 | "\n",
43 | "Net Price\n",
44 | "--- -----\n",
45 | "net1 33.6000\n",
46 | "net2 199.5965\n",
47 | "net3 24.0012\n",
48 | "\n",
49 | "Device Payment\n",
50 | "------ -------\n",
51 | "load1 1680.00\n",
52 | "load2 19959.65\n",
53 | "gen1 -3024.00\n",
54 | "gen2 -1440.07\n",
55 | "line1 -8299.83\n",
56 | "line2 -95.99\n",
57 | "line3 -8779.76\n",
58 | "\n",
59 | "Power and price are averages over the time horizon. Payment is total.\n",
60 | "\n"
61 | ]
62 | }
63 | ],
64 | "source": [
65 | "load1 = FixedLoad(power=50, name=\"load1\")\n",
66 | "load2 = FixedLoad(power=100, name=\"load2\")\n",
67 | "\n",
68 | "gen1 = Generator(power_max=1000, alpha=0.02, beta=30, name=\"gen1\")\n",
69 | "gen2 = Generator(power_max=100, alpha=0.2, beta=0, name=\"gen2\")\n",
70 | "\n",
71 | "line1 = TransmissionLine(power_max=50, name='line1')\n",
72 | "line2 = TransmissionLine(power_max=10, name='line2')\n",
73 | "line3 = TransmissionLine(power_max=50, name='line3')\n",
74 | "\n",
75 | "net1 = Net([load1.terminals[0], gen1.terminals[0], \n",
76 | " line1.terminals[0], line2.terminals[0]], name = 'net1')\n",
77 | "net2 = Net([load2.terminals[0], line1.terminals[1], \n",
78 | " line3.terminals[0]], name = 'net2')\n",
79 | "net3 = Net([gen2.terminals[0], line2.terminals[1], \n",
80 | " line3.terminals[1]], name = 'net3')\n",
81 | "network = Group([load1, load2, gen1, gen2,\n",
82 | " line1, line2, line3],\n",
83 | " [net1, net2, net3])\n",
84 | "\n",
85 | "network.init_problem()\n",
86 | "network.optimize(solver='ECOS')\n",
87 | "print(network.results.summary())"
88 | ]
89 | },
90 | {
91 | "cell_type": "code",
92 | "execution_count": 5,
93 | "metadata": {},
94 | "outputs": [
95 | {
96 | "data": {
97 | "text/plain": [
98 | "['__add__',\n",
99 | " '__class__',\n",
100 | " '__delattr__',\n",
101 | " '__dict__',\n",
102 | " '__dir__',\n",
103 | " '__doc__',\n",
104 | " '__eq__',\n",
105 | " '__format__',\n",
106 | " '__ge__',\n",
107 | " '__getattribute__',\n",
108 | " '__gt__',\n",
109 | " '__hash__',\n",
110 | " '__init__',\n",
111 | " '__init_subclass__',\n",
112 | " '__le__',\n",
113 | " '__lt__',\n",
114 | " '__module__',\n",
115 | " '__ne__',\n",
116 | " '__new__',\n",
117 | " '__radd__',\n",
118 | " '__reduce__',\n",
119 | " '__reduce_ex__',\n",
120 | " '__repr__',\n",
121 | " '__setattr__',\n",
122 | " '__sizeof__',\n",
123 | " '__str__',\n",
124 | " '__subclasshook__',\n",
125 | " '__weakref__',\n",
126 | " 'payments',\n",
127 | " 'plot',\n",
128 | " 'power',\n",
129 | " 'price',\n",
130 | " 'status',\n",
131 | " 'summary']"
132 | ]
133 | },
134 | "execution_count": 5,
135 | "metadata": {},
136 | "output_type": "execute_result"
137 | }
138 | ],
139 | "source": [
140 | "flows = pd.Series()\n",
141 | "dir(network.results)"
142 | ]
143 | },
144 | {
145 | "cell_type": "code",
146 | "execution_count": 64,
147 | "metadata": {},
148 | "outputs": [
149 | {
150 | "data": {
151 | "text/plain": [
152 | "{: array([[24.00116919]]),\n",
153 | " : array([[33.59995155]]),\n",
154 | " : array([[199.59646122]])}"
155 | ]
156 | },
157 | "execution_count": 64,
158 | "metadata": {},
159 | "output_type": "execute_result"
160 | }
161 | ],
162 | "source": [
163 | "network.results.price"
164 | ]
165 | },
166 | {
167 | "cell_type": "code",
168 | "execution_count": 50,
169 | "metadata": {},
170 | "outputs": [
171 | {
172 | "name": "stdout",
173 | "output_type": "stream",
174 | "text": [
175 | "\\begin{tabular}{lll}\n",
176 | "\\toprule\n",
177 | "{} & terminal 1 & terminal 2 \\\\\n",
178 | "\\midrule\n",
179 | "load1 & 50MW & - \\\\\n",
180 | "load2 & 100MW & - \\\\\n",
181 | "gen1 & -90MW & - \\\\\n",
182 | "gen2 & -60MW & - \\\\\n",
183 | "line1 & 50MW & -50MW \\\\\n",
184 | "line2 & -10MW & 10MW \\\\\n",
185 | "line3 & -50MW & 50MW \\\\\n",
186 | "\\bottomrule\n",
187 | "\\end{tabular}\n",
188 | "\n"
189 | ]
190 | }
191 | ],
192 | "source": [
193 | "import pandas as pd\n",
194 | "flows = pd.DataFrame(columns=['terminal 1', 'terminal 2'])\n",
195 | "for (device, terminal) in network.results.power.keys():\n",
196 | " flows.loc[device.name, 'terminal %d' % (terminal +1)] = network.results.power[(device, terminal)][0][0]\n",
197 | " \n",
198 | "print(flows.to_latex(na_rep = '-', float_format=lambda x: '%.0fMW' % x))"
199 | ]
200 | },
201 | {
202 | "cell_type": "code",
203 | "execution_count": null,
204 | "metadata": {},
205 | "outputs": [],
206 | "source": [
207 | "import pandas as pd\n",
208 | "prices = pd.DataFrame(columns=['price'])\n",
209 | "for (device, terminal) in network.results.power.keys():\n",
210 | " flows.loc[device.name, 'terminal %d' % (terminal +1)] = network.results.power[(device, terminal)][0][0]\n",
211 | " \n",
212 | "print(flows.to_latex(na_rep = '-', float_format=lambda x: '%.0fMW' % x))"
213 | ]
214 | },
215 | {
216 | "cell_type": "code",
217 | "execution_count": 63,
218 | "metadata": {},
219 | "outputs": [
220 | {
221 | "name": "stdout",
222 | "output_type": "stream",
223 | "text": [
224 | "\\begin{tabular}{lllr}\n",
225 | "\\toprule\n",
226 | "{} & terminal 1 & terminal 2 & total \\\\\n",
227 | "\\midrule\n",
228 | "load1 & \\$1680 & - & \\$1680 \\\\\n",
229 | "load2 & \\$19960 & - & \\$19960 \\\\\n",
230 | "gen1 & -\\$3024 & - & -\\$3024 \\\\\n",
231 | "gen2 & -\\$1440 & - & -\\$1440 \\\\\n",
232 | "line1 & \\$1680 & -\\$9980 & -\\$8300 \\\\\n",
233 | "line2 & -\\$336 & \\$240 & -\\$96 \\\\\n",
234 | "line3 & -\\$9980 & \\$1200 & -\\$8780 \\\\\n",
235 | "\\bottomrule\n",
236 | "\\end{tabular}\n",
237 | "\n"
238 | ]
239 | }
240 | ],
241 | "source": [
242 | "payments = pd.DataFrame(columns=['terminal 1', 'terminal 2', 'total'])\n",
243 | "for (device, terminal) in network.results.payments.keys():\n",
244 | " payments.loc[device.name, 'terminal %d' % (terminal +1)] = network.results.payments[(device, terminal)][0][0]\n",
245 | "\n",
246 | "payments['total'] = payments.iloc[:,:2].sum(1)\n",
247 | "print(payments.to_latex(na_rep = '-', float_format=lambda x: ('-' if np.sign(x) < 0 else '') + '$%.0f' % np.abs(x)))"
248 | ]
249 | },
250 | {
251 | "cell_type": "code",
252 | "execution_count": 3,
253 | "metadata": {},
254 | "outputs": [
255 | {
256 | "data": {
257 | "image/png": "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\n",
258 | "text/plain": [
259 | ""
260 | ]
261 | },
262 | "metadata": {},
263 | "output_type": "display_data"
264 | }
265 | ],
266 | "source": [
267 | "alpha1, beta1, pmax1 = 0.02, 30, 1000\n",
268 | "alpha2, beta2, pmax2 = 0.2, 0, 100\n",
269 | "\n",
270 | "#plt.figure(figsize=FIGREGULAR)\n",
271 | "xs = np.linspace(0.001,pmax1)\n",
272 | "plt.plot(xs, (alpha1*xs**2 + beta1*xs)/xs, label=r'$f_{\\rm gen1}$')\n",
273 | "xs = np.linspace(0.001,pmax2)\n",
274 | "plt.plot(xs, (alpha2*xs**2 + beta2*xs)/xs, label=r'$f_{\\rm gen2}$')\n",
275 | "plt.legend()\n",
276 | "plt.xlabel(r'power produced (MW)')\n",
277 | "plt.ylabel(r'Operating cost ($ / MWh)')\n",
278 | "#plt.xlim([0, 1000])\n",
279 | "#plt.ylim([0, max(pmax1, pmax2)])\n",
280 | "plt.savefig(\"three_bus_gen_cost.pdf\")"
281 | ]
282 | },
283 | {
284 | "cell_type": "code",
285 | "execution_count": 24,
286 | "metadata": {},
287 | "outputs": [],
288 | "source": [
289 | "# load1 = FixedLoad(power=50, name=\"load1\")\n",
290 | "# load2 = FixedLoad(power=100, name=\"load2\")\n",
291 | "\n",
292 | "# gen1 = Generator(power_max=1000,\n",
293 | "# alpha=0.01, beta=100, name=\"gen1\")\n",
294 | "# gen2 = Generator(power_max=100, alpha=0.1,\n",
295 | "# beta=0.1, name=\"gen2\")\n",
296 | "\n",
297 | "# line1 = TransmissionLine(power_max=50, name = 'line1', alpha=0.001)\n",
298 | "# #line1 = TransmissionLine(name = 'line1', alpha=0.001)\n",
299 | "# line2 = TransmissionLine(power_max=10, name = 'line2')\n",
300 | "# line3 = TransmissionLine(power_max=50, name = 'line3')\n",
301 | "\n",
302 | "# net1 = Net([load1.terminals[0],\n",
303 | "# gen1.terminals[0], line1.terminals[0],\n",
304 | "# line2.terminals[0]], name = 'net1')\n",
305 | "# net2 = Net([load2.terminals[0], line1.terminals[1], \n",
306 | "# line3.terminals[0]], name = 'net2')\n",
307 | "# net3 = Net([gen2.terminals[0], line2.terminals[1], \n",
308 | "# line3.terminals[1]], name = 'net3')\n",
309 | "# network = Group([load1, load2, gen1, gen2,\n",
310 | "# line1, line2, line3],\n",
311 | "# [net1, net2, net3])\n",
312 | "\n",
313 | "# network.init_problem()\n",
314 | "# network.optimize()\n",
315 | "# print(network.results.summary())"
316 | ]
317 | },
318 | {
319 | "cell_type": "code",
320 | "execution_count": null,
321 | "metadata": {},
322 | "outputs": [],
323 | "source": []
324 | }
325 | ],
326 | "metadata": {
327 | "kernelspec": {
328 | "display_name": "Python 3",
329 | "language": "python",
330 | "name": "python3"
331 | },
332 | "language_info": {
333 | "codemirror_mode": {
334 | "name": "ipython",
335 | "version": 3
336 | },
337 | "file_extension": ".py",
338 | "mimetype": "text/x-python",
339 | "name": "python",
340 | "nbconvert_exporter": "python",
341 | "pygments_lexer": "ipython3",
342 | "version": "3.6.4"
343 | }
344 | },
345 | "nbformat": 4,
346 | "nbformat_minor": 2
347 | }
348 |
--------------------------------------------------------------------------------
/examples/gen_bid_curve.py:
--------------------------------------------------------------------------------
1 | """An example of generator with cost function specified by bid curve."""
2 |
3 | import cvxpy as cvx
4 | from dem import *
5 |
6 |
7 | class GeneratorWithBidCurve(Generator):
8 | """Generators with cost function specified by bid curve.
9 |
10 | Cost function will have the form:
11 |
12 | max(cost0, a_1*p + b_1, ..., a_N*p + b_N),
13 |
14 | where a_i is the price for bid curve segment i and b_i is the appropriate
15 | offset, depending on previous segments.
16 | """
17 |
18 | def __init__(self, no_load_cost=0, bid_curve=[], name=None):
19 | super(GeneratorWithBidCurve, self).__init__(name=name)
20 | self.no_load_cost = no_load_cost
21 | self.bid_curve = bid_curve
22 |
23 | @property
24 | def cost(self):
25 | p = -self.terminals[0].power_var
26 |
27 | segments = [cvx.Constant(self.no_load_cost)]
28 | prev_power = None
29 | for power, price in self.bid_curve[1:]:
30 | if prev_power is None:
31 | offset = self.no_load_cost
32 | else:
33 | offset += (power - prev_power) * prev_price
34 | segments.append(price * (p - power) + offset)
35 | prev_power = power
36 | prev_price = price
37 |
38 | return cvx.max_elemwise(*segments)
39 |
40 |
41 | gen = GeneratorWithBidCurve(
42 | no_load_cost=290.42,
43 | bid_curve=[
44 | (30, 21.21),
45 | (33.1, 21.43),
46 | (36.2, 21.66),
47 | (39.4, 21.93),
48 | (42.5, 22.22),
49 | (45.6, 22.53),
50 | (48.8, 22.87),
51 | (51.9, 23.22),
52 | (55, 23.6),
53 | ],
54 | )
55 | load = FixedLoad(power=43)
56 | net = Net([gen.terminals[0], load.terminals[0]])
57 |
58 | network = Group([gen, load], [net])
59 | print(network.optimize())
60 |
--------------------------------------------------------------------------------
/examples/ieee14cdf.txt:
--------------------------------------------------------------------------------
1 | 08/19/93 UW ARCHIVE 100.0 1962 W IEEE 14 Bus Test Case
2 | BUS DATA FOLLOWS 14 ITEMS
3 | 1 Bus 1 HV 1 1 3 1.060 0.0 0.0 0.0 232.4 -16.9 0.0 1.060 0.0 0.0 0.0 0.0 0
4 | 2 Bus 2 HV 1 1 2 1.045 -4.98 21.7 12.7 40.0 42.4 0.0 1.045 50.0 -40.0 0.0 0.0 0
5 | 3 Bus 3 HV 1 1 2 1.010 -12.72 94.2 19.0 0.0 23.4 0.0 1.010 40.0 0.0 0.0 0.0 0
6 | 4 Bus 4 HV 1 1 0 1.019 -10.33 47.8 -3.9 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0
7 | 5 Bus 5 HV 1 1 0 1.020 -8.78 7.6 1.6 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0
8 | 6 Bus 6 LV 1 1 2 1.070 -14.22 11.2 7.5 0.0 12.2 0.0 1.070 24.0 -6.0 0.0 0.0 0
9 | 7 Bus 7 ZV 1 1 0 1.062 -13.37 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0
10 | 8 Bus 8 TV 1 1 2 1.090 -13.36 0.0 0.0 0.0 17.4 0.0 1.090 24.0 -6.0 0.0 0.0 0
11 | 9 Bus 9 LV 1 1 0 1.056 -14.94 29.5 16.6 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.19 0
12 | 10 Bus 10 LV 1 1 0 1.051 -15.10 9.0 5.8 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0
13 | 11 Bus 11 LV 1 1 0 1.057 -14.79 3.5 1.8 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0
14 | 12 Bus 12 LV 1 1 0 1.055 -15.07 6.1 1.6 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0
15 | 13 Bus 13 LV 1 1 0 1.050 -15.16 13.5 5.8 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0
16 | 14 Bus 14 LV 1 1 0 1.036 -16.04 14.9 5.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0
17 | -999
18 | BRANCH DATA FOLLOWS 20 ITEMS
19 | 1 2 1 1 1 0 0.01938 0.05917 0.0528 0 0 0 0 0 0.0 0.0 0.0 0.0 0.0 0.0 0.0
20 | 1 5 1 1 1 0 0.05403 0.22304 0.0492 0 0 0 0 0 0.0 0.0 0.0 0.0 0.0 0.0 0.0
21 | 2 3 1 1 1 0 0.04699 0.19797 0.0438 0 0 0 0 0 0.0 0.0 0.0 0.0 0.0 0.0 0.0
22 | 2 4 1 1 1 0 0.05811 0.17632 0.0340 0 0 0 0 0 0.0 0.0 0.0 0.0 0.0 0.0 0.0
23 | 2 5 1 1 1 0 0.05695 0.17388 0.0346 0 0 0 0 0 0.0 0.0 0.0 0.0 0.0 0.0 0.0
24 | 3 4 1 1 1 0 0.06701 0.17103 0.0128 0 0 0 0 0 0.0 0.0 0.0 0.0 0.0 0.0 0.0
25 | 4 5 1 1 1 0 0.01335 0.04211 0.0 0 0 0 0 0 0.0 0.0 0.0 0.0 0.0 0.0 0.0
26 | 4 7 1 1 1 0 0.0 0.20912 0.0 0 0 0 0 0 0.978 0.0 0.0 0.0 0.0 0.0 0.0
27 | 4 9 1 1 1 0 0.0 0.55618 0.0 0 0 0 0 0 0.969 0.0 0.0 0.0 0.0 0.0 0.0
28 | 5 6 1 1 1 0 0.0 0.25202 0.0 0 0 0 0 0 0.932 0.0 0.0 0.0 0.0 0.0 0.0
29 | 6 11 1 1 1 0 0.09498 0.19890 0.0 0 0 0 0 0 0.0 0.0 0.0 0.0 0.0 0.0 0.0
30 | 6 12 1 1 1 0 0.12291 0.25581 0.0 0 0 0 0 0 0.0 0.0 0.0 0.0 0.0 0.0 0.0
31 | 6 13 1 1 1 0 0.06615 0.13027 0.0 0 0 0 0 0 0.0 0.0 0.0 0.0 0.0 0.0 0.0
32 | 7 8 1 1 1 0 0.0 0.17615 0.0 0 0 0 0 0 0.0 0.0 0.0 0.0 0.0 0.0 0.0
33 | 7 9 1 1 1 0 0.0 0.11001 0.0 0 0 0 0 0 0.0 0.0 0.0 0.0 0.0 0.0 0.0
34 | 9 10 1 1 1 0 0.03181 0.08450 0.0 0 0 0 0 0 0.0 0.0 0.0 0.0 0.0 0.0 0.0
35 | 9 14 1 1 1 0 0.12711 0.27038 0.0 0 0 0 0 0 0.0 0.0 0.0 0.0 0.0 0.0 0.0
36 | 10 11 1 1 1 0 0.08205 0.19207 0.0 0 0 0 0 0 0.0 0.0 0.0 0.0 0.0 0.0 0.0
37 | 12 13 1 1 1 0 0.22092 0.19988 0.0 0 0 0 0 0 0.0 0.0 0.0 0.0 0.0 0.0 0.0
38 | 13 14 1 1 1 0 0.17093 0.34802 0.0 0 0 0 0 0 0.0 0.0 0.0 0.0 0.0 0.0 0.0
39 | -999
40 | LOSS ZONES FOLLOWS 1 ITEMS
41 | 1 IEEE 14 BUS
42 | -99
43 | INTERCHANGE DATA FOLLOWS 1 ITEMS
44 | 1 2 Bus 2 HV 0.0 999.99 IEEE14 IEEE 14 Bus Test Case
45 | -9
46 | TIE LINES FOLLOWS 0 ITEMS
47 | -999
48 | END OF DATA
49 |
--------------------------------------------------------------------------------
/examples/p_wind.pickle:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cvxgrp/cvxpower/11c48a8d41eff897c05e716903f44908a77e6759/examples/p_wind.pickle
--------------------------------------------------------------------------------
/examples/residual_params.pickle:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cvxgrp/cvxpower/11c48a8d41eff897c05e716903f44908a77e6759/examples/residual_params.pickle
--------------------------------------------------------------------------------
/examples/sigma_epsilon.pickle:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cvxgrp/cvxpower/11c48a8d41eff897c05e716903f44908a77e6759/examples/sigma_epsilon.pickle
--------------------------------------------------------------------------------
/examples/static.ipynb:
--------------------------------------------------------------------------------
1 | {
2 | "cells": [
3 | {
4 | "cell_type": "markdown",
5 | "metadata": {},
6 | "source": [
7 | "# Static power flow"
8 | ]
9 | },
10 | {
11 | "cell_type": "code",
12 | "execution_count": 8,
13 | "metadata": {},
14 | "outputs": [],
15 | "source": [
16 | "%matplotlib inline\n",
17 | "import matplotlib.pyplot as plt\n",
18 | "import matplotlib\n",
19 | "import numpy as np\n",
20 | "\n",
21 | "from cvxpower import *\n",
22 | "\n",
23 | "matplotlib.rc(\"figure\", figsize=(7,7))\n",
24 | "matplotlib.rc(\"lines\", linewidth=2)"
25 | ]
26 | },
27 | {
28 | "cell_type": "markdown",
29 | "metadata": {},
30 | "source": [
31 | "## Basic examples"
32 | ]
33 | },
34 | {
35 | "cell_type": "markdown",
36 | "metadata": {},
37 | "source": [
38 | "### Hello world"
39 | ]
40 | },
41 | {
42 | "cell_type": "code",
43 | "execution_count": 2,
44 | "metadata": {},
45 | "outputs": [
46 | {
47 | "name": "stdout",
48 | "output_type": "stream",
49 | "text": [
50 | "Status: optimal\n",
51 | "Terminal Power\n",
52 | "-------- -----\n",
53 | "FixedLoad[0] 100.00\n",
54 | "Generator[0] -100.00\n",
55 | "\n",
56 | "Net Price\n",
57 | "--- -----\n",
58 | "Net 102.0000\n",
59 | "\n",
60 | "Device Payment\n",
61 | "------ -------\n",
62 | "FixedLoad 10200.00\n",
63 | "Generator -10200.00\n",
64 | "\n",
65 | "Power and price are averages over the time horizon. Payment is total.\n",
66 | "\n"
67 | ]
68 | }
69 | ],
70 | "source": [
71 | "load = FixedLoad(power=100)\n",
72 | "gen = Generator(power_max=1000, alpha=0.01, beta=100)\n",
73 | "net = Net([load.terminals[0], gen.terminals[0]])\n",
74 | "network = Group([load, gen], [net])\n",
75 | "\n",
76 | "network.init_problem()\n",
77 | "network.problem.solve()\n",
78 | "print(network.results.summary())"
79 | ]
80 | },
81 | {
82 | "cell_type": "markdown",
83 | "metadata": {},
84 | "source": [
85 | "### Curtailable load"
86 | ]
87 | },
88 | {
89 | "cell_type": "code",
90 | "execution_count": 3,
91 | "metadata": {},
92 | "outputs": [
93 | {
94 | "name": "stdout",
95 | "output_type": "stream",
96 | "text": [
97 | "Status: optimal\n",
98 | "Terminal Power\n",
99 | "-------- -----\n",
100 | "CurtailableLoad[0] 25.00\n",
101 | "Generator[0] -25.00\n",
102 | "\n",
103 | "Net Price\n",
104 | "--- -----\n",
105 | "Net 150.0000\n",
106 | "\n",
107 | "Device Payment\n",
108 | "------ -------\n",
109 | "CurtailableLoad 3750.00\n",
110 | "Generator -3750.00\n",
111 | "\n",
112 | "Power and price are averages over the time horizon. Payment is total.\n",
113 | "\n"
114 | ]
115 | }
116 | ],
117 | "source": [
118 | "load = CurtailableLoad(power=1000, alpha=150)\n",
119 | "gen = Generator(power_max=1000, alpha=1, beta=100)\n",
120 | "net = Net([load.terminals[0], gen.terminals[0]])\n",
121 | "network = Group([load, gen], [net])\n",
122 | "\n",
123 | "network.init_problem()\n",
124 | "network.problem.solve()\n",
125 | "print(network.results.summary())"
126 | ]
127 | },
128 | {
129 | "cell_type": "markdown",
130 | "metadata": {},
131 | "source": [
132 | "### Two generators, transmission line"
133 | ]
134 | },
135 | {
136 | "cell_type": "code",
137 | "execution_count": 4,
138 | "metadata": {},
139 | "outputs": [
140 | {
141 | "name": "stdout",
142 | "output_type": "stream",
143 | "text": [
144 | "Status: optimal\n",
145 | "Terminal Power\n",
146 | "-------- -----\n",
147 | "FixedLoad[0] 100.00\n",
148 | "Gen1[0] -50.00\n",
149 | "Gen2[0] -50.00\n",
150 | "TransmissionLine[0] -50.00\n",
151 | "TransmissionLine[1] 50.00\n",
152 | "\n",
153 | "Net Price\n",
154 | "--- -----\n",
155 | "Net 101.0000\n",
156 | "Net 10.1000\n",
157 | "\n",
158 | "Device Payment\n",
159 | "------ -------\n",
160 | "FixedLoad 10100.00\n",
161 | "Gen1 -5050.00\n",
162 | "Gen2 -505.00\n",
163 | "TransmissionLine -4545.00\n",
164 | "\n",
165 | "Power and price are averages over the time horizon. Payment is total.\n",
166 | "\n"
167 | ]
168 | }
169 | ],
170 | "source": [
171 | "load = FixedLoad(power=100)\n",
172 | "gen1 = Generator(power_max=1000, alpha=0.01, beta=100, name=\"Gen1\")\n",
173 | "gen2 = Generator(power_max=100, alpha=0.1, beta=0.1, name=\"Gen2\")\n",
174 | "line = TransmissionLine(power_max=50)\n",
175 | "\n",
176 | "net1 = Net([load.terminals[0], gen1.terminals[0], line.terminals[0]])\n",
177 | "net2 = Net([gen2.terminals[0], line.terminals[1]])\n",
178 | "network = Group([load, gen1, gen2, line], [net1, net2])\n",
179 | "\n",
180 | "network.init_problem()\n",
181 | "network.problem.solve()\n",
182 | "print(network.results.summary())"
183 | ]
184 | },
185 | {
186 | "cell_type": "markdown",
187 | "metadata": {},
188 | "source": [
189 | "### Three buses\n",
190 | "\n",
191 | "Figure 2.1 from Kraning, et al. without the battery.\n",
192 | "\n",
193 | "\n",
194 | "\n"
195 | ]
196 | },
197 | {
198 | "cell_type": "code",
199 | "execution_count": 5,
200 | "metadata": {},
201 | "outputs": [
202 | {
203 | "name": "stdout",
204 | "output_type": "stream",
205 | "text": [
206 | "Status: optimal\n",
207 | "Terminal Power\n",
208 | "-------- -----\n",
209 | "Load1[0] 50.00\n",
210 | "Load2[0] 100.00\n",
211 | "Gen1[0] -90.00\n",
212 | "Gen2[0] -60.00\n",
213 | "TransmissionLine[0] 50.00\n",
214 | "TransmissionLine[1] -50.00\n",
215 | "TransmissionLine[0] -10.00\n",
216 | "TransmissionLine[1] 10.00\n",
217 | "TransmissionLine[0] -50.00\n",
218 | "TransmissionLine[1] 50.00\n",
219 | "\n",
220 | "Net Price\n",
221 | "--- -----\n",
222 | "Net 101.8001\n",
223 | "Net 926.6617\n",
224 | "Net 12.1000\n",
225 | "\n",
226 | "Device Payment\n",
227 | "------ -------\n",
228 | "Load1 5090.00\n",
229 | "Load2 92666.17\n",
230 | "Gen1 -9161.98\n",
231 | "Gen2 -726.00\n",
232 | "TransmissionLine -41243.11\n",
233 | "TransmissionLine -897.03\n",
234 | "TransmissionLine -45728.06\n",
235 | "\n",
236 | "Power and price are averages over the time horizon. Payment is total.\n",
237 | "\n"
238 | ]
239 | }
240 | ],
241 | "source": [
242 | "load1 = FixedLoad(power=50, name=\"Load1\")\n",
243 | "load2 = FixedLoad(power=100, name=\"Load2\")\n",
244 | "gen1 = Generator(power_max=1000, alpha=0.01, beta=100, name=\"Gen1\")\n",
245 | "gen2 = Generator(power_max=100, alpha=0.1, beta=0.1, name=\"Gen2\")\n",
246 | "line1 = TransmissionLine(power_max=50)\n",
247 | "line2 = TransmissionLine(power_max=10)\n",
248 | "line3 = TransmissionLine(power_max=50)\n",
249 | "\n",
250 | "net1 = Net([load1.terminals[0], gen1.terminals[0], line1.terminals[0], line2.terminals[0]])\n",
251 | "net2 = Net([load2.terminals[0], line1.terminals[1], line3.terminals[0]])\n",
252 | "net3 = Net([gen2.terminals[0], line2.terminals[1], line3.terminals[1]])\n",
253 | "network = Group([load1, load2, gen1, gen2, line1, line2, line3], [net1, net2, net3])\n",
254 | "\n",
255 | "network.init_problem()\n",
256 | "network.problem.solve()\n",
257 | "print(network.results.summary())"
258 | ]
259 | },
260 | {
261 | "cell_type": "markdown",
262 | "metadata": {},
263 | "source": [
264 | "## Grouping devices\n",
265 | "\n",
266 | "We can wrap up several devices and nets into a single device"
267 | ]
268 | },
269 | {
270 | "cell_type": "code",
271 | "execution_count": 6,
272 | "metadata": {},
273 | "outputs": [
274 | {
275 | "name": "stdout",
276 | "output_type": "stream",
277 | "text": [
278 | "Status: optimal\n",
279 | "Terminal Power\n",
280 | "-------- -----\n",
281 | "Solar[0] -10.00\n",
282 | "FixedLoad[0] 13.00\n",
283 | "TransmissionLine[0] -3.00\n",
284 | "TransmissionLine[1] 3.00\n",
285 | "Grid[0] -3.00\n",
286 | "\n",
287 | "Net Price\n",
288 | "--- -----\n",
289 | "Net 100.2993\n",
290 | "Meter 100.2998\n",
291 | "\n",
292 | "Device Payment\n",
293 | "------ -------\n",
294 | "Solar -1003.00\n",
295 | "FixedLoad 1303.89\n",
296 | "TransmissionLine 0.00\n",
297 | "Grid -300.90\n",
298 | "\n",
299 | "Power and price are averages over the time horizon. Payment is total.\n",
300 | "\n"
301 | ]
302 | }
303 | ],
304 | "source": [
305 | "solar = Generator(power_max=10, alpha=0, beta=0, name=\"Solar\")\n",
306 | "load = FixedLoad(power=13)\n",
307 | "line = TransmissionLine(power_max=25)\n",
308 | "net = Net([load.terminals[0], solar.terminals[0], line.terminals[0]])\n",
309 | "home = Group([solar, load, line], [net], [line.terminals[1]], name=\"Home\")\n",
310 | "\n",
311 | "grid = Generator(power_max=1e6, alpha=0.05, beta=100, name=\"Grid\")\n",
312 | "meter = Net([line.terminals[1], grid.terminals[0]], name=\"Meter\")\n",
313 | "network = Group([home, grid], [meter])\n",
314 | "\n",
315 | "network.init_problem()\n",
316 | "network.problem.solve()\n",
317 | "print(network.results.summary())"
318 | ]
319 | },
320 | {
321 | "cell_type": "markdown",
322 | "metadata": {},
323 | "source": [
324 | "## Varying parameters\n",
325 | "\n",
326 | "We can modify a single parameter and reoptimize, which is useful for sweeping over a parameter range."
327 | ]
328 | },
329 | {
330 | "cell_type": "code",
331 | "execution_count": 7,
332 | "metadata": {},
333 | "outputs": [
334 | {
335 | "data": {
336 | "text/plain": [
337 | ""
338 | ]
339 | },
340 | "execution_count": 7,
341 | "metadata": {},
342 | "output_type": "execute_result"
343 | },
344 | {
345 | "data": {
346 | "image/png": "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\n",
347 | "text/plain": [
348 | ""
349 | ]
350 | },
351 | "metadata": {
352 | "needs_background": "light"
353 | },
354 | "output_type": "display_data"
355 | }
356 | ],
357 | "source": [
358 | "load1 = FixedLoad(power=50, name=\"Load1\")\n",
359 | "load2 = FixedLoad(power=100, name=\"Load2\")\n",
360 | "gen1 = Generator(power_max=100, alpha=1, beta=10, name=\"Gen1\")\n",
361 | "gen2 = Generator(power_max=1000, alpha=0.01, beta=0, name=\"Gen2\")\n",
362 | "line1 = TransmissionLine(power_max=100)\n",
363 | "line2 = TransmissionLine(power_max=10)\n",
364 | "line3 = TransmissionLine(power_max=Parameter(1))\n",
365 | "\n",
366 | "net1 = Net([load1.terminals[0], gen1.terminals[0], line1.terminals[0], line2.terminals[0]])\n",
367 | "net2 = Net([load2.terminals[0], line1.terminals[1], line3.terminals[0]])\n",
368 | "net3 = Net([gen2.terminals[0], line2.terminals[1], line3.terminals[1]])\n",
369 | "network = Group([load1, load2, gen1, gen2, line1, line2, line3], [net1, net2, net3])\n",
370 | "\n",
371 | "network.init_problem()\n",
372 | "xs = np.linspace(0, 150, 100)\n",
373 | "prices = np.empty((len(xs), 3))\n",
374 | "for i, x in enumerate(xs):\n",
375 | " line3.power_max.value = [x]\n",
376 | " network.problem.solve()\n",
377 | " prices[i,:] = [net.price for net in network.nets]\n",
378 | " \n",
379 | "plt.plot(xs, prices)\n",
380 | "plt.xlabel(\"Line capacity\")\n",
381 | "plt.ylabel(\"Price\")\n",
382 | "plt.legend([\"Bus 1\", \"Bus 2\", \"Bus 3\"])"
383 | ]
384 | },
385 | {
386 | "cell_type": "code",
387 | "execution_count": null,
388 | "metadata": {},
389 | "outputs": [],
390 | "source": []
391 | }
392 | ],
393 | "metadata": {
394 | "kernelspec": {
395 | "display_name": "Python 3",
396 | "language": "python",
397 | "name": "python3"
398 | },
399 | "language_info": {
400 | "codemirror_mode": {
401 | "name": "ipython",
402 | "version": 3
403 | },
404 | "file_extension": ".py",
405 | "mimetype": "text/x-python",
406 | "name": "python",
407 | "nbconvert_exporter": "python",
408 | "pygments_lexer": "ipython3",
409 | "version": "3.6.7"
410 | }
411 | },
412 | "nbformat": 4,
413 | "nbformat_minor": 1
414 | }
415 |
--------------------------------------------------------------------------------
/examples/three_bus.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cvxgrp/cvxpower/11c48a8d41eff897c05e716903f44908a77e6759/examples/three_bus.png
--------------------------------------------------------------------------------
/examples/wind_baseline.pickle:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cvxgrp/cvxpower/11c48a8d41eff897c05e716903f44908a77e6759/examples/wind_baseline.pickle
--------------------------------------------------------------------------------
/examples/wind_power_test.pickle:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cvxgrp/cvxpower/11c48a8d41eff897c05e716903f44908a77e6759/examples/wind_power_test.pickle
--------------------------------------------------------------------------------
/examples/wind_power_train.pickle:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cvxgrp/cvxpower/11c48a8d41eff897c05e716903f44908a77e6759/examples/wind_power_train.pickle
--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
1 | from setuptools import setup, find_packages
2 |
3 | setup(
4 | name="cvxpower",
5 | version="0.1.2",
6 | url="http://github.com/cvxgrp/cvxpower",
7 | packages=find_packages(),
8 | license="Apache",
9 | install_requires=["cvxpy>=1.0.6", "tqdm",],
10 | test_suite="cvxpower",
11 | description="Power Network Optimization and Simulation.",
12 | )
13 |
--------------------------------------------------------------------------------
/tools/generate_notebooks.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash -eu
2 |
3 | notebooks=$PWD/notebooks/*.ipynb
4 | output=docs/notebooks
5 |
6 | cd $output
7 | for nb in $notebooks; do
8 | jupyter nbconvert $nb --to rst
9 | done
10 |
--------------------------------------------------------------------------------
/tools/push_docker.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash -eu
2 |
3 | tag=cvxgrp/dem
4 |
5 | docker build -t $tag .
6 | docker push $tag
7 |
--------------------------------------------------------------------------------
/tools/run_ci.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash -eux
2 | #
3 | # Script for running continuous integration tests
4 |
5 | cd $(dirname "${BASH_SOURCE[0]}")/..
6 |
7 | # Run Python 2.7 tests
8 | python2 setup.py test
9 |
10 | # Run Python 3.4 tests
11 | python3 setup.py test
12 |
--------------------------------------------------------------------------------