Skip to content

StorageScheduler overwrites schedules when multiple sessions use the same sensor #1947

@Ahmad-Wahid

Description

@Ahmad-Wahid

When scheduling multiple EV charging sessions that share the same power sensor, the StorageScheduler currently overwrites earlier schedules instead of combining them. As a result, only the last session is effectively scheduled, even though multiple sessions are defined in the flex-model.

This happens when the flex-model contains multiple storage entries referencing the same sensor (e.g. multiple EVSE sessions on a single charge point), which is a valid and common modeling pattern.


Problematic behavior

The issue originates from this code in StorageScheduler.compute:

storage_schedule = {
    sensor: ems_schedule[d]
    for d, sensor in enumerate(sensors)
    if sensor is not None
}

If the same sensor appears multiple times, earlier schedules are silently overwritten.


Expected behavior

If multiple flex-model entries reference the same sensor, the scheduler should:

  • Compute each session independently
  • Aggregate (sum) their schedules
  • Return one combined schedule per sensor

Fix

The issue is resolved by accumulating schedules instead of overwriting them:

for d, sensor in enumerate(sensors):
    if sensor is not None and sensor not in storage_schedule:
        storage_schedule[sensor] = ems_schedule[d]
    elif sensor is not None and sensor in storage_schedule:
        storage_schedule[sensor] += ems_schedule[d]

This correctly supports:

  • Single session
  • Overlapping sessions
  • Non-overlapping sessions

Reproduction test case

The following test demonstrates the issue and validates the fix.

import pandas as pd
import pytest
import timely_beliefs as tb
from flexmeasures import Sensor, Source
from flexmeasures.data.models.generic_assets import GenericAsset, GenericAssetType
from flexmeasures.data.services.scheduling import StorageScheduler
from flexmeasures.data.services.utils import get_or_create_model
from flexmeasures.data.utils import save_to_db
pd.set_option("display.max_rows", None)

def load_pv_production(
    *,
    pv_power_sensor: Sensor,
    start: pd.Timestamp,
    end: pd.Timestamp,
    resolution: pd.Timedelta,
    db,
):
    """
    Create deterministic PV production data and store it on the PV power sensor.
    Convention: production = negative consumption.
    """
    source = get_or_create_model(Source, name="PV forecast")

    index = pd.date_range(start=start, end=end, freq=resolution, inclusive="left")

    # Simple deterministic PV profile (kW)
    values = []
    for ts in index:
        hour = ts.hour + ts.minute / 60
        if 6 <= hour <= 18:
            # bell-ish shape
            values.append(-max(0, 25 * (1 - abs(hour - 12) / 6)))
        else:
            values.append(0.0)

    bdf = tb.BeliefsDataFrame(
        pd.Series(values, index=index, name="event_value"),
        belief_horizon=pd.Timedelta(0),
        sensor=pv_power_sensor,
        source=source,
    )

    save_to_db(bdf, bulk_save_objects=True, save_changed_beliefs_only=True)
    db.session.commit()


def build_flex_model(case, pv_power_sensor, evse_power_sensor):
    base_pv = {
        "sensor": pv_power_sensor.id,
        "consumption-capacity": "0 kW",
        "production-capacity": {"sensor": pv_power_sensor.id},
        "power-capacity": "1 GW",
    }

    if case == "single_session":
        return [
            base_pv,
            {
                "sensor": evse_power_sensor.id,
                "soc-at-start": 20.0,
                "soc-min": 0.0,
                "soc-max": 100.0,
                "soc-targets": [
                    {"datetime": "2023-01-01T11:45:00+01:00", "value": 100.0}
                ],
                "soc-minima": [
                    {"datetime": "2023-01-01T11:45:00+01:00", "value": 97.0}
                ],
                "soc-maxima": [
                    {"datetime": "2023-01-01T04:15:00+01:00", "value": 25.5}
                ],
                "power-capacity": [
                    {
                        "start": "2023-01-01T00:00:00+01:00",
                        "end": "2023-01-01T04:15:00+01:00",
                        "value": "0 kW",
                    },
                    {
                        "start": "2023-01-01T04:15:00+01:00",
                        "end": "2023-01-01T11:45:00+01:00",
                        "value": "22 kW",
                    },
                    {
                        "start": "2023-01-01T11:45:00+01:00",
                        "end": "2023-01-02T00:00:00+01:00",
                        "value": "0 kW",
                    },
                ],
                "charging-efficiency": 0.9486832980505138,
                "production-capacity": "0 kW",
            },
        ]

    if case == "overlap_sessions":
        return [
            base_pv,
            # Session 1
            {
                "sensor": evse_power_sensor.id,
                "soc-at-start": 20.0,
                "soc-min": 0.0,
                "soc-max": 100.0,
                "soc-targets": [
                    {"datetime": "2023-01-01T11:45:00+01:00", "value": 100.0}
                ],
                "soc-minima": [
                    {"datetime": "2023-01-01T11:45:00+01:00", "value": 97.0}
                ],
                "soc-maxima": [
                    {"datetime": "2023-01-01T04:15:00+01:00", "value": 25.5}
                ],
                "power-capacity": [
                    {
                        "start": "2023-01-01T00:00:00+01:00",
                        "end": "2023-01-01T04:15:00+01:00",
                        "value": "0 kW",
                    },
                    {
                        "start": "2023-01-01T04:15:00+01:00",
                        "end": "2023-01-01T10:15:00+01:00",
                        "value": "22 kW",
                    },
                    {
                        "start": "2023-01-01T10:15:00+01:00",
                        "end": "2023-01-01T11:45:00+01:00",
                        "value": "11 kW",
                    },
                    {
                        "start": "2023-01-01T11:45:00+01:00",
                        "end": "2023-01-02T00:00:00+01:00",
                        "value": "0 kW",
                    },
                ],
                "charging-efficiency": 0.9486832980505138,
                "production-capacity": "0 kW",
            },
            # Session 2
            {
                "sensor": evse_power_sensor.id,
                "soc-at-start": 30.0,
                "soc-min": 0.0,
                "soc-max": 100.0,
                "soc-targets": [
                    {"datetime": "2023-01-01T18:15:00+01:00", "value": 100.0}
                ],
                "soc-minima": [
                    {"datetime": "2023-01-01T18:15:00+01:00", "value": 97.0}
                ],
                "soc-maxima": [
                    {"datetime": "2023-01-01T10:15:00+01:00", "value": 35.0}
                ],
                "power-capacity": [
                    {
                        "start": "2023-01-01T00:00:00+01:00",
                        "end": "2023-01-01T10:15:00+01:00",
                        "value": "0 kW",
                    },
                    {
                        "start": "2023-01-01T10:15:00+01:00",
                        "end": "2023-01-01T11:45:00+01:00",
                        "value": "11 kW",
                    },
                    {
                        "start": "2023-01-01T11:45:00+01:00",
                        "end": "2023-01-01T18:15:00+01:00",
                        "value": "22 kW",
                    },
                    {
                        "start": "2023-01-01T18:15:00+01:00",
                        "end": "2023-01-02T00:00:00+01:00",
                        "value": "0 kW",
                    },
                ],
                "charging-efficiency": 0.9486832980505138,
                "production-capacity": "0 kW",
            },
        ]

    if case == "non_overlap_sessions":
        return [
            base_pv,
            {
                "sensor": evse_power_sensor.id,
                "soc-at-start": 20.0,
                "soc-min": 0.0,
                "soc-max": 100.0,
                "soc-targets": [
                    {"datetime": "2023-01-01T11:45:00+01:00", "value": 100.0}
                ],
                "soc-minima": [
                    {"datetime": "2023-01-01T11:45:00+01:00", "value": 97.0}
                ],
                "soc-maxima": [
                    {"datetime": "2023-01-01T04:15:00+01:00", "value": 25.5}
                ],
                "power-capacity": [
                    {
                        "start": "2023-01-01T00:00:00+01:00",
                        "end": "2023-01-01T04:15:00+01:00",
                        "value": "0 kW",
                    },
                    {
                        "start": "2023-01-01T04:15:00+01:00",
                        "end": "2023-01-01T11:45:00+01:00",
                        "value": "22 kW",
                    },
                    {
                        "start": "2023-01-01T11:45:00+01:00",
                        "end": "2023-01-02T00:00:00+01:00",
                        "value": "0 kW",
                    },
                ],
                "charging-efficiency": 0.9486832980505138,
                "production-capacity": "0 kW",
            },
            {
                "sensor": evse_power_sensor.id,
                "soc-at-start": 30.0,
                "soc-min": 0.0,
                "soc-max": 100.0,
                "soc-targets": [
                    {"datetime": "2023-01-01T18:15:00+01:00", "value": 100.0}
                ],
                "soc-minima": [
                    {"datetime": "2023-01-01T18:15:00+01:00", "value": 97.0}
                ],
                "soc-maxima": [
                    {"datetime": "2023-01-01T12:15:00+01:00", "value": 35.0}
                ],
                "power-capacity": [
                    {
                        "start": "2023-01-01T00:00:00+01:00",
                        "end": "2023-01-01T12:15:00+01:00",
                        "value": "0 kW",
                    },
                    {
                        "start": "2023-01-01T12:15:00+01:00",
                        "end": "2023-01-01T18:15:00+01:00",
                        "value": "22 kW",
                    },
                    {
                        "start": "2023-01-01T18:15:00+01:00",
                        "end": "2023-01-02T00:00:00+01:00",
                        "value": "0 kW",
                    },
                ],
                "charging-efficiency": 0.9486832980505138,
                "production-capacity": "0 kW",
            },
        ]

    raise ValueError(case)


@pytest.mark.parametrize(
    "case",
    ["single_session", "overlap_sessions", "non_overlap_sessions"],
)
def test_multi_asset_schedule(app, db, setup_reporters, simulation_datalake, case):
    pv_asset_type = get_or_create_model(GenericAssetType, name="pv")
    evse_asset_type = get_or_create_model(GenericAssetType, name="evse")

    start = pd.Timestamp("2023-01-01T00:00:00+01:00")
    end = pd.Timestamp("2023-01-02T00:00:00+01:00")
    resolution = pd.Timedelta("15min")

    pv = GenericAsset(name="PV", generic_asset_type=pv_asset_type)
    evse = GenericAsset(
        name="EVSE",
        generic_asset_type=evse_asset_type,
        attributes={"energy-capacity": "100 kWh"},
    )

    db.session.add_all([pv, evse])
    db.session.commit()

    pv_power_sensor = Sensor(
        name="pv power",
        unit="kW",
        event_resolution=resolution,
        generic_asset=pv,
        attributes={"consumption_is_positive": False},
    )

    evse_power_sensor = Sensor(
        name="evse power",
        unit="kW",
        event_resolution=resolution,
        generic_asset=evse,
    )

    db.session.add_all([pv_power_sensor, evse_power_sensor])
    db.session.commit()

    # ---- load PV data (no files)
    load_pv_production(
        pv_power_sensor=pv_power_sensor,
        start=start,
        end=end,
        resolution=resolution,
        db=db,
    )

    flex_model = build_flex_model(case, pv_power_sensor, evse_power_sensor)

    scheduler = StorageScheduler(
        asset_or_sensor=evse,
        start=start,
        end=end,
        resolution=resolution,
        belief_time=start,
        flex_model=flex_model,
        flex_context={
            "consumption-price": "50 EUR/MWh",
            "production-price": "50 EUR/MWh",
        },
        return_multiple=True,
    )


    schedules = scheduler.compute(skip_validation=True)

    breakpoint()
    assert isinstance(schedules, list)
    sensors = {s["sensor"] for s in schedules}
    assert pv_power_sensor in sensors
    assert evse_power_sensor in sensors

Metadata

Metadata

Assignees

No one assigned

    Labels

    documentationImprovements or additions to documentationenhancementNew feature or request

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions