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: add ProcessScheduler #729

Merged
merged 31 commits into from Jul 31, 2023
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
f64709a
test: add shiftable_load fixture
victorgarcia98 Jun 14, 2023
35ca7e7
test: move fixture setup_dummy_sensors from test_reporting.py to conf…
victorgarcia98 Jun 14, 2023
99af08c
feat: add ShiftableLoadFlexModelSchema
victorgarcia98 Jun 14, 2023
5216fa3
test: add ShiftableLoadFlexModelSchema tests
victorgarcia98 Jun 14, 2023
8faf7e5
feat: add ShiftableLoadScheduler
victorgarcia98 Jun 14, 2023
b1472a5
tests: add ShiftableLoadScheduler tests
victorgarcia98 Jun 14, 2023
e32767a
test: add required parameter
victorgarcia98 Jun 14, 2023
c1e9db5
Merge branch 'main' into feature/shiftable-load-scheduler
victorgarcia98 Jun 14, 2023
d26cd53
docs: improve docstrings
victorgarcia98 Jun 15, 2023
3016bdd
Merge branch 'main' into feature/shiftable-load-scheduler
victorgarcia98 Jul 20, 2023
48eed71
fix: pandas 2.0 deprecated argument
victorgarcia98 Jul 20, 2023
84c23be
Merge branch 'main' into feature/shiftable-load-scheduler
victorgarcia98 Jul 21, 2023
850dd51
docs: add changelog
victorgarcia98 Jul 21, 2023
b099e25
refactor: move TimeIntervalSchema to data.schemas.time
victorgarcia98 Jul 21, 2023
bf8c20d
refactor: rename cost_sensor to consumption_price_sensor
victorgarcia98 Jul 21, 2023
405ca40
docs: add attribute description
victorgarcia98 Jul 21, 2023
c20669c
address change requests
victorgarcia98 Jul 25, 2023
fbffd2e
Merge branch 'main' into feature/shiftable-load-scheduler
victorgarcia98 Jul 25, 2023
98d4a16
use consumption_price_sensor from flex_context
victorgarcia98 Jul 25, 2023
b991863
making block_invalid_starting_times_for_whole_process_scheduling work…
victorgarcia98 Jul 25, 2023
6822958
remove consumption_price_sensor from flex_model
victorgarcia98 Jul 25, 2023
1529a91
refactor: rename shiftable_load to process
victorgarcia98 Jul 26, 2023
827df19
rename optimization_sense to optimization_direction
victorgarcia98 Jul 27, 2023
9d7e6e8
consistent capitalization of INFLEXIBLE, SHIFTABLE AND BREAKABLE
victorgarcia98 Jul 27, 2023
8f7c276
fix typo
victorgarcia98 Jul 27, 2023
d642b2a
Merge branch 'main' into feature/shiftable-load-scheduler
victorgarcia98 Jul 27, 2023
e5cad35
fix capitalizationof `inflexible-device-sensors`
victorgarcia98 Jul 27, 2023
ab81e41
Merge remote-tracking branch 'origin/feature/shiftable-load-scheduler…
victorgarcia98 Jul 27, 2023
92b5d97
rename OptimizationSense to OptimizationDirection
victorgarcia98 Jul 31, 2023
d11a191
fix missing optimization direction renaming
victorgarcia98 Jul 31, 2023
d3d6b9c
Merge branch 'main' into feature/shiftable-load-scheduler
victorgarcia98 Jul 31, 2023
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
217 changes: 217 additions & 0 deletions flexmeasures/data/models/planning/shiftable_load.py
@@ -0,0 +1,217 @@
from __future__ import annotations

from math import ceil
from datetime import timedelta
import pytz

import pandas as pd

from flexmeasures.data.models.planning import Scheduler

from flexmeasures.data.queries.utils import simplify_index
from flexmeasures.data.models.time_series import Sensor
from flexmeasures.data.schemas.scheduling.shiftable_load import (
ShiftableLoadFlexModelSchema,
LoadType,
OptimizationSense,
)
from flexmeasures.data.schemas.scheduling import FlexContextSchema


class ShiftableLoadScheduler(Scheduler):

__version__ = "1"
__author__ = "Seita"

def compute(self) -> pd.Series | None:
"""Schedule a load, defined as a `power` and a `duration`, within the specified time window.
victorgarcia98 marked this conversation as resolved.
Show resolved Hide resolved
For example, this scheduler can plan the start of a process that lasts 5h and requires a power of 10kW.
victorgarcia98 marked this conversation as resolved.
Show resolved Hide resolved

This scheduler supports three types of `load_types`:
- Inflexible: this load requires to be scheduled as soon as possible.
- Breakable: this load can be divisible in smaller consumption periods.
- Shiftable: this load can start at any time within the specified time window.

The resulting schedule provides the power flow at each time period.

Parameters
==========

cost_sensor: it defines the utility (economic, environmental, ) in each
victorgarcia98 marked this conversation as resolved.
Show resolved Hide resolved
time period. It has units of quantity/energy, for example, EUR/kWh.
power: nominal power of the load.
duration: time that the load lasts.

optimization_sense: objective of the scheduler, to maximize or minimize.
time_restrictions: time periods in which the load cannot be schedule to.
load_type: Inflexible, Breakable or Shiftable.

:returns: The computed schedule.
"""

if not self.config_deserialized:
self.deserialize_config()

start = self.start.astimezone(pytz.utc)
end = self.end.astimezone(pytz.utc)
resolution = self.resolution
belief_time = self.belief_time
sensor = self.sensor

cost_sensor: Sensor = self.flex_model.get("cost_sensor")
duration: timedelta = self.flex_model.get("duration")
power = self.flex_model.get("power")
optimization_sense = self.flex_model.get("optimization_sense")
load_type: LoadType = self.flex_model.get("load_type")
time_restrictions = self.flex_model.get("time_restrictions")

# get cost data
cost = cost_sensor.search_beliefs(
event_starts_after=start,
event_ends_before=end,
resolution=resolution,
one_deterministic_belief_per_event=True,
beliefs_before=belief_time,
)
cost = simplify_index(cost)

# create an empty schedule
schedule = pd.Series(
index=pd.date_range(
start,
end,
freq=sensor.event_resolution,
closed="left",
name="event_start",
),
data=0,
name="event_value",
)

# optimize schedule for tomorrow. We can fill len(schedule) rows, at most
victorgarcia98 marked this conversation as resolved.
Show resolved Hide resolved
rows_to_fill = min(ceil(duration / cost_sensor.event_resolution), len(schedule))

# convert power to energy using the resolution of the sensor.
# e.g. resolution=15min, power=1kW -> energy=250W
energy = power * cost_sensor.event_resolution / timedelta(hours=1)

if rows_to_fill > len(schedule):
victorgarcia98 marked this conversation as resolved.
Show resolved Hide resolved
raise ValueError(
f"Duration of the period exceeds the schedule window. The resulting schedule will be trimmed to fit the planning window ({start}, {end})."
)

# check if the time_restrictions allow for a load of the duration provided
victorgarcia98 marked this conversation as resolved.
Show resolved Hide resolved
if load_type in [LoadType.INFLEXIBLE, LoadType.SHIFTABLE]:
# get start time instants that are not feasible, i.e. some time during the ON period goes through
victorgarcia98 marked this conversation as resolved.
Show resolved Hide resolved
# a time restriction interval
time_restrictions = (
time_restrictions.rolling(duration).max().shift(-rows_to_fill + 1)
)
time_restrictions = (time_restrictions == 1) | time_restrictions.isna()

if time_restrictions.sum() == len(time_restrictions):
victorgarcia98 marked this conversation as resolved.
Show resolved Hide resolved
raise ValueError(
"Cannot allocate a block of time {duration} given the time restrictions provided."
)
else: # LoadType.BREAKABLE
if (~time_restrictions).sum() < rows_to_fill:
raise ValueError(
"Cannot allocate a block of time {duration} given the time restrictions provided."
)

# create schedule
if load_type == LoadType.INFLEXIBLE:
self.compute_inflexible(schedule, time_restrictions, rows_to_fill, energy)
elif load_type == LoadType.BREAKABLE:
self.compute_breakable(
schedule,
optimization_sense,
time_restrictions,
cost,
rows_to_fill,
energy,
)
elif load_type == LoadType.SHIFTABLE:
self.compute_shiftable(
schedule,
optimization_sense,
time_restrictions,
cost,
rows_to_fill,
energy,
)
else:
raise ValueError(f"Unknown load type '{load_type}'")

return schedule.tz_convert(self.start.tzinfo)

def compute_inflexible(
self,
schedule: pd.Series,
time_restrictions: pd.Series,
rows_to_fill: int,
energy: float,
) -> None:
"""Schedule load as early as possible."""
start = time_restrictions[~time_restrictions].index[0]

schedule.loc[start : start + self.resolution * (rows_to_fill - 1)] = energy

def compute_breakable(
self,
schedule: pd.Series,
optimization_sense: OptimizationSense,
time_restrictions: pd.Series,
cost: pd.DataFrame,
rows_to_fill: int,
energy: float,
) -> None:
"""Break up schedule and divide it over the time slots with the largest utility (max/min cost depending on optimization_sense)."""
cost = cost[~time_restrictions].reset_index()

if optimization_sense == OptimizationSense.MIN:
cost_ranking = cost.sort_values(
by=["event_value", "event_start"], ascending=[True, True]
)
else:
cost_ranking = cost.sort_values(
by=["event_value", "event_start"], ascending=[False, True]
)

schedule.loc[cost_ranking.head(rows_to_fill).event_start] = energy

def compute_shiftable(
self,
schedule: pd.Series,
optimization_sense: OptimizationSense,
time_restrictions: pd.Series,
cost: pd.DataFrame,
rows_to_fill: int,
energy: float,
) -> None:
"""Schedules a block of consumption/production of `rows_to_fill` periods to maximize a utility."""
block_cost = simplify_index(
cost.rolling(rows_to_fill).sum().shift(-rows_to_fill + 1)
)

if optimization_sense == OptimizationSense.MIN:
start = block_cost[~time_restrictions].idxmin()
else:
start = block_cost[~time_restrictions].idxmax()

start = start.event_value
victorgarcia98 marked this conversation as resolved.
Show resolved Hide resolved

schedule.loc[start : start + self.resolution * (rows_to_fill - 1)] = energy

def deserialize_flex_config(self):
"""Deserialize flex_model using the schema ShiftableLoadFlexModelSchema and
flex_context using FlexContextSchema
"""
if self.flex_model is None:
self.flex_model = {}

self.flex_model = ShiftableLoadFlexModelSchema(
start=self.start, end=self.end, sensor=self.sensor
).load(self.flex_model)

self.flex_context = FlexContextSchema().load(self.flex_context)
16 changes: 16 additions & 0 deletions flexmeasures/data/models/planning/tests/conftest.py
Expand Up @@ -187,6 +187,22 @@ def add_inflexible_device_forecasts(
}


@pytest.fixture(scope="module")
def shiftable_load(db, building, setup_sources) -> dict[str, Sensor]:
"""
Set up a shiftable load sensor where the output of the optimization is stored.
"""
_shiftable_load = Sensor(
name="Shiftable Load",
generic_asset=building,
event_resolution=timedelta(hours=1),
unit="kWh",
)
db.session.add(_shiftable_load)

return _shiftable_load


def add_as_beliefs(db, sensor, values, time_slots, source):
beliefs = [
TimedBelief(
Expand Down