Skip to content

Commit

Permalink
Backport PR #1047: [fix] skip conversion of SOC targets minima or max…
Browse files Browse the repository at this point in the history
…ima are defined as Sensors (#1047)

* fix: skip conversion of soc_targets, soc_minima and soc_maxima if they are defined from sensors.

Signed-off-by: Victor Garcia Reolid <victor@seita.nl>

* add test

Signed-off-by: Victor Garcia Reolid <victor@seita.nl>

* style: small review fixes

Signed-off-by: F.N. Claessen <felix@seita.nl>

* fix: remove confusing attribute for SoC sensors

Signed-off-by: F.N. Claessen <felix@seita.nl>

* docs: clarify fixture

Signed-off-by: F.N. Claessen <felix@seita.nl>

* docs: changelog entry

Signed-off-by: F.N. Claessen <felix@seita.nl>

---------

Signed-off-by: Victor Garcia Reolid <victor@seita.nl>
Signed-off-by: F.N. Claessen <felix@seita.nl>
Co-authored-by: F.N. Claessen <felix@seita.nl>
(cherry picked from commit db69234)
  • Loading branch information
victorgarcia98 authored and Flix6x committed May 6, 2024
1 parent 1ff6438 commit b825555
Show file tree
Hide file tree
Showing 4 changed files with 151 additions and 5 deletions.
3 changes: 2 additions & 1 deletion documentation/changelog.rst
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ Bugfixes
-----------

* Prevent **p**lay/**p**ause/**s**top of replays when editing a text field in the UI [see `PR #1024 <https://github.com/FlexMeasures/flexmeasures/pull/1024>`_]
* Skip unit conversion of :abbr:`SoC (state of charge)` related fields that are defined as sensors in a ``flex-model`` (specifically, ``soc-maxima``, ``soc-minima`` and ``soc-targets`` [see `PR #1047 <https://github.com/FlexMeasures/flexmeasures/pull/1047>`_]

v0.20.0 | March 26, 2024
Expand Down Expand Up @@ -128,7 +129,7 @@ New features
* Better navigation experience through listings (sensors / assets / users / accounts) in the :abbr:`UI (user interface)`, by heading to the selected entity upon a click (or CTRL + click) anywhere within a row [see `PR #923 <https://github.com/FlexMeasures/flexmeasures/pull/923>`_]
* Introduce a breadcrumb to navigate through assets and sensor pages using its child-parent relationship [see `PR #930 <https://github.com/FlexMeasures/flexmeasures/pull/930>`_]
* Define device-level power constraints as sensors to create schedules with changing power limits [see `PR #897 <https://github.com/FlexMeasures/flexmeasures/pull/897>`_]
* Allow to provide external storage usage or gain components using the ``soc-usage`` and ``soc-gain`` fields of the `flex-model` [see `PR #906 <https://github.com/FlexMeasures/flexmeasures/pull/906>`_]
* Allow to provide external storage usage or gain components using the ``soc-usage`` and ``soc-gain`` fields of the ``flex-model`` [see `PR #906 <https://github.com/FlexMeasures/flexmeasures/pull/906>`_]
* Define time-varying charging and discharging efficiencies as sensors or as constant values which allows to define the :abbr:`COP (coefficient of performance)` [see `PR #933 <https://github.com/FlexMeasures/flexmeasures/pull/933>`_]

Infrastructure / Support
Expand Down
68 changes: 67 additions & 1 deletion flexmeasures/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -1185,11 +1185,77 @@ def capacity_sensors(db, add_battery_assets, setup_sources):
)


@pytest.fixture(scope="module")
def soc_sensors(db, add_battery_assets, setup_sources) -> tuple:
"""Add battery sensors for instantaneous soc-maxima (in kWh), soc-maxima (in MWh) and soc-targets (in MWh).
The SoC values on each sensor linearly increase from 0 to 5 MWh.
"""
battery = add_battery_assets["Test battery with dynamic power capacity"]

soc_maxima = Sensor(
name="soc_maxima",
generic_asset=battery,
unit="kWh",
event_resolution=timedelta(0),
)

soc_minima = Sensor(
name="soc_minima",
generic_asset=battery,
unit="MWh",
event_resolution=timedelta(0),
)

soc_targets = Sensor(
name="soc_targets",
generic_asset=battery,
unit="MWh",
event_resolution=timedelta(0),
)

db.session.add_all([soc_maxima, soc_minima, soc_targets])
db.session.flush()

time_slots = pd.date_range(
datetime(2015, 1, 1, 2), datetime(2015, 1, 2), freq="15T"
).tz_localize("Europe/Amsterdam")

values = np.arange(len(time_slots)) / (len(time_slots) - 1)
values = values * 5

add_beliefs(
db=db,
sensor=soc_maxima,
time_slots=time_slots,
values=values * 1000, # MWh -> kWh
source=setup_sources["Seita"],
)

add_beliefs(
db=db,
sensor=soc_minima,
time_slots=time_slots,
values=values,
source=setup_sources["Seita"],
)

add_beliefs(
db=db,
sensor=soc_targets,
time_slots=time_slots,
values=values,
source=setup_sources["Seita"],
)

yield soc_maxima, soc_minima, soc_targets, values


def add_beliefs(
db,
sensor: Sensor,
time_slots: pd.DatetimeIndex,
values: list[int | float],
values: list[int | float] | np.ndarray,
source: DataSource,
):
beliefs = [
Expand Down
70 changes: 70 additions & 0 deletions flexmeasures/data/models/planning/tests/test_solver.py
Original file line number Diff line number Diff line change
Expand Up @@ -1968,3 +1968,73 @@ def test_add_storage_constraint_from_sensor(
equals[expected_target_start + resolution : expected_target_end]
== expected_soc_target_value
)


def test_soc_maxima_minima_targets(db, add_battery_assets, soc_sensors):
"""
Check that the SOC maxima, minima and targets can be defined as sensors in the StorageScheduler.
The SOC is forced to follow a certain trajectory both by means of the SOC target and by setting SOC maxima = SOC minima = SOC targets.
Moreover, the SOC maxima constraints are defined in MWh to check that the unit conversion works well.
"""
power = add_battery_assets["Test battery with dynamic power capacity"].sensors[0]
epex_da = get_test_sensor(db)

soc_maxima, soc_minima, soc_targets, values = soc_sensors

tz = pytz.timezone("Europe/Amsterdam")
start = tz.localize(datetime(2015, 1, 1))
end = tz.localize(datetime(2015, 1, 2))
resolution = timedelta(minutes=15)
soc_at_start = 0.0
soc_max = 10
soc_min = 0

flex_model = {
"soc-at-start": soc_at_start,
"soc-max": soc_max,
"soc-min": soc_min,
"power-capacity": "2 MW",
"production-capacity": "2 MW",
"consumption-capacity": "2 MW",
"storage-efficiency": 1,
"charging-efficiency": "100%",
"discharging-efficiency": "100%",
}

def compute_schedule(flex_model):
scheduler: Scheduler = StorageScheduler(
power,
start,
end,
resolution,
flex_model=flex_model,
flex_context={
"site-power-capacity": "100 MW",
"production-price-sensor": epex_da.id,
"consumption-price-sensor": epex_da.id,
},
)
return scheduler.compute()

flex_model["soc-targets"] = {"sensor": soc_targets.id}
schedule = compute_schedule(flex_model)

soc = check_constraints(power, schedule, soc_at_start)

# soc targets are achieved
assert all(abs(soc[9:].values - values[:-1]) < 1e-5)

# remove soc-targets and use soc-maxima and soc-minima
del flex_model["soc-targets"]
flex_model["soc-minima"] = {"sensor": soc_minima.id}
flex_model["soc-maxima"] = {"sensor": soc_maxima.id}
schedule = compute_schedule(flex_model)

soc = check_constraints(power, schedule, soc_at_start)

# soc-maxima and soc-minima constraints are respected
# this yields the same results as with the SOC targets
# because soc-maxima = soc-minima = soc-targets
assert all(abs(soc[9:].values - values[:-1]) < 1e-5)
15 changes: 12 additions & 3 deletions flexmeasures/data/schemas/scheduling/storage.py
Original file line number Diff line number Diff line change
Expand Up @@ -194,13 +194,22 @@ def post_load_sequence(self, data: dict, **kwargs) -> dict:
data["soc_min"] /= 1000.0
if data.get("soc_max") is not None:
data["soc_max"] /= 1000.0
if data.get("soc_targets"):
if (
not isinstance(data.get("soc_targets"), Sensor)
and data.get("soc_targets") is not None
):
for target in data["soc_targets"]:
target["value"] /= 1000.0
if data.get("soc_minima"):
if (
not isinstance(data.get("soc_minima"), Sensor)
and data.get("soc_minima") is not None
):
for minimum in data["soc_minima"]:
minimum["value"] /= 1000.0
if data.get("soc_maxima"):
if (
not isinstance(data.get("soc_maxima"), Sensor)
and data.get("soc_maxima") is not None
):
for maximum in data["soc_maxima"]:
maximum["value"] /= 1000.0
data["soc_unit"] = "MWh"
Expand Down

0 comments on commit b825555

Please sign in to comment.