66
77import logging
88from abc import ABC , abstractmethod
9- from collections .abc import Iterable , Mapping , Set
9+ from collections .abc import Mapping , Set
1010from datetime import datetime , timezone
1111from typing import Generic , TypeVar
1212
1313from ... import timeseries
14- from ...microgrid import connection_manager
15- from ...microgrid .component import ComponentCategory , ComponentMetricId , InverterType
14+ from ...actor .power_distributing ._distribution_algorithm ._distribution_algorithm import (
15+ _aggregate_battery_power_bounds ,
16+ )
17+ from ...actor .power_distributing .power_distributing import (
18+ _get_battery_inverter_mappings ,
19+ )
20+ from ...actor .power_distributing .result import PowerBounds
21+ from ...microgrid .component import ComponentMetricId
1622from ...timeseries import Sample
1723from .._quantities import Energy , Percentage , Power , Temperature
1824from ._component_metrics import ComponentMetricsData
2430"""Minimal timestamp that can be used in the formula."""
2531
2632
27- def battery_inverter_mapping (batteries : Iterable [int ]) -> dict [int , int ]:
28- """Create mapping between battery and adjacent inverter.
29-
30- Args:
31- batteries: Set of batteries
32-
33- Returns:
34- Mapping between battery and adjacent inverter.
35- """
36- graph = connection_manager .get ().component_graph
37- bat_inv_map : dict [int , int ] = {}
38- for battery_id in batteries :
39- try :
40- predecessors = graph .predecessors (battery_id )
41- except KeyError as err :
42- # If battery_id is not in the component graph, then print error and ignore
43- # this id. Wrong component id might be bug in config file. We won't stop
44- # everything because of bug in config file.
45- _logger .error (str (err ))
46- continue
47-
48- inverter_id = next (
49- (
50- comp .component_id
51- for comp in predecessors
52- if comp .category == ComponentCategory .INVERTER
53- and comp .type == InverterType .BATTERY
54- ),
55- None ,
56- )
57- if inverter_id is None :
58- _logger .info ("Battery %d has no adjacent inverter." , battery_id )
59- else :
60- bat_inv_map [battery_id ] = inverter_id
61- return bat_inv_map
62-
63-
6433# Formula output types class have no common interface
6534# Print all possible types here.
6635T = TypeVar ("T" , Sample [Percentage ], Sample [Energy ], PowerMetrics , Sample [Temperature ])
@@ -462,7 +431,13 @@ def __init__(
462431 Args:
463432 batteries: What batteries should be used for calculation.
464433 """
465- self ._bat_inv_map = battery_inverter_mapping (batteries )
434+ mappings : dict [str , dict [int , frozenset [int ]]] = _get_battery_inverter_mappings (
435+ batteries , inv_bats = False , bat_bats = True , inv_invs = False
436+ )
437+
438+ self ._bat_inv_map = mappings ["bat_invs" ]
439+ self ._bat_bats_map = mappings ["bat_bats" ]
440+
466441 used_batteries = set (self ._bat_inv_map .keys ())
467442
468443 if len (self ._bat_inv_map ) == 0 :
@@ -518,86 +493,13 @@ def inverter_metrics(self) -> Mapping[int, list[ComponentMetricId]]:
518493 Returns:
519494 Map between inverter id and set of required metrics id.
520495 """
521- return {cid : self ._inverter_metrics for cid in set (self ._bat_inv_map .values ())}
522-
523- def _fetch_inclusion_bounds (
524- self ,
525- battery_id : int ,
526- inverter_id : int ,
527- metrics_data : dict [int , ComponentMetricsData ],
528- ) -> tuple [datetime , list [float ], list [float ]]:
529- timestamp = _MIN_TIMESTAMP
530- inclusion_lower_bounds : list [float ] = []
531- inclusion_upper_bounds : list [float ] = []
532-
533- # Inclusion upper and lower bounds are not related.
534- # If one is missing, then we can still use the other.
535- if battery_id in metrics_data :
536- data = metrics_data [battery_id ]
537- value = data .get (ComponentMetricId .POWER_INCLUSION_UPPER_BOUND )
538- if value is not None :
539- timestamp = max (timestamp , data .timestamp )
540- inclusion_upper_bounds .append (value )
541-
542- value = data .get (ComponentMetricId .POWER_INCLUSION_LOWER_BOUND )
543- if value is not None :
544- timestamp = max (timestamp , data .timestamp )
545- inclusion_lower_bounds .append (value )
546-
547- if inverter_id in metrics_data :
548- data = metrics_data [inverter_id ]
549-
550- value = data .get (ComponentMetricId .ACTIVE_POWER_INCLUSION_UPPER_BOUND )
551- if value is not None :
552- timestamp = max (data .timestamp , timestamp )
553- inclusion_upper_bounds .append (value )
554-
555- value = data .get (ComponentMetricId .ACTIVE_POWER_INCLUSION_LOWER_BOUND )
556- if value is not None :
557- timestamp = max (data .timestamp , timestamp )
558- inclusion_lower_bounds .append (value )
559-
560- return (timestamp , inclusion_lower_bounds , inclusion_upper_bounds )
561-
562- def _fetch_exclusion_bounds (
563- self ,
564- battery_id : int ,
565- inverter_id : int ,
566- metrics_data : dict [int , ComponentMetricsData ],
567- ) -> tuple [datetime , list [float ], list [float ]]:
568- timestamp = _MIN_TIMESTAMP
569- exclusion_lower_bounds : list [float ] = []
570- exclusion_upper_bounds : list [float ] = []
571-
572- # Exclusion upper and lower bounds are not related.
573- # If one is missing, then we can still use the other.
574- if battery_id in metrics_data :
575- data = metrics_data [battery_id ]
576- value = data .get (ComponentMetricId .POWER_EXCLUSION_UPPER_BOUND )
577- if value is not None :
578- timestamp = max (timestamp , data .timestamp )
579- exclusion_upper_bounds .append (value )
580-
581- value = data .get (ComponentMetricId .POWER_EXCLUSION_LOWER_BOUND )
582- if value is not None :
583- timestamp = max (timestamp , data .timestamp )
584- exclusion_lower_bounds .append (value )
585-
586- if inverter_id in metrics_data :
587- data = metrics_data [inverter_id ]
588-
589- value = data .get (ComponentMetricId .ACTIVE_POWER_EXCLUSION_UPPER_BOUND )
590- if value is not None :
591- timestamp = max (data .timestamp , timestamp )
592- exclusion_upper_bounds .append (value )
593-
594- value = data .get (ComponentMetricId .ACTIVE_POWER_EXCLUSION_LOWER_BOUND )
595- if value is not None :
596- timestamp = max (data .timestamp , timestamp )
597- exclusion_lower_bounds .append (value )
598-
599- return (timestamp , exclusion_lower_bounds , exclusion_upper_bounds )
496+ return {
497+ inverter_id : self ._inverter_metrics
498+ for inverters in set (self ._bat_inv_map .values ())
499+ for inverter_id in inverters
500+ }
600501
502+ # pylint: disable=too-many-locals
601503 def calculate (
602504 self ,
603505 metrics_data : dict [int , ComponentMetricsData ],
@@ -618,32 +520,89 @@ def calculate(
618520 Return None if there are no component metrics.
619521 """
620522 timestamp = _MIN_TIMESTAMP
523+ loop_timestamp = _MIN_TIMESTAMP
621524 inclusion_bounds_lower = 0.0
622525 inclusion_bounds_upper = 0.0
623526 exclusion_bounds_lower = 0.0
624527 exclusion_bounds_upper = 0.0
625528
626- for battery_id in working_batteries :
627- inverter_id = self ._bat_inv_map [battery_id ]
628- (
629- _ts ,
630- inclusion_lower_bounds ,
631- inclusion_upper_bounds ,
632- ) = self ._fetch_inclusion_bounds (battery_id , inverter_id , metrics_data )
633- timestamp = max (timestamp , _ts )
634- (
635- _ts ,
636- exclusion_lower_bounds ,
637- exclusion_upper_bounds ,
638- ) = self ._fetch_exclusion_bounds (battery_id , inverter_id , metrics_data )
639- if len (inclusion_upper_bounds ) > 0 :
640- inclusion_bounds_upper += min (inclusion_upper_bounds )
641- if len (inclusion_lower_bounds ) > 0 :
642- inclusion_bounds_lower += max (inclusion_lower_bounds )
643- if len (exclusion_upper_bounds ) > 0 :
644- exclusion_bounds_upper += max (exclusion_upper_bounds )
645- if len (exclusion_lower_bounds ) > 0 :
646- exclusion_bounds_lower += min (exclusion_lower_bounds )
529+ battery_sets = {
530+ self ._bat_bats_map [battery_id ] for battery_id in working_batteries
531+ }
532+
533+ def get_validated_bounds (
534+ comp_id : int , comp_metric_ids : list [ComponentMetricId ]
535+ ) -> PowerBounds | None :
536+ results : list [float ] = []
537+ # Make timestamp accessible
538+ nonlocal loop_timestamp
539+ local_timestamp = loop_timestamp
540+
541+ if data := metrics_data .get (comp_id ):
542+ for comp_metric_id in comp_metric_ids :
543+ val = data .get (comp_metric_id )
544+ if val is not None :
545+ local_timestamp = max (loop_timestamp , data .timestamp )
546+ results .append (val )
547+
548+ if len (results ) != len (comp_metric_ids ):
549+ return None
550+
551+ loop_timestamp = local_timestamp
552+ return PowerBounds (
553+ inclusion_lower = results [0 ],
554+ exclusion_lower = results [1 ],
555+ exclusion_upper = results [2 ],
556+ inclusion_upper = results [3 ],
557+ )
558+
559+ def get_bounds_list (
560+ comp_ids : frozenset [int ], comp_metric_ids : list [ComponentMetricId ]
561+ ) -> list [PowerBounds ]:
562+ return list (
563+ x
564+ for x in map (
565+ lambda comp_id : get_validated_bounds (comp_id , comp_metric_ids ),
566+ comp_ids ,
567+ )
568+ if x is not None
569+ )
570+
571+ for battery_ids in battery_sets :
572+ loop_timestamp = timestamp
573+
574+ inverter_ids = self ._bat_inv_map [next (iter (battery_ids ))]
575+
576+ battery_bounds = get_bounds_list (battery_ids , self ._battery_metrics )
577+
578+ if len (battery_bounds ) == 0 :
579+ continue
580+
581+ aggregated_bat_bounds = _aggregate_battery_power_bounds (battery_bounds )
582+
583+ inverter_bounds = get_bounds_list (inverter_ids , self ._inverter_metrics )
584+
585+ if len (inverter_bounds ) == 0 :
586+ continue
587+
588+ timestamp = max (timestamp , loop_timestamp )
589+
590+ inclusion_bounds_lower += max (
591+ aggregated_bat_bounds .inclusion_lower ,
592+ sum (bound .inclusion_lower for bound in inverter_bounds ),
593+ )
594+ inclusion_bounds_upper += min (
595+ aggregated_bat_bounds .inclusion_upper ,
596+ sum (bound .inclusion_upper for bound in inverter_bounds ),
597+ )
598+ exclusion_bounds_lower += min (
599+ aggregated_bat_bounds .exclusion_lower ,
600+ sum (bound .exclusion_lower for bound in inverter_bounds ),
601+ )
602+ exclusion_bounds_upper += max (
603+ aggregated_bat_bounds .exclusion_upper ,
604+ sum (bound .exclusion_upper for bound in inverter_bounds ),
605+ )
647606
648607 if timestamp == _MIN_TIMESTAMP :
649608 return PowerMetrics (
0 commit comments