Skip to content

Commit

Permalink
Merge 304cd48 into 3978be3
Browse files Browse the repository at this point in the history
  • Loading branch information
victorgarcia98 committed Jan 26, 2024
2 parents 3978be3 + 304cd48 commit 9b823a8
Show file tree
Hide file tree
Showing 4 changed files with 180 additions and 7 deletions.
135 changes: 129 additions & 6 deletions flexmeasures/cli/data_add.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@
LongitudeField,
SensorIdField,
TimeIntervalField,
QuantityOrSensor,
)
from flexmeasures.data.schemas.sources import DataSourceIdField
from flexmeasures.data.schemas.times import TimeIntervalSchema
Expand Down Expand Up @@ -1071,6 +1072,17 @@ def create_schedule(ctx):
pass


def serialize_quantity_or_sensor(qos: Sensor | ur.Quantity) -> dict | str:
if isinstance(qos, Sensor):
return {"sensor": qos.id}
elif isinstance(qos, ur.Quantity):
return str(qos)
else:
raise TypeError(
f"`qos` function argument type can be either a Sensor or ur.Quantity. `{type(qos)}` was provided."
)


@create_schedule.command("for-storage", cls=DeprecatedOptionsCommand)
@with_appcontext
@click.option(
Expand Down Expand Up @@ -1168,14 +1180,85 @@ def create_schedule(ctx):
default=1,
help="Round-trip efficiency (e.g. 85% or 0.85) to use for the schedule. Defaults to 100% (no losses).",
)
@click.option(
"--charging-efficiency",
"charging_efficiency",
type=QuantityOrSensor("%"),
required=False,
default=None,
help="Storage charging efficiency to use for the schedule."
"Provide a quantity with units (e.g. 94%) or a sensor storing the value with the syntax sensor:<id> (e.g. sensor:20)."
"Defaults to 100% (no losses).",
)
@click.option(
"--discharging-efficiency",
"discharging_efficiency",
type=QuantityOrSensor("%"),
required=False,
default=None,
help="Storage discharging efficiency to use for the schedule."
"Provide a quantity with units (e.g. 94%) or a sensor storing the value with the syntax sensor:<id> (e.g. sensor:20)."
"Defaults to 100% (no losses).",
)
@click.option(
"--soc-gain",
"soc_gain",
type=QuantityOrSensor("MW"),
required=False,
default=None,
help="Specify the State of Charge (SoC) gain as a quantity in power units (e.g. 1 MW or 1000 kW)"
"or reference a sensor by using 'sensor:<id>' (e.g. sensor:34)."
"This represents the rate at which storage is charged from a different source.",
)
@click.option(
"--soc-usage",
"soc_usage",
type=QuantityOrSensor("MW"),
required=False,
default=None,
help="Specify the State of Charge (SoC) usage as a quantity in power units (e.g. 1 MW or 1000 kW) "
"or reference a sensor by using 'sensor:<id>' (e.g. sensor:34)."
"This represents the rate at which the storage is discharged from a different source.",
)
@click.option(
"--storage-power-capacity",
"storage_power_capacity",
type=QuantityOrSensor("MW"),
required=False,
default=None,
help="Storage consumption/production power capacity. Provide this as a quantity in power units (e.g. 1 MW or 1000 kW)"
"or reference a sensor using 'sensor:<id>' (e.g. sensor:34)."
"It defines both-ways maximum power capacity.",
)
@click.option(
"--storage-consumption-capacity",
"storage_consumption_capacity",
type=QuantityOrSensor("MW"),
required=False,
default=None,
help="Storage consumption power capacity. Provide this as a quantity in power units (e.g. 1 MW or 1000 kW)"
"or reference a sensor using 'sensor:<id>' (e.g. sensor:34)."
"It defines the storage maximum consumption (charging) capacity.",
)
@click.option(
"--storage-production-capacity",
"storage_production_capacity",
type=QuantityOrSensor("MW"),
required=False,
default=None,
help="Storage production power capacity. Provide this as a quantity in power units (e.g. 1 MW or 1000 kW)"
"or reference a sensor using 'sensor:<id>' (e.g. sensor:34)."
"It defines the storage maximum production (discharging) capacity.",
)
@click.option(
"--storage-efficiency",
"storage_efficiency",
type=EfficiencyField(),
type=QuantityOrSensor("%", default_src_unit="dimensionless"),
required=False,
default=1,
help="Storage efficiency (e.g. 95% or 0.95) to use for the schedule,"
" applied over each time step equal to the sensor resolution."
"This parameter also supports using a reference sensor as 'sensor:<id>' (e.g. sensor:34)."
" For example, a storage efficiency of 99 percent per (absolute) day, for scheduling a 1-hour resolution sensor, should be passed as a storage efficiency of 0.99**(1/24)."
" Defaults to 100% (no losses).",
)
Expand All @@ -1185,7 +1268,7 @@ def create_schedule(ctx):
help="Whether to queue a scheduling job instead of computing directly. "
"To process the job, run a worker (on any computer, but configured to the same databases) to process the 'scheduling' queue. Defaults to False.",
)
def add_schedule_for_storage(
def add_schedule_for_storage( # noqa C901
power_sensor: Sensor,
consumption_price_sensor: Sensor,
production_price_sensor: Sensor,
Expand All @@ -1194,11 +1277,18 @@ def add_schedule_for_storage(
start: datetime,
duration: timedelta,
soc_at_start: ur.Quantity,
charging_efficiency: ur.Quantity | Sensor | None,
discharging_efficiency: ur.Quantity | Sensor | None,
soc_gain: ur.Quantity | Sensor | None,
soc_usage: ur.Quantity | Sensor | None,
storage_power_capacity: ur.Quantity | Sensor | None,
storage_consumption_capacity: ur.Quantity | Sensor | None,
storage_production_capacity: ur.Quantity | Sensor | None,
soc_target_strings: list[tuple[ur.Quantity, str]],
soc_min: ur.Quantity | None = None,
soc_max: ur.Quantity | None = None,
roundtrip_efficiency: ur.Quantity | None = None,
storage_efficiency: ur.Quantity | None = None,
storage_efficiency: ur.Quantity | Sensor | None = None,
as_job: bool = False,
):
"""Create a new schedule for a storage asset.
Expand Down Expand Up @@ -1256,8 +1346,6 @@ def add_schedule_for_storage(
soc_max = convert_units(soc_max.magnitude, str(soc_max.units), "MWh", capacity=capacity_str) # type: ignore
if roundtrip_efficiency is not None:
roundtrip_efficiency = roundtrip_efficiency.magnitude / 100.0
if storage_efficiency is not None:
storage_efficiency = storage_efficiency.magnitude / 100.0

scheduling_kwargs = dict(
start=start,
Expand All @@ -1271,14 +1359,49 @@ def add_schedule_for_storage(
"soc-max": soc_max,
"soc-unit": "MWh",
"roundtrip-efficiency": roundtrip_efficiency,
"storage-efficiency": storage_efficiency,
},
flex_context={
"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],
},
)

if charging_efficiency is not None:
scheduling_kwargs["flex_model"][
"charging-efficiency"
] = serialize_quantity_or_sensor(charging_efficiency)
if discharging_efficiency is not None:
scheduling_kwargs["flex_model"][
"discharging-efficiency"
] = serialize_quantity_or_sensor(discharging_efficiency)
if storage_efficiency is not None:
scheduling_kwargs["flex_model"][
"storage-efficiency"
] = serialize_quantity_or_sensor(storage_efficiency)

if soc_gain is not None:
scheduling_kwargs["flex_model"]["soc-gain"] = serialize_quantity_or_sensor(
soc_gain
)
if soc_usage is not None:
scheduling_kwargs["flex_model"]["soc-usage"] = serialize_quantity_or_sensor(
soc_usage
)

if storage_power_capacity is not None:
scheduling_kwargs["flex_model"][
"power-capacity"
] = serialize_quantity_or_sensor(storage_power_capacity)
if storage_consumption_capacity is not None:
scheduling_kwargs["flex_model"][
"consumption-capacity"
] = serialize_quantity_or_sensor(storage_consumption_capacity)
if storage_production_capacity is not None:
scheduling_kwargs["flex_model"][
"production-capacity"
] = serialize_quantity_or_sensor(storage_production_capacity)

if as_job:
job = create_scheduling_job(asset_or_sensor=power_sensor, **scheduling_kwargs)
if job:
Expand Down
2 changes: 1 addition & 1 deletion flexmeasures/data/schemas/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,6 @@
from .account import AccountIdField
from .generic_assets import GenericAssetIdField as AssetIdField
from .locations import LatitudeField, LongitudeField
from .sensors import SensorIdField
from .sensors import SensorIdField, QuantityOrSensor
from .sources import DataSourceIdField as SourceIdField
from .times import AwareDateTimeField, DurationField, TimeIntervalField
11 changes: 11 additions & 0 deletions flexmeasures/data/schemas/sensors.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from pint import DimensionalityError

import json
import re

from flexmeasures.data import ma
from flexmeasures.data.models.generic_assets import GenericAsset
Expand Down Expand Up @@ -156,3 +157,13 @@ def _serialize(
raise FMValidationError(
"Serialized Quantity Or Sensor needs to be of type int, float or Sensor"
)

def convert(self, value, param, ctx, **kwargs):
_value = re.match(r"sensor:(\d+)", value)

if _value is not None:
_value = {"sensor": int(_value.groups()[0])}
else:
_value = value

return super().convert(_value, param, ctx, **kwargs)
39 changes: 39 additions & 0 deletions flexmeasures/data/schemas/tests/test_sensor.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import pytest
from flexmeasures import Sensor
from flexmeasures.data.schemas.sensors import QuantityOrSensor
from flexmeasures.utils.unit_utils import ur
from marshmallow import ValidationError


Expand Down Expand Up @@ -55,3 +57,40 @@ def test_quantity_or_sensor_conversion(

schema = QuantityOrSensor(to_unit="MW")
assert schema.deserialize(src_quantity).magnitude == expected_magnitude


@pytest.mark.parametrize(
"sensor_id, input_param, dst_unit, fails",
[
# deserialize a sensor
(1, "sensor:1", "MWh", False),
(1, "sensor:1", "kWh", False),
(1, "sensor:1", "kW", False),
(1, "sensor:1", "EUR", True),
(2, "sensor:2", "EUR/kWh", False),
(2, "sensor:2", "EUR", True),
# deserialize a quantity
(None, "1MWh", "MWh", False),
(None, "1 MWh", "kWh", False),
(None, "1 MWh", "kW", True),
(None, "100 EUR/MWh", "EUR/kWh", False),
(None, "100 EUR/MWh", "EUR", True),
],
)
def test_quantity_or_sensor_field(
setup_dummy_sensors, sensor_id, input_param, dst_unit, fails
):

field = QuantityOrSensor(to_unit=dst_unit)

try:
if sensor_id is None:
val = field.convert(input_param, None, None)
assert val.units == ur.Unit(dst_unit)
else:
val = field.convert(input_param, None, None)
assert val == Sensor.query.get(sensor_id)

assert not fails
except Exception:
assert fails

0 comments on commit 9b823a8

Please sign in to comment.