Skip to content

Battery pool bounds are not calculated correctly #1180

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

Open
llucax opened this issue Mar 14, 2025 · 0 comments
Open

Battery pool bounds are not calculated correctly #1180

llucax opened this issue Mar 14, 2025 · 0 comments
Labels
Milestone

Comments

@llucax
Copy link
Contributor

llucax commented Mar 14, 2025

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.

@llucax llucax added the type:bug Something isn't working label Mar 14, 2025
llucax added a commit to llucax/frequenz-sdk-python that referenced this issue Mar 20, 2025
The battery pool bounds calculation is buggy and these tests are wrong
(see frequenz-floss#1180).
By switching to using ranges of bounds, the buggy behaviour changes and
make these tests fail.

Fixing them is difficult without switching to using ranges natively
first, so we just skip these tests for now.

Signed-off-by: Leandro Lucarella <[email protected]>
@llucax llucax added this to the Untriaged milestone May 16, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
Status: To do
Development

No branches or pull requests

1 participant