Skip to content

Battery pool bounds are not calculated correctly #1180

Open
@llucax

Description

@llucax

What happened?

In tests/timeseries/_battery_pool/test_battery_pool.py, test_all_batteries_power_bounds, the first test scenario is something like this:

    for battery_id, inverter_ids in bat_invs_map.items():
        # Sampling rate choose to reflect real application.
        streamer.start_streaming(
            BatteryDataWrapper(
                component_id=battery_id,
                timestamp=datetime.now(tz=timezone.utc),
                power_inclusion_lower_bound=-1000,
                power_inclusion_upper_bound=5000,
                power_exclusion_lower_bound=-300,
                power_exclusion_upper_bound=300,
            ),
            sampling_rate=0.05,
        )
        for inverter_id in inverter_ids:
            streamer.start_streaming(
                InverterDataWrapper(
                    component_id=inverter_id,
                    timestamp=datetime.now(tz=timezone.utc),
                    active_power_inclusion_lower_bound=-900,
                    active_power_inclusion_upper_bound=6000,
                    active_power_exclusion_lower_bound=-200,
                    active_power_exclusion_upper_bound=200,
                ),
                sampling_rate=0.1,
            )

    # pylint: disable=protected-access
    receiver = battery_pool._system_power_bounds.new_receiver(limit=50)
    # pylint: enable=protected-access

    # First metrics delivers slower because of the startup delay in the pool.
    msg = await asyncio.wait_for(
        receiver.receive(), timeout=WAIT_FOR_COMPONENT_DATA_SEC + 0.2
    )
    now = datetime.now(tz=timezone.utc)
    expected = SystemBounds(
        timestamp=now,
        inclusion_bounds=Bounds(Power.from_watts(-1800), Power.from_watts(10000)),
        exclusion_bounds=Bounds(Power.from_watts(-600), Power.from_watts(600)),
    )
    compare_messages(msg, expected)

    batteries_in_pool = list(battery_pool.component_ids)

    scenarios: list[Scenario[SystemBounds]] = [
        Scenario(
            next(iter(bat_invs_map[batteries_in_pool[0]])),
            {
                "active_power_inclusion_lower_bound": -100,
                "active_power_exclusion_lower_bound": -400,
            },
            SystemBounds(
                timestamp=now,
                inclusion_bounds=Bounds(Power.from_watts(-1000), Power.from_watts(10000)),
                exclusion_bounds=Bounds(Power.from_watts(-700), Power.from_watts(600)),
            ),
        ),

Adding some logging, this is the result of the initial state (we have battery 5 with inverter 4 and battery 8 with inverter 7):

INFO     frequenz.sdk.timeseries.battery_pool._metric_calculator:_metric_calculator.py:545 working_batteries={8, 5}
WARNING  frequenz.sdk.timeseries.battery_pool._metric_calculator:_metric_calculator.py:591 >>> battery_ids=frozenset({5}) inverter_ids=frozenset({4}) battery_bounds=[PowerBounds(inclusion_lower=-1000, exclusion_lower=-300, exclusion_upper=300, inclusion_upper=5000)]
WARNING  frequenz.sdk.timeseries.battery_pool._metric_calculator:_metric_calculator.py:596 >>> aggregated_bat_bounds=PowerBounds(inclusion_lower=-1000, exclusion_lower=-300, exclusion_upper=300, inclusion_upper=5000)
WARNING  frequenz.sdk.timeseries.battery_pool._metric_calculator:_metric_calculator.py:599 >>> inverter_bounds=[PowerBounds(inclusion_lower=-900, exclusion_lower=-200, exclusion_upper=200, inclusion_upper=6000)]
WARNING  frequenz.sdk.timeseries.battery_pool._metric_calculator:_metric_calculator.py:591 >>> battery_ids=frozenset({8}) inverter_ids=frozenset({7}) battery_bounds=[PowerBounds(inclusion_lower=-1000, exclusion_lower=-300, exclusion_upper=300, inclusion_upper=5000)]
WARNING  frequenz.sdk.timeseries.battery_pool._metric_calculator:_metric_calculator.py:596 >>> aggregated_bat_bounds=PowerBounds(inclusion_lower=-1000, exclusion_lower=-300, exclusion_upper=300, inclusion_upper=5000)
WARNING  frequenz.sdk.timeseries.battery_pool._metric_calculator:_metric_calculator.py:599 >>> inverter_bounds=[PowerBounds(inclusion_lower=-900, exclusion_lower=-200, exclusion_upper=200, inclusion_upper=6000)]
WARNING  frequenz.sdk.timeseries.battery_pool._metric_calculator:_metric_calculator.py:649 Calculated Power Bounds: inclusion=[-1800.0, 10000.0] exclusion=[-600.0, 600.0]

So we have the same data for both battery chains, that look like:

             ____________________ incl ________________________________________
            |                                                                  |
INV   ------|xxxxxxxxxxx|---------|xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx|-----
         -900        -200       200                                           6000
           :           |_ excl __|
           :
          ______________________ incl ____________________________________
         | :                                                              |
BAT  ----|xxxxxxxxxxx|-------------|xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx|----------
       -1000        -300          300                                   5000
           :         |___ excl ____|                                      :
           :         :             :                                      :
           :         :             :                                      :
           :____________________ incl ____________________________________:
           |         :             :                                      |
RES  ------|xxxxxxxxx|-------------|xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx|----------
         -900       -300          300                                   5000
                     |___ excl ____|

So the end result is twice these bounds, as it is one per battery chain:

incl = [-1_800, 10_000]
excl = [-600, 600]

For the first test scenario, the inverter 7 (for battery 8) gets new bounds:

inclusion_lower=-100, exclusion_lower=-400, exclusion_upper=200, inclusion_upper=6000

Note that the exclusion bounds are not contained in the inclusion bounds.

This is the extra info for the tests:

INFO     frequenz.sdk.timeseries.battery_pool._metric_calculator:_metric_calculator.py:545 working_batteries={8, 5}
WARNING  frequenz.sdk.timeseries.battery_pool._metric_calculator:_metric_calculator.py:591 >>> battery_ids=frozenset({5}) inverter_ids=frozenset({4}) battery_bounds=[PowerBounds(inclusion_lower=-1000, exclusion_lower=-300, exclusion_upper=300, inclusion_upper=5000)]
WARNING  frequenz.sdk.timeseries.battery_pool._metric_calculator:_metric_calculator.py:596 >>> aggregated_bat_bounds=PowerBounds(inclusion_lower=-1000, exclusion_lower=-300, exclusion_upper=300, inclusion_upper=5000)
WARNING  frequenz.sdk.timeseries.battery_pool._metric_calculator:_metric_calculator.py:599 >>> inverter_bounds=[PowerBounds(inclusion_lower=-900, exclusion_lower=-200, exclusion_upper=200, inclusion_upper=6000)]
WARNING  frequenz.sdk.timeseries.battery_pool._metric_calculator:_metric_calculator.py:591 >>> battery_ids=frozenset({8}) inverter_ids=frozenset({7}) battery_bounds=[PowerBounds(inclusion_lower=-1000, exclusion_lower=-300, exclusion_upper=300, inclusion_upper=5000)]
WARNING  frequenz.sdk.timeseries.battery_pool._metric_calculator:_metric_calculator.py:596 >>> aggregated_bat_bounds=PowerBounds(inclusion_lower=-1000, exclusion_lower=-300, exclusion_upper=300, inclusion_upper=5000)
WARNING  frequenz.sdk.timeseries.battery_pool._metric_calculator:_metric_calculator.py:599 >>> inverter_bounds=[PowerBounds(inclusion_lower=-100, exclusion_lower=-400, exclusion_upper=200, inclusion_upper=6000)]
WARNING  frequenz.sdk.timeseries.battery_pool._metric_calculator:_metric_calculator.py:649 Calculated Power Bounds: inclusion=[-1000.0, 10000.0] exclusion=[-700.0, 600.0]

So the initial data is:

And this is how this looks like:

             ____________________ incl ________________________________________
            |                                                                  |
INV4  ------|xxxxxxxxxxx|---------|xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx|-----
         -900        -200       200                                           6000
           :           |_ excl __|
           :
          ______________________ incl ____________________________________
         | :                                                              |
BAT5 ----|xxxxxxxxxxx|-------------|xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx|----------
       -1000        -300          300                                   5000
           :         |___ excl ____|                                      :
           :         :             :                                      :
           :         :             :                                      :
           :____________________ incl ____________________________________:
           |         :             :                                      |
RES  ------|xxxxxxxxx|-------------|xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx|----------
         -900       -300          300                                   5000
                     |___ excl ____|
                           ____________________ incl __________________________
                          |                                                    |
INV7  -----------|--------|------|xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx|-----
               -400     -100    200                                           6000
                 |_____ excl ____|

          ______________________ incl ____________________________________
         |                                                                |
BAT8 ----|xxxxxxxxxxx|-------------|xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx|----------
       -1000        -300          300                                   5000
                     |___ excl ____|                                      :
                                   :                                      :
                                   :                                      :
                                   :__________________ incl ______________:
                                   :                                      |
RES  ------------------------------|xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx|----------
                                  300                                   5000

So the resulting bounds for each battery chain should be:

bat5_incl_lower = -900
bat5_excl_lower = -300
bat5_excl_upper = 300
bat5_incl_upper = 5000

bat8_incl_lower = 300
# no excl bounds
bat8_incl_upper = 5000

So the total resulting bounds should be:

incl_lower = (-900) + (300) = -600
excl_lower = (-300) + (300) = 0
excl_upper = (300) + (300) = 600
incl_upper = (5000) + (5000) = 10000

Nevertheless, the battery pool is producing:

incl_lower = -1000
excl_lower = -700
excl_upper = 600
incl_upper = 10000

And the tests are validating this wrong result.

Extra information

The problem seems to manifest when the exclusion bounds are not contained inside the inclusion bounds, the algorithm seems to be very naive and just do:

incl_lower = max(Battery inclusion_lower, Inverter inclusion_lower)
incl_upper = min(Battery inclusion_upper, Inverter inclusion_upper)
excl_lower = min(Battery exclusion_lower, Inverter exclusion_lower)
excl_upper = max(Battery exclusion_upper, Inverter exclusion_upper)

Which doesn't really work when there is effectively no exclusion bounds for some case.

This was discovered while migrating to the microgrid API v0.17, which uses a set of allowed ranges instead a pair of inclusion/exclusion bounds.

Also when working with allowed sets, the calculation should be much more straightforward, and probably handling edge cases will have more predictable results. For example now if we have something like: incl = [100, 1000]; excl = [100, 200], is 100 an allowed value or not? This is not clear. With ranges, this can be clearly represented as [100, 100], [200, 1000] or just [200, 1000] depending on if 100 is allowed or not.

Metadata

Metadata

Assignees

No one assigned

    Type

    Projects

    Status

    To do

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions