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

feat(scheduling): Price devices distinctively in scheduling #654

Draft
wants to merge 23 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 21 commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
ab72c4f
Adapt FlexContextSchema for pricing devices distinctively
anirudh-ramesh Apr 26, 2023
a9550f0
Add price sensors for individual devices to the CLI
anirudh-ramesh Apr 26, 2023
bea996e
Merge branch 'main' into feature/scheduling
anirudh-ramesh Apr 29, 2023
a80a475
Merge branch 'main' into feature/scheduling
anirudh-ramesh May 9, 2023
014edfa
Add help text for CLI option corresponding to price sensors per device
anirudh-ramesh May 9, 2023
515c31b
Pass prices from the database to the scheduler
anirudh-ramesh May 9, 2023
c34952d
Edit model in order to ingest multiple price sensors
anirudh-ramesh May 9, 2023
be79cf7
Add new index dimensions to linear_optimization.py
anirudh-ramesh May 12, 2023
901b7f3
Update linear_optimization.py
rajath-09 May 12, 2023
fc12afb
Update linear_optimization.py
rajath-09 May 19, 2023
346030c
Update linear_optimization.py
rajath-09 May 19, 2023
be387b5
Update data_add.py
rajath-09 Jun 2, 2023
f830cfc
Update data_add.py
rajath-09 Jun 2, 2023
e2fcb5a
Update data_add.py
rajath-09 Jun 2, 2023
7307256
Merge branch 'main' of https://github.com/irasus-technologies/flexmea…
Jun 2, 2023
0a9c6f0
"Adding tests for dictionary price sensor parameters"
Jun 5, 2023
9fe1831
"Correction in Schedule End date"
Jun 6, 2023
a931838
"Changes Suggested in TestFunction"
Jun 7, 2023
f0919ae
"Flake8 and Black changes"
Jun 8, 2023
41d3a2f
"Flake changes in Storage.py and linear_optimization.py"
Jun 8, 2023
11d2b03
Merge branch 'FlexMeasures:main' into feature/scheduling
rajath-09 Jun 9, 2023
95da3f4
"Changes made according to PR reviews"
Jun 15, 2023
ea323eb
"Revert back to 'd' index dimension"
Jun 26, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions flexmeasures/cli/data_add.py
Original file line number Diff line number Diff line change
Expand Up @@ -947,6 +947,20 @@ def create_schedule(ctx):
required=False,
help="Optimize production against this sensor. Defaults to the consumption price sensor. The sensor typically records an electricity price (e.g. in EUR/kWh), but this field can also be used to optimize against some emission intensity factor (e.g. in kg CO₂ eq./kWh). Follow up with the sensor's ID.",
)
@click.option(
"--consumption-price-sensor-per-device",
"consumption_price_sensor_per_device",
type=dict,
anirudh-ramesh marked this conversation as resolved.
Show resolved Hide resolved
required=False,
help="Optimize consumption against this dictionary of sensors. The sensors typically record electricity prices (e.g. in EUR/kWh), but this field can also be used to optimize against some emission intensity factors (e.g. in kg CO₂ eq./kWh).",
)
@click.option(
"--production-price-sensor-per-device",
"production_price_sensor_per_device",
type=dict,
required=False,
help="Optimize production against this dictionary of sensors. Defaults to the consumption price sensor. The sensors typically record electricity prices (e.g. in EUR/kWh), but this field can also be used to optimize against some emission intensity factors (e.g. in kg CO₂ eq./kWh).",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here I would show an example of how to pass a dictionary in the command line.

Moreover, I'm not sure this type (dict) would work straightaway, given that:

  • The dict type is not listed in the click parameters.
  • I created a simple command to test it and I couldn't get to accept any of the formats {"a" : 2}, a=1, "a"=1, a:1 or '{"a":2}'.

Test command:

@fm_add_data.command("test-dict")
@click.option("--dictionary", type=dict)
def test_dict_type(dictionary : dict):
    click.secho(dictionary)

Copy link

@rajath-09 rajath-09 Jun 13, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Correctly said.I propose we do something like this?

@click.option("--dictionary", type=str)
def test_dict_type(dictionary: str):
    try:
        dictionary = json.loads(dictionary)
        click.secho(str(dictionary))
    except json.JSONDecodeError:
        click.secho("Invalid dictionary format.", fg='red')

Using this now I can use the format { "19" : 21}
Let me know if that works?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey @victorgarcia98
I worked on this now it supports for the following type of input :
flexmeasures add schedule for-storage --sensor-id 1 --consumption-price-sensor-per-device '{"1": 2}' --start ${TOMORROW}T07:00+01:00 --duration PT12H --soc-at-start 50% --roundtrip-efficiency 90% --as-job
Let me know if that's fine?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also given the introduction to the new price sensor parameters, I believe the consumption_price_sensor should no longer be a necessary argument in the CLI and rather an option.It worked for me by omitting the function call to replace_deprecated_argument.Is that how it is supposed to be done?

)
@click.option(
"--optimization-context-id",
"optimization_context_sensor",
Expand Down Expand Up @@ -1029,6 +1043,8 @@ def add_schedule_for_storage(
production_price_sensor: Sensor,
optimization_context_sensor: Sensor,
inflexible_device_sensors: list[Sensor],
consumption_price_sensor_per_device: dict[Sensor, Sensor],
production_price_sensor_per_device: dict[Sensor, Sensor],
start: datetime,
duration: timedelta,
soc_at_start: ur.Quantity,
Expand Down Expand Up @@ -1109,6 +1125,14 @@ def add_schedule_for_storage(
"consumption-price-sensor": consumption_price_sensor.id,
"production-price-sensor": production_price_sensor.id,
"inflexible-device-sensors": [s.id for s in inflexible_device_sensors],
"consumption-price-sensor-per-device": {
power.id: price.id
for power, price in consumption_price_sensor_per_device.items()
},
"production-price-sensor-per-device": {
power.id: price.id
for power, price in production_price_sensor_per_device.items()
},
},
)
if as_job:
Expand Down
89 changes: 58 additions & 31 deletions flexmeasures/data/models/planning/linear_optimization.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from flask import current_app
import pandas as pd
import numpy as np
from typing import Dict
from pandas.tseries.frequencies import to_offset
from pyomo.core import (
ConcreteModel,
Expand All @@ -29,8 +30,12 @@ def device_scheduler( # noqa C901
device_constraints: List[pd.DataFrame],
ems_constraints: pd.DataFrame,
commitment_quantities: List[pd.Series],
commitment_downwards_deviation_price: Union[List[pd.Series], List[float]],
commitment_upwards_deviation_price: Union[List[pd.Series], List[float]],
consumption_price_sensor_per_device: Dict[int, int],
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure if device_scheduler function is used somewhere else (e.g a plugin). In any case, I think we should still support the previous way (i.e commitment_downwards_deviation_price, commitment_upwards_deviation_price). What do you think @Flix6x?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Indeed we should make this backwards compatible instead of introducing a change that potentially breaks things.

At the moment, I think this constitutes a breaking change not only in terms of changing the function signature, but also in terms of breaking a use case. Namely, applying prices to the whole system rather than to each device individually.

production_price_sensor_per_device: Dict[int, int],
commitment_downwards_deviation_price_array: List[
Union[List[pd.Series], List[float]]
],
commitment_upwards_deviation_price_array: List[Union[List[pd.Series], List[float]]],
) -> Tuple[List[pd.Series], float, SolverResults]:
"""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,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The docstring is not updated with the new parameters.

Expand Down Expand Up @@ -89,22 +94,26 @@ def device_scheduler( # noqa C901
)

# Turn prices per commitment into prices per commitment flow
if len(commitment_downwards_deviation_price) != 0:
if all(
isinstance(price, float) for price in commitment_downwards_deviation_price
):
commitment_downwards_deviation_price = [
initialize_series(price, start, end, resolution)
for price in commitment_downwards_deviation_price
]
if len(commitment_upwards_deviation_price) != 0:
if all(
isinstance(price, float) for price in commitment_upwards_deviation_price
):
commitment_upwards_deviation_price = [
initialize_series(price, start, end, resolution)
for price in commitment_upwards_deviation_price
]
for i in range(0, len(commitment_downwards_deviation_price_array)):
if len(commitment_downwards_deviation_price_array[i]) != 0:
if all(
isinstance(price, float)
for price in commitment_downwards_deviation_price_array[i]
):
commitment_downwards_deviation_price_array[i] = [
initialize_series(price, start, end, resolution)
for price in commitment_downwards_deviation_price_array[i]
]
for i in range(0, len(commitment_upwards_deviation_price_array)):
if len(commitment_upwards_deviation_price_array[i]) != 0:
if all(
isinstance(price, float)
for price in commitment_upwards_deviation_price_array[i]
):
commitment_upwards_deviation_price_array[i] = [
initialize_series(price, start, end, resolution)
for price in commitment_upwards_deviation_price_array[i]
]

model = ConcreteModel()

Expand All @@ -114,13 +123,23 @@ def device_scheduler( # noqa C901
0, len(device_constraints[0].index.to_pydatetime()) - 1, doc="Set of datetimes"
)
model.c = RangeSet(0, len(commitment_quantities) - 1, doc="Set of commitments")

model.p = RangeSet(
0,
len(commitment_downwards_deviation_price_array) - 1,
doc="Set of Production price sensors",
)
model.u = RangeSet(
0,
len(commitment_upwards_deviation_price_array) - 1,
doc="Set of Consumption price sensors",
)
# Add parameters
def price_down_select(m, c, j):
return commitment_downwards_deviation_price[c].iloc[j]

def price_up_select(m, c, j):
return commitment_upwards_deviation_price[c].iloc[j]
def price_down_select(m, p, c, j):
return commitment_downwards_deviation_price_array[p][c].iloc[j]

def price_up_select(m, u, c, j):
return commitment_upwards_deviation_price_array[u][c].iloc[j]

def commitment_quantity_select(m, c, j):
return commitment_quantities[c].iloc[j]
Expand Down Expand Up @@ -191,8 +210,8 @@ def device_derivative_up_efficiency(m, d, j):
return 1
return eff

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.up_price = Param(model.u, model.c, model.j, initialize=price_up_select)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that instead of using u and p indexes, the price should depend only on the device (d).

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using 'u','p' allows the user to have different number or 'consumption_price_sensor_per device' and 'production_price_sensor_per_device'. Using 'd' would not serve that purpose.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Correct me if I'm wrong, the idea of this PR is to allow having different price sensor between devices. That means that we want to map each device to a price sensor.

For example:

Device 1  -> Consumption 1, Production 2
Device 2  -> Consumption 3, Production 4
Device 3  -> Consumption 5, Production 6

In this example, we would have consumption_price_sensor_per_device = [1,3,5] and production_price_sensor_per_device = [2,4,6].

Having the `consumption_price_sensor_per_device[d]' indexed by the device, we could get the corresponding price sensor to each device. This is, of course, assuming that we can have only 2 price sensors per device (consumption and production).

Another interpretation of having 'allow having different price sensor between devices could be to allow defining multiple price sensors to a device. In that case, there are different ways to have multiple price sensors "attached" to a device: sum, different times of the day. Is this later interpretation the one that you had in mind?

Copy link

@rajath-09 rajath-09 Jun 12, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My interpretation was on the basis of the assumption that a device may have only one price sensor attached to it.For example,

  1. Solar Plants will have only Production Price Sensor as they ideally wont be consuming electricity
  2. Load/Building will have consumption price sensor only as they are supposed to only consume electricity.

Let me know if my understanding is right.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What do you think? @victorgarcia98

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, we are on the same page!

Then, we can use the device index d (instead of u and p) to get which price time series correspond to a particular device, given that there would be at most one.

Given a dictionary that maps devices to the prices, we can iterate through all the devices and in case the price is missing in the dictionary, we just have one with price zero, as @Flix6x mentioned in another conversation.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure @victorgarcia98 .That works.
But couple of things i needed to clarify before working it through:

  1. Is there any particular problem with the introduction of the new index dimensions?
  2. For instance,grid is a flexible device.So in that case ,I wont be able to map price sensors to the grid because 'd' here refers to the set of inflexible devices.How will that be taken care of?
  3. Will putting price zero to some price sensor mean that the device can consume/produce at cost 0 or will it restrict the consumption/production of energy for that device(in case consumption/production is set to zero)?According to my understanding it will be the first case and that will make our model give different results.

Please let me know if you understood my concerns.Also let me know that you think?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. Applying prices to the power flow exchanged with the grid is exactly the use case we currently support (and want to keep supporting). We do not explicitly model the grid as a separate device, but rather model it implicitly as the sum of (the power flow of) all devices. Grid (power) constraints can be set through the variable ems_constraints. Grid prices can be set separately for consumption and production, using consumption_price_sensor and production_price_sensor, respectively.

  2. A zero device consumption price would indeed mean that the device consumes without accruing costs, and a zero device production price would mean that the device produces without accruing revenue. The zero price should not constraint their power flow. But their power flow may still lead to accruing costs or revenues in case there are non-zero prices on the grid.

Hope this clarifies things?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes thanks @Flix6x.Things are clearer now.
I will change the dependency of the price sensors to 'd' instead then.

Copy link

@rajath-09 rajath-09 Jun 19, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi again @victorgarcia98 @Flix6x
Upon thinking it through even more, I realized that d index wont suffice for the situation where i have a building/load as one of my inflexible devices.
In this case,the prices for consumption are not to be set and are to be decided by the solver.
But according to what @Flix6x said we could either set the prices or set it to 0 while iterating through the inflexible devices.This wont work out here.
I really think there is the necessity for these new index dimensions,which otherwise would just complicate stuff.
We could get on a call if you are still not satisfied with the reasoning.

model.down_price = Param(model.p, model.c, model.j, initialize=price_down_select)
model.commitment_quantity = Param(
model.c, model.j, initialize=commitment_quantity_select
)
Expand Down Expand Up @@ -220,10 +239,10 @@ def device_derivative_up_efficiency(m, d, j):
)
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.p, model.c, model.j, domain=NonPositiveReals, initialize=0
)
model.commitment_upwards_deviation = Var(
model.c, model.j, domain=NonNegativeReals, initialize=0
model.u, model.c, model.j, domain=NonNegativeReals, initialize=0
)

# Add constraints as a tuple of (lower bound, value, upper bound)
Expand Down Expand Up @@ -270,8 +289,8 @@ def ems_flow_commitment_equalities(m, j):
return (
0,
sum(m.commitment_quantity[:, j])
+ sum(m.commitment_downwards_deviation[:, j])
+ sum(m.commitment_upwards_deviation[:, j])
+ sum(m.commitment_downwards_deviation[:, :, j])
+ sum(m.commitment_upwards_deviation[:, :, j])
- sum(m.ems_power[:, j]),
0,
)
Expand Down Expand Up @@ -307,8 +326,16 @@ def cost_function(m):
costs = 0
for c in m.c:
for j in m.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]
for p in m.p:
for u in m.u:
costs += (
m.commitment_downwards_deviation[p, c, j]
* m.down_price[p, c, j]
)
costs += (
m.commitment_upwards_deviation[u, c, j]
* m.up_price[u, c, j]
)
return costs

model.costs = Objective(rule=cost_function, sense=minimize)
Expand Down
88 changes: 56 additions & 32 deletions flexmeasures/data/models/planning/storage.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,8 +75,12 @@ def compute(self, skip_validation: bool = False) -> pd.Series | None:
roundtrip_efficiency = self.flex_model.get("roundtrip_efficiency")
prefer_charging_sooner = self.flex_model.get("prefer_charging_sooner", True)

consumption_price_sensor = self.flex_context.get("consumption_price_sensor")
production_price_sensor = self.flex_context.get("production_price_sensor")
consumption_price_sensor_per_device = self.flex_context.get(
"consumption_price_sensor_per_device", {}
)
production_price_sensor_per_device = self.flex_context.get(
"production_price_sensor_per_device", {}
)
inflexible_device_sensors = self.flex_context.get(
"inflexible_device_sensors", []
)
Expand All @@ -85,46 +89,64 @@ def compute(self, skip_validation: bool = False) -> pd.Series | None:
self.sensor.check_required_attributes([("capacity_in_mw", (float, int))])

# Check for known prices or price forecasts, trimming planning window accordingly
up_deviation_prices, (start, end) = get_prices(
(start, end),
resolution,
beliefs_before=belief_time,
price_sensor=consumption_price_sensor,
sensor=sensor,
allow_trimmed_query_window=False,
)
down_deviation_prices, (start, end) = get_prices(
(start, end),
resolution,
beliefs_before=belief_time,
price_sensor=production_price_sensor,
sensor=sensor,
allow_trimmed_query_window=False,
)
up_deviation_prices_array = []
for power_sensor, price_sensor in consumption_price_sensor_per_device.items():
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What if consumption_price_sensor is provided instead of consumption_price_sensor_per_device?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For example, if the StorageScheduler is called through the api, it will fail.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed. Shall we just use if case to handle that ?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Alternatively, I was wondering if I could append the 'consumption_price_sensor' to 'consumption_price_sensor_per_device' ,and similarly for production, by making the dictionary link between 'sensor=self.sensor' with 'consumption_price_sensor'?
What do you think about that?If you agree I will proceed with that.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I think that could work. If consumption_price_sensor_per_device is not provided, you can create internally one that maps each device to the same consumption_price_sensor. The same for production.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do I need to map each device with consumption_price_sensor or could i just map consumption_price_sensor with the battery sensor either?Shouldn't that work as well?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If consumption_price_sensor_per_device is not provided, you can create internally one that maps each device to the same consumption_price_sensor. The same for production.

This would not be the same as applying the consumption price sensor to the aggregate power flow, which is the use case we currently support.

For example, for a given time step, we currently have:

Example A

  • Device A consumes 10 kWh
  • Device B produces 8 kWh
  • Devices A and B belong to the same system, with an aggregate consumption of 2 kWh
  • The system's aggregate consumption is priced at 0.5 EUR/kWh
  • The system costs are 1 EUR.

If I am reading your suggestion right, you propose:

Example B

  • Device A consumes 10 kWh, priced at 0.5 EUR/kWh, incurring costs of 5 EUR
  • Device B produces 8 kWh, priced at 0.5 EUR/kWh, incurring a revenue of 4 EUR
  • The system costs are 5 - 4 = 1 EUR

Is that what you had in mind?

This might look the same, but things change when the consumption price differs from the production price. Let's say the production price is 0.4 EUR/kWh. Example A wouldn't change, because there is no aggregate production, only aggregate consumption. Example B changes, however:

  • Device A consumes 10 kWh, priced at 0.5 EUR/kWh, incurring costs of 5 EUR
  • Device B produces 8 kWh, priced at 0.4 EUR/kWh, incurring a revenue of 3.20 EUR
  • The system costs are 5 - 3.20 = 1.80 EUR

In my opinion, we should not touch the current use case by trying to map the consumption_price_sensor argument to individual devices. Instead, we should extend the cost function with additional cost components for each device, allowing to specify a consumption and production price per device. If a price for a specific device is not specified, it can be assumed to be zero.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes agreed @victorgarcia98 .But what I was suggesting was to map consumption_price_sensor to the battery sensor(which would in turn take care of the aggregate consumption/production just like in Example A). In my opinion ,that's gonna work perfectly as mapping it with the battery would mean mapping it up with the system.Let me know if that's something that works?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't follow. Can you explain it with an example in more detail, with example values?

Copy link

@rajath-09 rajath-09 Jun 14, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Earlier, we had the following:

up_deviation_prices, (start, end) = get_prices(
            (start, end),
            resolution,
            beliefs_before=belief_time,
            price_sensor=consumption_price_sensor,
            sensor=sensor,
            allow_trimmed_query_window=False,
        )

which now has changed to this:

up_deviation_prices_array = []
        for power_sensor, price_sensor in consumption_price_sensor_per_device.items():
            up_deviation_prices, (start, end) = get_prices(
                (start, end),
                resolution,
                beliefs_before=belief_time,
                price_sensor=price_sensor,
                sensor=power_sensor,
                allow_trimmed_query_window=False,
            )
            up_deviation_prices_array.append(up_deviation_prices)

Keeping this in mind, I believe the sensor to be passed to get_prices should be the battery power sensor to make sure the price sensor is applied to the aggregate power flow.
So the following code should be added to compute function :

#Convert single price sensors to Multiple Price Sensors Dict
        if consumption_price_sensor is not None:
            consumption_price_sensor_per_device[sensor]=consumption_price_sensor
        
        if production_price_sensor is not None:
            production_price_sensor_per_device[sensor]=production_price_sensor

This will make the situation exactly like Example A.There is no difference in the example.

up_deviation_prices, (start, end) = get_prices(
(start, end),
resolution,
beliefs_before=belief_time,
price_sensor=price_sensor,
sensor=power_sensor,
allow_trimmed_query_window=False,
)
up_deviation_prices_array.append(up_deviation_prices)
down_deviation_prices_array = []
for power_sensor, price_sensor in production_price_sensor_per_device.items():
down_deviation_prices, (start, end) = get_prices(
(start, end),
resolution,
beliefs_before=belief_time,
price_sensor=price_sensor,
sensor=power_sensor,
allow_trimmed_query_window=False,
)
down_deviation_prices_array.append(down_deviation_prices)

start = pd.Timestamp(start).tz_convert("UTC")
end = pd.Timestamp(end).tz_convert("UTC")

# Add tiny price slope to prefer charging now rather than later, and discharging later rather than now.
# We penalise the future with at most 1 per thousand times the price spread.
if prefer_charging_sooner:
up_deviation_prices = add_tiny_price_slope(
up_deviation_prices, "event_value"
)
down_deviation_prices = add_tiny_price_slope(
down_deviation_prices, "event_value"
)
for i in range(0, len(up_deviation_prices_array)):
up_deviation_prices_array[i] = add_tiny_price_slope(
up_deviation_prices_array[i], "event_value"
)
for i in range(0, len(down_deviation_prices_array)):
down_deviation_prices_array[i] = add_tiny_price_slope(
down_deviation_prices_array[i], "event_value"
)

# Set up commitments to optimise for
commitment_quantities = [initialize_series(0, start, end, self.resolution)]

# Todo: convert to EUR/(deviation of commitment, which is in MW)
commitment_upwards_deviation_price = [
up_deviation_prices.loc[start : end - resolution]["event_value"]
]
commitment_downwards_deviation_price = [
down_deviation_prices.loc[start : end - resolution]["event_value"]
]
commitment_upwards_deviation_price_array = []
for up_deviation_price in up_deviation_prices_array:
commitment_upwards_deviation_price = [
up_deviation_price.loc[start : end - resolution]["event_value"]
]
commitment_upwards_deviation_price_array.append(
commitment_upwards_deviation_price
)
commitment_downwards_deviation_price_array = []
for down_deviation_price in down_deviation_prices_array:
commitment_downwards_deviation_price = [
down_deviation_price.loc[start : end - resolution]["event_value"]
]
commitment_downwards_deviation_price_array.append(
commitment_downwards_deviation_price
)

# Set up device _constraints: only one scheduled flexible device for this EMS (at index 0), plus the forecasted inflexible devices (at indices 1 to n).
device_constraints = [
Expand Down Expand Up @@ -197,8 +219,10 @@ def compute(self, skip_validation: bool = False) -> pd.Series | None:
device_constraints,
ems_constraints,
commitment_quantities,
commitment_downwards_deviation_price,
commitment_upwards_deviation_price,
consumption_price_sensor_per_device,
production_price_sensor_per_device,
commitment_downwards_deviation_price_array,
commitment_upwards_deviation_price_array,
)
if scheduler_results.solver.termination_condition == "infeasible":
# Fallback policy if the problem was unsolvable
Expand Down
6 changes: 6 additions & 0 deletions flexmeasures/data/schemas/scheduling/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,9 @@ class FlexContextSchema(Schema):
inflexible_device_sensors = fields.List(
SensorIdField(), data_key="inflexible-device-sensors"
)
consumption_price_sensor_per_device = fields.Dict(
SensorIdField(), SensorIdField(), data_key="consumption-price-sensor-per-device"
)
production_price_sensor_per_device = fields.Dict(
SensorIdField(), SensorIdField(), data_key="production-price-sensor-per-device"
)
Loading