Skip to content

Commit 8792ef4

Browse files
Remove flag to include battery charge & discharge binary variables (#45)
* feat: remove flag to include battery charge / discharge binary variables * test: fix up changed battery dispatch behaviour * test: make hypothesis tests quiet * test: increase timeout on hypothesis testing * test: increase hypothesis examples * test: fix battery charge test by clipping battery values to zero * test: test battery check helper * test: separate out distance between import export price tests * test: test updates, documentation & refactors based on self review * test: fix test name * refactor: move optimizer config into asset.optimize * docs: changelog updates * test: increase timeout for hypothesis testing * test: add timeout onto hypothesis tests in battery * docs: fix readme line numbers in mkdocs index
1 parent fe2ab76 commit 8792ef4

File tree

21 files changed

+240
-146
lines changed

21 files changed

+240
-146
lines changed

README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,8 @@ asset = epl.Battery(
4242
power_mw=2,
4343
capacity_mwh=4,
4444
efficiency_pct=0.9,
45-
electricity_prices=[100.0, 50, 200, -100, 0, 200, 100, -100]
45+
electricity_prices=[100.0, 50, 200, -100, 0, 200, 100, -100],
46+
export_electricity_prices=40
4647
)
4748

4849
simulation = asset.optimize()

docs/docs/changelog.md

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,47 @@
11
# Changelog
22

3+
## [1.1.0](https://github.com/ADGEfficiency/energy-py-linear/releases/tag/v1.1.0)
4+
5+
### Export Electricity Prices
6+
7+
Assets can now accept export electricity prices - these are an optional time series that can either be a constant value or interval data:
8+
9+
```python
10+
asset = epl.Battery(
11+
electricity_prices=[100.0, 50, 200, -100, 0, 200, 100, -100],
12+
export_electricity_prices=40
13+
)
14+
```
15+
16+
These export electricity prices are used to calculate the value of electricity exported from site.
17+
18+
### Optimizer Config
19+
20+
The `.optimize()` method of assets now accepts an `epl.OptimizerConfig` object, which allows configuration of the CBC optimizer used by Pulp:
21+
22+
```python
23+
asset.optimize(
24+
optimizer_config=epl.OptimizerConfig(timeout=60, relative_tolerance=0.05)
25+
)
26+
```
27+
28+
### Bugs
29+
30+
Fixed a bug on the `allow_infeasible` flag in `epl.Site.optimize`.
31+
32+
Fixed a bug on the `export_limit_mw` in `epl.Site.__init__`.
33+
34+
#### Netting Off Battery Charge and Discharge
35+
36+
`energypylinear` has the ability to constrain battery charge or discharge into a single interval, using binary variables that are linked to the charge and discharge energy.
37+
38+
By default these were turned off, because it slows down the optimization. The effect on the site electricity balance was zero, as the charge and discharge energy were netted off in the balance.
39+
40+
However, as the battery losses are a percentage of battery charge, this led to situations where when electricity prices were negative, the optimizer would be incentivized to have a large simultaneous charge and discharge. This would also lead to the situation where the losses calculations were correct as a percentage of battery charge, but not of battery net charge.
41+
42+
The solution is to remove the flag that allowed toggling of these binary variables on and off - this now means that the battery model always runs with binary variables limiting only one of charge or discharge to occur in a single interval.
43+
44+
345
## [1.0.0](https://github.com/ADGEfficiency/energy-py-linear/releases/tag/v1.0.0)
446

547
### Add Renewable Generator Asset

docs/docs/how-to/dispatch-forecast.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ print(f"{perfect_foresight=}")
2828
```
2929

3030
```
31-
perfect_foresight=<Accounts profit=1057.78 emissions=0.0822>
31+
perfect_foresight=<Accounts profit=1037.78 emissions=0.0622>
3232
```
3333

3434
## Optimize to a Forecast
@@ -65,7 +65,7 @@ print(f"{variance=}")
6565
```
6666

6767
```
68-
variance=<Account profit=1197.78 emissions=0.0022>
68+
variance=<Account profit=1177.78 emissions=-0.0178>
6969
```
7070

7171
## Full Example

docs/docs/how-to/price-carbon.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ print(f"{price_account=}")
6262
```
6363

6464
```
65-
price_account=<Accounts profit=1057.78 emissions=-1.6558>
65+
price_account=<Accounts profit=1037.78 emissions=-1.6578>
6666
```
6767

6868
## Calculate Variance Between Accounts
@@ -75,6 +75,6 @@ print(f"{-variance.cost / variance.emissions:.2f} $/tC")
7575
```
7676

7777
```
78-
variance=<Account profit=923.33 emissions=0.6176>
79-
1495.14 $/tC
78+
variance=<Account profit=903.33 emissions=0.6156>
79+
1467.51 $/tC
8080
```

docs/docs/index.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
# energy-py-linear
22

3-
{!../../README.md!lines=11-93}
3+
{!../../README.md!lines=11-94}

docs/docs/static/battery-fast.png

-985 Bytes
Loading

docs/docs/static/battery.png

-76 Bytes
Loading

docs/docs/validation/battery.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ Now let's change the prices and see how the dispatch changes:
3737
import energypylinear as epl
3838

3939
asset = epl.Battery(
40-
electricity_prices=[200, -50, -50, 200, 200],
40+
electricity_prices=[200, -50, -50, 200, 220],
4141
)
4242
simulation = asset.optimize(verbose=False)
4343
print(simulation.results[["site-electricity_prices", "site-electricity_balance_mwh"]])
@@ -48,8 +48,8 @@ print(simulation.results[["site-electricity_prices", "site-electricity_balance_m
4848
0 200 0.0
4949
1 -50 2.0
5050
2 -50 2.0
51-
3 200 -2.0
52-
4 200 -1.6
51+
3 200 -1.6
52+
4 220 -2.0
5353
```
5454

5555
As expected, the battery continues to charge during low electricity price intervals, and discharge when electricity prices are high.

docs/generate-plots.py

Lines changed: 45 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -29,63 +29,57 @@ def test_battery_performance() -> None:
2929
num_trials = 15
3030

3131
run_times = collections.defaultdict(list)
32-
for flag in [True, False]:
33-
for idx_length in idx_lengths:
34-
trial_times = collections.defaultdict(list)
35-
36-
for n_trial in range(num_trials):
37-
print(f"idx_length: {idx_length} trial {n_trial}")
38-
st = time.perf_counter()
39-
40-
ds = {"electricity_prices": np.random.uniform(-1000, 1000, idx_length)}
41-
asset = epl.Battery(
42-
power_mw=2,
43-
capacity_mwh=4,
44-
efficiency_pct=0.9,
45-
electricity_prices=ds["electricity_prices"],
46-
)
47-
48-
asset.optimize(
49-
verbose=False,
50-
flags=Flags(
51-
allow_evs_discharge=True,
52-
fail_on_spill_asset_use=True,
53-
allow_infeasible=False,
54-
include_charge_discharge_binary_variables=flag,
55-
),
56-
)
57-
58-
trial_times["time"].append(time.perf_counter() - st)
59-
60-
run_times["time"].append(
61-
{
62-
"mean": statistics.mean(trial_times["time"]),
63-
"std": statistics.stdev(trial_times["time"]),
64-
"flag": flag,
65-
"idx_length": idx_length,
66-
}
32+
for idx_length in idx_lengths:
33+
trial_times = collections.defaultdict(list)
34+
35+
for n_trial in range(num_trials):
36+
print(f"idx_length: {idx_length} trial {n_trial}")
37+
st = time.perf_counter()
38+
39+
ds = {"electricity_prices": np.random.uniform(-1000, 1000, idx_length)}
40+
asset = epl.Battery(
41+
power_mw=2,
42+
capacity_mwh=4,
43+
efficiency_pct=0.9,
44+
electricity_prices=ds["electricity_prices"],
6745
)
68-
print(run_times["time"])
46+
47+
asset.optimize(
48+
verbose=False,
49+
flags=Flags(
50+
allow_evs_discharge=True,
51+
fail_on_spill_asset_use=True,
52+
allow_infeasible=False,
53+
),
54+
)
55+
56+
trial_times["time"].append(time.perf_counter() - st)
57+
58+
run_times["time"].append(
59+
{
60+
"mean": statistics.mean(trial_times["time"]),
61+
"std": statistics.stdev(trial_times["time"]),
62+
"idx_length": idx_length,
63+
}
64+
)
65+
print(run_times["time"])
6966

7067
fig, axes = plt.subplots(nrows=1, sharex=True)
7168
print("[red]final run times:[/]")
7269
print(run_times)
73-
for flag in [True, False]:
7470

75-
subset: list = [p for p in run_times["time"] if p["flag"] == flag]
76-
axes.plot(
77-
[p["idx_length"] for p in subset],
78-
[p["mean"] for p in subset],
79-
marker="o",
80-
label=f"include_charge_discharge_binary_variables={flag}",
81-
)
82-
axes.set_title(asset.__repr__())
83-
axes.set_ylabel("Run Time (seconds)")
84-
axes.legend()
85-
axes.grid(True)
86-
plt.xlabel("Index Length")
87-
plt.tight_layout()
88-
fig.savefig("./docs/docs/static/battery-performance.png")
71+
axes.plot(
72+
[p["idx_length"] for p in run_times["time"]],
73+
[p["mean"] for p in run_times["time"]],
74+
marker="o",
75+
)
76+
axes.set_title(asset.__repr__())
77+
axes.set_ylabel("Run Time (seconds)")
78+
axes.legend()
79+
axes.grid(True)
80+
plt.xlabel("Index Length")
81+
plt.tight_layout()
82+
fig.savefig("./docs/docs/static/battery-performance.png")
8983

9084

9185
def test_evs_performance() -> None:

energypylinear/assets/battery.py

Lines changed: 18 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -72,20 +72,19 @@ def constrain_only_charge_or_discharge(
7272
difference to calculate net charge.
7373
"""
7474
assert isinstance(battery, BatteryOneInterval)
75-
if flags.include_charge_discharge_binary_variables:
76-
optimizer.constrain_max(
77-
battery.electric_charge_mwh,
78-
battery.electric_charge_binary,
79-
battery.cfg.capacity_mwh,
80-
)
81-
optimizer.constrain_max(
82-
battery.electric_discharge_mwh,
83-
battery.electric_discharge_binary,
84-
battery.cfg.capacity_mwh,
85-
)
86-
optimizer.constrain(
87-
battery.electric_charge_binary + battery.electric_discharge_binary <= 1
88-
)
75+
optimizer.constrain_max(
76+
battery.electric_charge_mwh,
77+
battery.electric_charge_binary,
78+
battery.cfg.capacity_mwh,
79+
)
80+
optimizer.constrain_max(
81+
battery.electric_discharge_mwh,
82+
battery.electric_discharge_binary,
83+
battery.cfg.capacity_mwh,
84+
)
85+
optimizer.constrain(
86+
battery.electric_charge_binary + battery.electric_discharge_binary <= 1
87+
)
8988

9089

9190
def constrain_battery_electricity_balance(
@@ -167,7 +166,6 @@ def __init__(
167166
initial_charge_mwh: float = 0.0,
168167
final_charge_mwh: float | None = None,
169168
freq_mins: int = defaults.freq_mins,
170-
optimizer_config: "epl.OptimizerConfig" = epl.optimizer.OptimizerConfig(),
171169
):
172170
"""Initializes the asset."""
173171

@@ -193,7 +191,6 @@ def __init__(
193191
export_electricity_prices=export_electricity_prices,
194192
electricity_carbon_intensities=electricity_carbon_intensities,
195193
freq_mins=self.cfg.freq_mins,
196-
optimizer_config=optimizer_config,
197194
)
198195

199196
def __repr__(self) -> str:
@@ -216,14 +213,10 @@ def one_interval(
216213
),
217214
electric_charge_binary=optimizer.binary(
218215
f"{self.cfg.name}-electric_charge_binary-{i}"
219-
)
220-
if flags.include_charge_discharge_binary_variables
221-
else 0,
216+
),
222217
electric_discharge_binary=optimizer.binary(
223218
f"{self.cfg.name}-electric_discharge_binary-{i}"
224-
)
225-
if flags.include_charge_discharge_binary_variables
226-
else 0,
219+
),
227220
electric_loss_mwh=optimizer.continuous(
228221
f"{self.cfg.name}-electric_loss_mwh-{i}"
229222
),
@@ -283,13 +276,15 @@ def optimize(
283276
objective: str = "price",
284277
verbose: bool = True,
285278
flags: Flags = Flags(),
279+
optimizer_config: "epl.OptimizerConfig" = epl.optimizer.OptimizerConfig(),
286280
) -> "epl.SimulationResult":
287281
"""Optimize the asset.
288282
289283
Args:
290284
objective: the optimization objective - either "price" or "carbon".
291285
flags: boolean flags to change simulation and results behaviour.
292286
verbose: level of printing.
287+
optimizer_config: configuration options for the optimizer.
293288
294289
Returns:
295290
epl.results.SimulationResult
@@ -298,6 +293,7 @@ def optimize(
298293
objective=objective,
299294
flags=flags,
300295
verbose=verbose,
296+
optimizer_config=optimizer_config,
301297
)
302298

303299
def plot(self, results: "epl.SimulationResult", path: pathlib.Path | str) -> None:

0 commit comments

Comments
 (0)