Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Issue 273 add roundtrip efficiency to scheduler #291

Merged
merged 64 commits into from Jan 3, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
64 commits
Select commit Hold shift + click to select a range
8f4b724
Query TimedBelief rather than Power in api v1.3 tests
Flix6x Dec 23, 2021
61e2fd8
Query TimedBelief rather than Power in api v1.3 implementations
Flix6x Dec 23, 2021
07a8059
Query TimedBelief rather than Power in user services tests
Flix6x Dec 23, 2021
0b94664
Query TimedBelief rather than Power in query tests
Flix6x Dec 23, 2021
f3504bd
Query TimedBelief rather than Power in forecasting tests
Flix6x Dec 23, 2021
d196847
Query TimedBelief rather than Power in scheduling tests
Flix6x Dec 23, 2021
9a7f32e
Query TimedBelief rather than Power in api v1 tests
Flix6x Dec 23, 2021
0651806
Simplify data deletion, like, by a lot
Flix6x Dec 23, 2021
923406e
Count ex-ante TimedBeliefs after populating time series forecasts
Flix6x Dec 23, 2021
acfbc8a
Query TimedBelief rather than Price in api v1_1 tests
Flix6x Dec 23, 2021
d892995
Query TimedBelief rather than Power/Price/Weather in Resource.load_se…
Flix6x Dec 23, 2021
70bed39
Query TimedBelief rather than Power/Price/Weather in api v2.0 tests
Flix6x Dec 23, 2021
8289bba
Refactor: simplify duplicate query construction
Flix6x Dec 23, 2021
78ab21d
Add custom join target to get rid of SA warning
Flix6x Dec 24, 2021
109d547
Filter criteria should work for both TimedBeliefs and TimedValues
Flix6x Dec 26, 2021
ff45672
Clarify docstring
Flix6x Dec 26, 2021
47ed019
Query TimedBelief rather than Power in api v1 implementations
Flix6x Dec 26, 2021
4eb0856
Schedules should contain one deterministic belief per event
Flix6x Dec 23, 2021
45f5bb5
Fix type annotation
Flix6x Dec 27, 2021
28041c6
flake8
Flix6x Dec 27, 2021
613f591
Query TimedBelief rather than Price/Weather for analytics
Flix6x Dec 27, 2021
3ca9a8f
Query deterministic TimedBelief rather than Price for planning queries
Flix6x Dec 27, 2021
27dc9ef
Forecast TimedBelief rather than Power/Price/Weather
Flix6x Dec 27, 2021
66e6da0
Schedule TimedBelief rather than Power
Flix6x Dec 27, 2021
fb58ec7
Apparently, to initialize a TimedBelief is to save a TimedBelief, too
Flix6x Dec 27, 2021
b15a445
Create TimedBelief rather than Power/Price/Weather in data generation…
Flix6x Dec 27, 2021
6af5027
Bump timely-beliefs dependency
Flix6x Dec 27, 2021
809c6d0
Fix latest state query
Flix6x Dec 27, 2021
4c94a97
Revert "Apparently, to initialize a TimedBelief is to save a TimedBel…
Flix6x Dec 28, 2021
9f618ee
Prevent saving TimedBelief to session upon updating Sensor or Source
Flix6x Dec 28, 2021
fb5d311
Create only TimedBeliefs in conftests
Flix6x Dec 27, 2021
23e42a1
Use session.add_all calls instead of session.bulk_save_objects or ind…
Flix6x Dec 27, 2021
b942fa0
API directly creates TimedBeliefs
Flix6x Dec 28, 2021
24a785e
CLI uses TimedBeliefs only
Flix6x Dec 28, 2021
d5f181d
Data scripts use TimedBeliefs only
Flix6x Dec 28, 2021
314f700
One more conftest switched to creating TimedBeliefs instead of Weathe…
Flix6x Dec 28, 2021
9ffd449
Expand docstring note on forbidden replacements
Flix6x Dec 30, 2021
9b1fb22
Clarify docstring note on saving changed beliefs only
Flix6x Dec 30, 2021
229e230
Remove redundant flush
Flix6x Dec 30, 2021
fa563f3
Catch forbidden belief replacements with more specific exception
Flix6x Dec 30, 2021
95ccb15
Rename variable
Flix6x Dec 30, 2021
57875e4
One transaction per request
Flix6x Dec 30, 2021
c514cb5
Only enqueue forecasting jobs upon successfully saving new data
Flix6x Dec 30, 2021
dda69a9
Flush instead of commit
Flix6x Dec 30, 2021
2d5f721
Expand test for forbidden data replacement
Flix6x Dec 30, 2021
cd6b8c6
Simplify play mode excemption for replacing beliefs
Flix6x Dec 30, 2021
d7c0512
Add note about potential session rollback
Flix6x Dec 30, 2021
bb7171a
Typo
Flix6x Dec 30, 2021
fec17b7
Move UniqueViolation catching logic to error handler
Flix6x Dec 30, 2021
31ec00a
flake8
Flix6x Dec 30, 2021
a2fc26b
Rewrite solver to deal with asymmetry in up and down commitment prices
Flix6x Jan 1, 2022
9f3ab24
Add optional roundtrip_efficiency field to UDI events, and use it to …
Flix6x Jan 1, 2022
4f9f5c8
Add test cases for various round-trip efficiencies
Flix6x Jan 1, 2022
32034eb
Add changelog entries
Flix6x Jan 1, 2022
ae0c9cf
Add documentation for the new API field
Flix6x Jan 1, 2022
981ff8f
Grammar corrections
Flix6x Jan 1, 2022
48d98e7
Fix return value for empty EMS
Flix6x Jan 1, 2022
6c86207
Allow efficiencies per device for multi-device EMS, by stopping the a…
Flix6x Jan 1, 2022
3c9d6bf
Relax tests using some tolerance
Flix6x Jan 1, 2022
ba12570
Fix mistake
Flix6x Jan 2, 2022
b3cea6f
Add test docstring
Flix6x Jan 2, 2022
a95936f
Check round-trip efficiency for acceptable range
Flix6x Jan 2, 2022
527d926
Expand docstring
Flix6x Jan 3, 2022
28109ea
Merge remote-tracking branch 'origin/project-9' into Issue-273_Add_ro…
Flix6x Jan 3, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
8 changes: 8 additions & 0 deletions documentation/api/change_log.rst
Expand Up @@ -39,6 +39,14 @@ v2.0-0 | 2020-11-14
- REST endpoints for managing assets: `/assets/` (GET, POST) and `/asset/<id>` (GET, PATCH, DELETE).


v1.3-11 | 2022-01-01
""""""""""""""""""""

*Affects all versions since v1.3*.

- Extended the *postUdiEvent* endpoint with an optional "roundtrip_efficiency" field, for use in scheduling.


v1.3-10 | 2021-11-08
""""""""""""""""""""

Expand Down
1 change: 1 addition & 0 deletions documentation/changelog.rst
Expand Up @@ -11,6 +11,7 @@ v0.8.0 | November XX, 2021
New features
-----------
* Charts with sensor data can be requested in one of the supported [`vega-lite themes <https://github.com/vega/vega-themes#included-themes>`_] (incl. a dark theme) [see `PR #221 <http://www.github.com/FlexMeasures/flexmeasures/pull/221>`_]
* Schedulers take into account round-trip efficiency if set [see `PR #291 <http://www.github.com/FlexMeasures/flexmeasures/pull/291>`_]

Bugfixes
-----------
Expand Down
4 changes: 4 additions & 0 deletions flexmeasures/api/v1_3/implementations.py
Expand Up @@ -280,6 +280,9 @@ def post_udi_event_response(unit):
if unit == "kWh":
value = value / 1000.0

# get optional efficiency
roundtrip_efficiency = form.get("roundtrip_efficiency", None)

# set soc targets
start_of_schedule = datetime
end_of_schedule = datetime + current_app.config.get("FLEXMEASURES_PLANNING_HORIZON")
Expand Down Expand Up @@ -349,6 +352,7 @@ def post_udi_event_response(unit):
belief_time=datetime,
soc_at_start=value,
soc_targets=soc_targets,
roundtrip_efficiency=roundtrip_efficiency,
udi_event_ea=form.get("event"),
enqueue=True,
)
Expand Down
4 changes: 3 additions & 1 deletion flexmeasures/api/v1_3/routes.py
Expand Up @@ -104,6 +104,7 @@ def post_udi_event():
This "PostUdiEventRequest" message posts a state of charge (soc) of 12.1 kWh at 10.00am,
and a target state of charge of 25 kWh at 4.00pm,
as UDI event 204 of device 10 of owner 7.
Roundtrip efficiency for use in scheduling is set to 98%.
Flix6x marked this conversation as resolved.
Show resolved Hide resolved

.. code-block:: json

Expand All @@ -118,7 +119,8 @@ def post_udi_event():
"value": 25,
"datetime": "2015-06-02T16:00:00+00:00"
}
]
],
"roundtrip_efficiency": 0.98
}

**Example response**
Expand Down
14 changes: 14 additions & 0 deletions flexmeasures/data/models/planning/battery.py
Expand Up @@ -21,6 +21,7 @@ def schedule_battery(
resolution: timedelta,
soc_at_start: float,
soc_targets: Optional[pd.Series] = None,
roundtrip_efficiency: Optional[float] = None,
prefer_charging_sooner: bool = True,
) -> Union[pd.Series, None]:
"""Schedule a battery asset based directly on the latest beliefs regarding market prices within the specified time
Expand All @@ -37,6 +38,13 @@ def schedule_battery(
],
)

# Check for round-trip efficiency
if roundtrip_efficiency is None:
# Get default from sensor, or use 100% otherwise
roundtrip_efficiency = sensor.get_attribute("roundtrip_efficiency", 1)
Flix6x marked this conversation as resolved.
Show resolved Hide resolved
if roundtrip_efficiency <= 0 or roundtrip_efficiency > 1:
raise ValueError("roundtrip_efficiency expected within the interval (0, 1]")

# Check for known prices or price forecasts, trimming planning window accordingly
prices, (start, end) = get_prices(
sensor, (start, end), resolution, allow_trimmed_query_window=True
Expand Down Expand Up @@ -69,6 +77,8 @@ def schedule_battery(
"derivative equals",
"derivative max",
"derivative min",
"derivative down efficiency",
"derivative up efficiency",
]
device_constraints = [initialize_df(columns, start, end, resolution)]
if soc_targets is not None:
Expand All @@ -90,6 +100,10 @@ def schedule_battery(
)
device_constraints[0]["derivative max"] = sensor.get_attribute("capacity_in_mw")

# Apply round-trip efficiency evenly to charging and discharging
device_constraints[0]["derivative down efficiency"] = roundtrip_efficiency ** 0.5
device_constraints[0]["derivative up efficiency"] = roundtrip_efficiency ** 0.5

# Set up EMS constraints (no additional constraints)
columns = ["derivative max", "derivative min"]
ems_constraints = initialize_df(columns, start, end, resolution)
Expand Down
14 changes: 13 additions & 1 deletion flexmeasures/data/models/planning/charging_station.py
@@ -1,4 +1,4 @@
from typing import Union
from typing import Optional, Union
from datetime import datetime, timedelta

from pandas import Series, Timestamp
Expand All @@ -21,6 +21,7 @@ def schedule_charging_station(
resolution: timedelta,
soc_at_start: float,
soc_targets: Series,
roundtrip_efficiency: Optional[float] = None,
prefer_charging_sooner: bool = True,
) -> Union[Series, None]:
"""Schedule a charging station asset based directly on the latest beliefs regarding market prices within the specified time
Expand All @@ -32,6 +33,13 @@ def schedule_charging_station(
# Check for required Sensor attributes
sensor.check_required_attributes([("capacity_in_mw", (float, int))])

# Check for round-trip efficiency
if roundtrip_efficiency is None:
# Get default from sensor, or use 100% otherwise
roundtrip_efficiency = sensor.get_attribute("roundtrip_efficiency", 1)
if roundtrip_efficiency <= 0 or roundtrip_efficiency > 1:
raise ValueError("roundtrip_efficiency expected within the interval (0, 1]")

# Check for known prices or price forecasts, trimming planning window accordingly
prices, (start, end) = get_prices(
sensor, (start, end), resolution, allow_trimmed_query_window=True
Expand Down Expand Up @@ -95,6 +103,10 @@ def schedule_charging_station(
else:
device_constraints[0]["derivative max"] = sensor.get_attribute("capacity_in_mw")

# Apply round-trip efficiency evenly to charging and discharging
Flix6x marked this conversation as resolved.
Show resolved Hide resolved
device_constraints[0]["derivative down efficiency"] = roundtrip_efficiency ** 0.5
device_constraints[0]["derivative up efficiency"] = roundtrip_efficiency ** 0.5

# Set up EMS constraints (no additional constraints)
columns = ["derivative max", "derivative min"]
ems_constraints = initialize_df(columns, start, end, resolution)
Expand Down
116 changes: 100 additions & 16 deletions flexmeasures/data/models/planning/solver.py
Expand Up @@ -10,6 +10,8 @@
RangeSet,
Param,
Reals,
NonNegativeReals,
NonPositiveReals,
Constraint,
Objective,
minimize,
Expand All @@ -30,8 +32,11 @@ def device_scheduler( # noqa C901
commitment_downwards_deviation_price: Union[List[pd.Series], List[float]],
commitment_upwards_deviation_price: Union[List[pd.Series], List[float]],
) -> Tuple[List[pd.Series], float, SolverResults]:
"""Schedule devices given constraints on a device and EMS level, and given a list of commitments by the EMS.
The commitments are assumed to be with regards to the flow of energy to the device (positive for consumption,
"""This generic device scheduler is able to handle an EMS with multiple devices,
with various types of constraints on the EMS level and on the device level,
and with multiple market commitments on the EMS level.
A typical example is a house with many devices.
The commitments are assumed to be with regard to the flow of energy to the device (positive for consumption,
negative for production). The solver minimises the costs of deviating from the commitments.

Device constraints are on a device level. Handled constraints (listed by column name):
Expand All @@ -41,6 +46,8 @@ def device_scheduler( # noqa C901
derivative max: maximum flow (e.g. in MW or boxes/h)
derivative min: minimum flow
derivative equals: exact amount of flow (we do this by clamping derivative min and derivative max)
derivative down efficiency: ratio of downwards flows (flow into EMS : flow out of device)
derivative up efficiency: ratio of upwards flows (flow into device : flow out of EMS)
EMS constraints are on an EMS level. Handled constraints (listed by column name):
derivative max: maximum flow
derivative min: minimum flow
Expand All @@ -54,13 +61,13 @@ def device_scheduler( # noqa C901

All Series and DataFrames should have the same resolution.

For now we pass in the various constraints and prices as separate variables, from which we make a MultiIndex
For now, we pass in the various constraints and prices as separate variables, from which we make a MultiIndex
DataFrame. Later we could pass in a MultiIndex DataFrame directly.
"""

# If the EMS has no devices, don't bother
if len(device_constraints) == 0:
return [], 0
return [], 0, SolverResults()

# Check if commitments have the same time window and resolution as the constraints
start = device_constraints[0].index.to_pydatetime()[0]
Expand Down Expand Up @@ -164,6 +171,18 @@ def ems_derivative_min_select(m, j):
else:
return v

def device_derivative_down_efficiency(m, d, j):
try:
return device_constraints[d]["derivative down efficiency"].iloc[j]
except KeyError:
return 1

def device_derivative_up_efficiency(m, d, j):
try:
return device_constraints[d]["derivative up efficiency"].iloc[j]
except KeyError:
return 1

model.up_price = Param(model.c, model.j, initialize=price_up_select)
model.down_price = Param(model.c, model.j, initialize=price_down_select)
model.commitment_quantity = Param(
Expand All @@ -179,45 +198,107 @@ def ems_derivative_min_select(m, j):
)
model.ems_derivative_max = Param(model.j, initialize=ems_derivative_max_select)
model.ems_derivative_min = Param(model.j, initialize=ems_derivative_min_select)
model.device_derivative_down_efficiency = Param(
model.d, model.j, initialize=device_derivative_down_efficiency
)
model.device_derivative_up_efficiency = Param(
model.d, model.j, initialize=device_derivative_up_efficiency
)

# Add variables
model.power = Var(model.d, model.j, domain=Reals, initialize=0)
model.ems_power = Var(model.d, model.j, domain=Reals, initialize=0)
model.device_power_down = Var(
model.d, model.j, domain=NonPositiveReals, initialize=0
)
model.device_power_up = Var(model.d, model.j, domain=NonNegativeReals, initialize=0)
model.commitment_downwards_deviation = Var(
model.c, model.j, domain=NonPositiveReals, initialize=0
)
model.commitment_upwards_deviation = Var(
model.c, model.j, domain=NonNegativeReals, initialize=0
)

# Add constraints as a tuple of (lower bound, value, upper bound)
def device_bounds(m, d, j):
return (
m.device_min[d, j],
sum(m.power[d, k] for k in range(0, j + 1)),
sum(
m.device_power_down[d, k] + m.device_power_up[d, k]
for k in range(0, j + 1)
),
m.device_max[d, j],
)

def device_derivative_bounds(m, d, j):
return (
m.device_derivative_min[d, j],
m.power[d, j],
m.device_power_down[d, j] + m.device_power_up[d, j],
m.device_derivative_max[d, j],
)

def device_down_derivative_bounds(m, d, j):
return (
m.device_derivative_min[d, j],
m.device_power_down[d, j],
0,
)

def device_up_derivative_bounds(m, d, j):
return (
0,
m.device_power_up[d, j],
m.device_derivative_max[d, j],
)

def ems_derivative_bounds(m, j):
return m.ems_derivative_min[j], sum(m.power[:, j]), m.ems_derivative_max[j]
return m.ems_derivative_min[j], sum(m.ems_power[:, j]), m.ems_derivative_max[j]

def ems_flow_commitment_equalities(m, j):
"""Couple EMS flows (sum over devices) to commitments."""
return (
0,
sum(m.commitment_quantity[:, j])
+ sum(m.commitment_downwards_deviation[:, j])
+ sum(m.commitment_upwards_deviation[:, j])
- sum(m.ems_power[:, j]),
0,
)

def device_derivative_equalities(m, d, j):
"""Couple device flows to EMS flows per device, applying efficiencies."""
return (
0,
m.device_power_up[d, j] / m.device_derivative_up_efficiency[d, j]
+ m.device_power_down[d, j] * m.device_derivative_down_efficiency[d, j]
- m.ems_power[d, j],
0,
)

model.device_energy_bounds = Constraint(model.d, model.j, rule=device_bounds)
model.device_power_bounds = Constraint(
model.d, model.j, rule=device_derivative_bounds
)
model.device_power_down_bounds = Constraint(
model.d, model.j, rule=device_down_derivative_bounds
)
model.device_power_up_bounds = Constraint(
model.d, model.j, rule=device_up_derivative_bounds
)
model.ems_power_bounds = Constraint(model.j, rule=ems_derivative_bounds)
model.ems_power_commitment_equalities = Constraint(
model.j, rule=ems_flow_commitment_equalities
)
model.device_power_equalities = Constraint(
model.d, model.j, rule=device_derivative_equalities
)

# Add objective
def cost_function(m):
costs = 0
for c in m.c:
for j in m.j:
ems_power_in_j = sum(m.power[d, j] for d in m.d)
ems_power_deviation = ems_power_in_j - m.commitment_quantity[c, j]
if value(ems_power_deviation) >= 0:
costs += ems_power_deviation * m.up_price[c, j]
else:
costs += ems_power_deviation * m.down_price[c, j]
costs += m.commitment_downwards_deviation[c, j] * m.down_price[c, j]
costs += m.commitment_upwards_deviation[c, j] * m.up_price[c, j]
return costs

model.costs = Objective(rule=cost_function, sense=minimize)
Expand All @@ -230,7 +311,10 @@ def cost_function(m):
planned_costs = value(model.costs)
planned_power_per_device = []
for d in model.d:
planned_device_power = [model.power[d, j].value for j in model.j]
planned_device_power = [
model.device_power_down[d, j].value + model.device_power_up[d, j].value
for j in model.j
]
planned_power_per_device.append(
pd.Series(
index=pd.date_range(
Expand All @@ -243,5 +327,5 @@ def cost_function(m):
# model.pprint()
# print(results.solver.termination_condition)
# print(planned_costs)
# input()
# model.display()
return planned_power_per_device, planned_costs, results