Skip to content

Commit e3efa3c

Browse files
authored
Merge pull request #4 from chrisyeh96/chris
Update nonlinear sim
2 parents 1ef8a2c + 13ef572 commit e3efa3c

20 files changed

+2714
-74445
lines changed

56_bus_pandapower_check.ipynb

Lines changed: 0 additions & 73134 deletions
This file was deleted.

README.md

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,27 @@
77

88
**California Institute of Technology and UC San Diego**
99

10+
## Getting started
11+
12+
### Install packages
13+
14+
1. Install [miniconda3](https://docs.conda.io/en/latest/miniconda.html).
15+
2. Install the `voltctrl` conda environment:
16+
```bash
17+
conda env update -f env.yml --prune
18+
```
19+
3. Request a Mosek license ([link](https://www.mosek.com/products/academic-licenses/)). Upon receiving the license file (`mosek.lic`) in an email, create a folder `~/mosek` and copy the license file into that folder.
20+
21+
### Running voltage control experiments
22+
23+
TODO
1024

1125
## Data Files (in `/data`)
1226

1327
The original data files were provided by the authors of the following paper:
1428
> Guannan Qu and Na Li. 2020. Optimal Distributed Feedback Voltage Control under Limited Reactive Power. _IEEE Transactions on Power Systems_ 35, 1 (Jan. 2020), 315–331. https://doi.org/10.1109/TPWRS.2019.2931685
1529

16-
The original data files ("orig_data.zip") are attached to the [releases](https://github.com/chrisyeh96/voltctrl/releases/tag/v1.0). These original data files have been processed into the following files, which are the only files relevant for our experiments. See the inspect_matlab_data.ipynb notebook for details.
30+
The original data files ("orig_data.zip") are attached to the [releases](https://github.com/chrisyeh96/voltctrl/releases/tag/v1.0). These original data files have been processed into the following files, which are the main files relevant for our experiments. See the [inspect_matlab_data.ipynb](notebooks/inspect_data.ipynb) notebook for details.
1731

1832
**PV.mat**
1933
- contains single key `'actual_PV_profile'`
@@ -57,6 +71,12 @@ The original data files ("orig_data.zip") are attached to the [releases](https:/
5771
- 'branch': shape [55, 13], type float64
5872
- 'gen': shape [1, 21], type int16
5973

74+
**nonlinear_voltage_baseline.npy**
75+
- float64 array, shape [14421, 56]
76+
- description: balanced AC nonlinear simulation voltages, generated by [nonlinear_no_control.py](nonlinear_no_control.py)
77+
- each column is the voltage of a bus, with column 0 being bus 0 (the substation)
78+
- units: p.u. voltage (multiply by 12 to get kV)
79+
6080
See the attachment in [releases](https://github.com/chrisyeh96/voltctrl/releases/tag/v1.0) for Python `.pkl` files containing the results of running the various algorithms. These Pickle files are read by the various Jupyter notebooks for plotting and analysis.
6181

6282

cbc/base.py

Lines changed: 78 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,14 @@
33

44
from collections.abc import Callable, Sequence
55
import io
6+
from typing import Any
67

78
import cvxpy as cp
89
import numpy as np
910
from tqdm.auto import tqdm
1011

1112
from network_utils import make_pd_and_pos, np_triangle_norm
12-
13-
Constraint = cp.constraints.constraint.Constraint
13+
from utils import solve_prob
1414

1515

1616
def cp_triangle_norm_sq(x: cp.Expression) -> cp.Expression:
@@ -19,12 +19,12 @@ def cp_triangle_norm_sq(x: cp.Expression) -> cp.Expression:
1919

2020
def project_into_X_set(X_init: np.ndarray, var_X: cp.Variable,
2121
log: tqdm | io.TextIOBase | None,
22-
X_set: list[Constraint], X_true: np.ndarray) -> None:
22+
X_set: list[cp.Constraint], X_true: np.ndarray) -> None:
2323
"""Project X_init into 𝒳 if necessary."""
2424
if log is not None:
2525
norm = np_triangle_norm(X_init)
2626
dist = np_triangle_norm(X_init - X_true)
27-
log.write(f'X_init: ||X̂||_△ = {norm:.3f}, ||X̂-X||_△ = {dist:.3f}')
27+
log.write(f'X_init: ‖X̂‖_△ = {norm:.3f}, X̂-X_△ = {dist:.3f}')
2828

2929
var_X.value = X_init # if var_X.is_psd(), this automatically checks that X_init is PSD
3030
total_violation = sum(np.sum(constraint.violation()) for constraint in X_set)
@@ -35,19 +35,14 @@ def project_into_X_set(X_init: np.ndarray, var_X: cp.Variable,
3535
log.write(f'X_init invalid. Violation: {total_violation:.3f}. Projecting into 𝒳.')
3636
obj = cp.Minimize(cp_triangle_norm_sq(X_init - var_X))
3737
prob = cp.Problem(objective=obj, constraints=X_set)
38-
try:
39-
prob.solve(solver=cp.MOSEK)
40-
except cp.error.SolverError as e:
41-
if log is not None:
42-
log.write(str(e))
43-
prob.solve(solver=cp.SCS)
38+
solve_prob(prob, log=log, name='projecting X_init into 𝒳')
4439
make_pd_and_pos(var_X.value)
4540
if log is not None:
4641
total_violation = sum(np.sum(constraint.violation()) for constraint in X_set)
4742
norm = np_triangle_norm(var_X.value)
4843
dist = np_triangle_norm(var_X.value - X_true)
4944
log.write(f'After projection: X_init violation: {total_violation:.3f}.')
50-
log.write(f' ||X̂||_△ = {norm:.3f}, ||X̂-X||_△ = {dist:.3f}')
45+
log.write(f' ‖X̂‖_△ = {norm:.3f}, X̂-X_△ = {dist:.3f}')
5146

5247

5348
class CBCBase:
@@ -76,7 +71,7 @@ class CBCBase:
7671
sel.update(t+1)
7772
"""
7873
def __init__(self, n: int, T: int, X_init: np.ndarray, v: np.ndarray,
79-
gen_X_set: Callable[[cp.Variable], list[Constraint]],
74+
gen_X_set: Callable[[cp.Variable], list[cp.Constraint]],
8075
X_true: np.ndarray,
8176
obs_nodes: Sequence[int] | None = None,
8277
log: tqdm | io.TextIOBase | None = None):
@@ -87,9 +82,9 @@ def __init__(self, n: int, T: int, X_init: np.ndarray, v: np.ndarray,
8782
- X_init: np.array, shape [n, n], initial guess for X matrix, must be
8883
PSD and entry-wise >= 0
8984
- v: np.array, shape [n], initial squared voltage magnitudes
90-
- gen_X_set: function, takes an optimization variable (cp.Variable) and returns
91-
a list of constraints (cp.Constraint) describing the convex, compact
92-
uncertainty set for X
85+
- gen_X_set: function, takes an optimization variable (cp.Variable) and
86+
returns a list of constraints (cp.Constraint) describing the
87+
convex, compact uncertainty set for X
9388
- X_true: np.array, shape [n, n], true X matrix, optional
9489
- obs_nodes: list of int, nodes that we can observe voltages for
9590
- log: object with .write() function, defaults to tqdm
@@ -105,9 +100,10 @@ def __init__(self, n: int, T: int, X_init: np.ndarray, v: np.ndarray,
105100
# history
106101
self.v = np.zeros([T, n]) # v[t] = v(t)
107102
self.v[0] = v
108-
self.delta_v = np.zeros([T-1, n]) # delta_v[t] = v(t+1) - v(t)
103+
self.Δv = np.zeros([T-1, n]) # Δv[t] = v(t+1) - v(t)
109104
self.u = np.zeros([T-1, n]) # u[t] = u(t) = q^c(t+1) - q^c(t)
110105
self.q = np.zeros([T, n]) # q[t] = q^c(t)
106+
self.ts_updated: list[int] = []
111107

112108
# define optimization variables
113109
self.var_X = cp.Variable((n, n), PSD=True)
@@ -136,17 +132,77 @@ def add_obs(self, t: int) -> None:
136132
"""
137133
assert t >= 1
138134
self.u[t-1] = self.q[t] - self.q[t-1]
139-
self.delta_v[t-1] = self.v[t] - self.v[t-1]
135+
self.Δv[t-1] = self.v[t] - self.v[t-1]
140136

141-
def select(self, t: int) -> np.ndarray:
142-
"""
143-
Args
144-
- t: int, current time step
137+
def select(self, t: int) -> Any:
138+
"""Selects a model.
145139
146140
When select() is called, we have seen t observations. That is, we have values for:
147141
v(0), ..., v(t) # recall: v(t) = vs[t]
148142
q^c(0), ..., q^c(t) # recall: q^c(t) = qs[t]
149143
u(0), ..., u(t-1) # recall: u(t) = us[t]
150-
Δv(0), ..., Δv(t-1) # recall: Δv(t) = delta_vs[t]
144+
Δv(0), ..., Δv(t-1) # recall: Δv(t) = Δvs[t]
145+
146+
Args
147+
- t: int, current time step
151148
"""
149+
raise NotImplementedError
150+
151+
152+
class CBCConst(CBCBase):
153+
"""CBC class that always returns the initial X.
154+
155+
Does not actually perform any model chasing.
156+
"""
157+
def select(self, t: int) -> np.ndarray:
152158
return self.X_init
159+
160+
161+
class CBCConstWithNoise(CBCBase):
162+
"""CBC class that always returns the initial X, but learns eta."""
163+
def __init__(self, n: int, T: int, X_init: np.ndarray, v: np.ndarray,
164+
gen_X_set: Callable[[cp.Variable], list[cp.Constraint]],
165+
X_true: np.ndarray,
166+
obs_nodes: Sequence[int] | None = None,
167+
log: tqdm | io.TextIOBase | None = None):
168+
super().__init__(n=n, T=T, X_init=X_init, v=v, gen_X_set=gen_X_set,
169+
X_true=X_true, obs_nodes=obs_nodes, log=log)
170+
self.eta = 0
171+
172+
def _check_newest_obs(self, t: int) -> tuple[bool, str]:
173+
"""Checks whether self.eta satisfies the
174+
newest observation: (v[t], q[t], u[t-1], Δv[t-1])
175+
176+
Returns
177+
- satisfied: bool, whether self.eta satisfies the newest observation
178+
- msg: str, (if not satisfied) describes which constraints are not satisfied,
179+
(if satisfied) is empty string ''
180+
"""
181+
X = self.X_init
182+
183+
obs = self.obs_nodes
184+
ŵ = self.Δv[t-1] - self.u[t-1] @ X
185+
ŵ_norm = np.max(np.abs(ŵ[obs]))
186+
187+
if ŵ_norm > self.eta:
188+
msg = f'‖ŵ(t)‖∞: {ŵ_norm:.3f}'
189+
self.eta = ŵ_norm
190+
return False, msg
191+
else:
192+
return True, ''
193+
194+
def add_obs(self, t: int) -> None:
195+
"""
196+
Args
197+
- t: int, current time step (>=1), v[t] and q[t] have just been updated
198+
"""
199+
# update self.u and self.Δv
200+
super().add_obs(t)
201+
202+
satisfied, msg = self._check_newest_obs(t)
203+
if not satisfied:
204+
self.ts_updated.append(t)
205+
self.log.write(f't = {t:6d}, CBC pre opt: {msg}')
206+
207+
def select(self, t: int) -> tuple[np.ndarray, float]:
208+
return self.X_init, self.eta

0 commit comments

Comments
 (0)