|
3 | 3 |
|
4 | 4 | """Formula generator from component graph for Consumer Power.""" |
5 | 5 |
|
6 | | -from __future__ import annotations |
7 | | - |
8 | 6 | import logging |
9 | | -from collections import abc |
10 | 7 |
|
11 | 8 | from ....microgrid import connection_manager |
12 | 9 | from ....microgrid.component import Component, ComponentCategory, ComponentMetricId |
@@ -50,95 +47,140 @@ def generate(self) -> FormulaEngine[Power]: |
50 | 47 | if not grid_successors: |
51 | 48 | raise ComponentNotFound("No components found in the component graph.") |
52 | 49 |
|
53 | | - if len(grid_successors) == 1: |
54 | | - grid_meter = next(iter(grid_successors)) |
55 | | - if grid_meter.category != ComponentCategory.METER: |
56 | | - raise RuntimeError( |
57 | | - "Only grid successor in the component graph is not a meter." |
58 | | - ) |
59 | | - return self._gen_with_grid_meter(builder, grid_meter) |
60 | | - return self._gen_without_grid_meter(builder, grid_successors) |
| 50 | + component_graph = connection_manager.get().component_graph |
| 51 | + if all( |
| 52 | + successor.category == ComponentCategory.METER |
| 53 | + and not component_graph.is_battery_chain(successor) |
| 54 | + and not component_graph.is_chp_chain(successor) |
| 55 | + and not component_graph.is_pv_chain(successor) |
| 56 | + and not component_graph.is_ev_charger_chain(successor) |
| 57 | + for successor in grid_successors |
| 58 | + ): |
| 59 | + return self._gen_with_grid_meter(builder, grid_successors) |
| 60 | + |
| 61 | + return self._gen_without_grid_meter(builder, self._get_grid_component()) |
61 | 62 |
|
62 | 63 | def _gen_with_grid_meter( |
63 | 64 | self, |
64 | 65 | builder: ResampledFormulaBuilder[Power], |
65 | | - grid_meter: Component, |
| 66 | + grid_meters: set[Component], |
66 | 67 | ) -> FormulaEngine[Power]: |
67 | 68 | """Generate formula for calculating consumer power with grid meter. |
68 | 69 |
|
69 | 70 | Args: |
70 | 71 | builder: The formula engine builder. |
71 | | - grid_meter: The grid meter component. |
| 72 | + grid_meters: The grid meter component. |
72 | 73 |
|
73 | 74 | Returns: |
74 | 75 | A formula engine that will calculate the consumer power. |
75 | 76 | """ |
| 77 | + assert grid_meters |
76 | 78 | component_graph = connection_manager.get().component_graph |
77 | | - successors = component_graph.successors(grid_meter.component_id) |
78 | 79 |
|
79 | | - builder.push_component_metric(grid_meter.component_id, nones_are_zeros=False) |
| 80 | + def non_consumer_component(component: Component) -> bool: |
| 81 | + """ |
| 82 | + Check if a component is not a consumer component. |
| 83 | +
|
| 84 | + Args: |
| 85 | + component: The component to check. |
80 | 86 |
|
81 | | - for successor in successors: |
| 87 | + Returns: |
| 88 | + True if the component is not a consumer component, False otherwise. |
| 89 | + """ |
82 | 90 | # If the component graph supports additional types of grid successors in the |
83 | 91 | # future, additional checks need to be added here. |
84 | | - if ( |
85 | | - component_graph.is_battery_chain(successor) |
86 | | - or component_graph.is_chp_chain(successor) |
87 | | - or component_graph.is_pv_chain(successor) |
88 | | - or component_graph.is_ev_charger_chain(successor) |
89 | | - ): |
| 92 | + return ( |
| 93 | + component_graph.is_battery_chain(component) |
| 94 | + or component_graph.is_chp_chain(component) |
| 95 | + or component_graph.is_pv_chain(component) |
| 96 | + or component_graph.is_ev_charger_chain(component) |
| 97 | + ) |
| 98 | + |
| 99 | + # join all non consumer components reachable from the different grid meters |
| 100 | + non_consumer_components: set[Component] = set() |
| 101 | + for grid_meter in grid_meters: |
| 102 | + non_consumer_components = non_consumer_components.union( |
| 103 | + component_graph.dfs(grid_meter, set(), non_consumer_component) |
| 104 | + ) |
| 105 | + |
| 106 | + # push all grid meters |
| 107 | + for idx, grid_meter in enumerate(grid_meters): |
| 108 | + if idx > 0: |
90 | 109 | builder.push_oper("-") |
91 | | - nones_are_zeros = True |
92 | | - if successor.category == ComponentCategory.METER: |
93 | | - nones_are_zeros = False |
94 | | - builder.push_component_metric( |
95 | | - successor.component_id, nones_are_zeros=nones_are_zeros |
96 | | - ) |
| 110 | + builder.push_component_metric( |
| 111 | + grid_meter.component_id, nones_are_zeros=False |
| 112 | + ) |
| 113 | + |
| 114 | + # push all non consumer components and subtract them from the grid meters |
| 115 | + for component in non_consumer_components: |
| 116 | + builder.push_oper("-") |
| 117 | + builder.push_component_metric( |
| 118 | + component.component_id, |
| 119 | + nones_are_zeros=component.category != ComponentCategory.METER, |
| 120 | + ) |
97 | 121 |
|
98 | 122 | return builder.build() |
99 | 123 |
|
100 | 124 | def _gen_without_grid_meter( |
101 | 125 | self, |
102 | 126 | builder: ResampledFormulaBuilder[Power], |
103 | | - grid_successors: abc.Iterable[Component], |
| 127 | + grid: Component, |
104 | 128 | ) -> FormulaEngine[Power]: |
105 | 129 | """Generate formula for calculating consumer power without a grid meter. |
106 | 130 |
|
107 | 131 | Args: |
108 | 132 | builder: The formula engine builder. |
109 | | - grid_successors: The grid successors. |
| 133 | + grid: The grid component. |
110 | 134 |
|
111 | 135 | Returns: |
112 | 136 | A formula engine that will calculate the consumer power. |
113 | 137 | """ |
114 | | - component_graph = connection_manager.get().component_graph |
115 | | - is_first = True |
116 | | - for successor in grid_successors: |
| 138 | + |
| 139 | + def consumer_component(component: Component) -> bool: |
| 140 | + """ |
| 141 | + Check if a component is a consumer component. |
| 142 | +
|
| 143 | + Args: |
| 144 | + component: The component to check. |
| 145 | +
|
| 146 | + Returns: |
| 147 | + True if the component is a consumer component, False otherwise. |
| 148 | + """ |
117 | 149 | # If the component graph supports additional types of grid successors in the |
118 | 150 | # future, additional checks need to be added here. |
119 | | - if ( |
120 | | - component_graph.is_battery_chain(successor) |
121 | | - or component_graph.is_chp_chain(successor) |
122 | | - or component_graph.is_pv_chain(successor) |
123 | | - or component_graph.is_ev_charger_chain(successor) |
124 | | - ): |
125 | | - continue |
126 | | - if not is_first: |
127 | | - builder.push_oper("+") |
128 | | - is_first = False |
129 | | - builder.push_component_metric(successor.component_id, nones_are_zeros=False) |
| 151 | + return ( |
| 152 | + component.category |
| 153 | + in {ComponentCategory.METER, ComponentCategory.INVERTER} |
| 154 | + and not component_graph.is_battery_chain(component) |
| 155 | + and not component_graph.is_chp_chain(component) |
| 156 | + and not component_graph.is_pv_chain(component) |
| 157 | + and not component_graph.is_ev_charger_chain(component) |
| 158 | + ) |
| 159 | + |
| 160 | + component_graph = connection_manager.get().component_graph |
| 161 | + consumer_components = component_graph.dfs(grid, set(), consumer_component) |
130 | 162 |
|
131 | | - if len(builder.finalize()[0]) == 0: |
| 163 | + if not consumer_components: |
| 164 | + _logger.warning( |
| 165 | + "Unable to find any consumers in the component graph. " |
| 166 | + "Subscribing to the resampling actor with a non-existing " |
| 167 | + "component id, so that `0` values are sent from the formula." |
| 168 | + ) |
132 | 169 | # If there are no consumer components, we have to send 0 values at the same |
133 | 170 | # frequency as the other streams. So we subscribe with a non-existing |
134 | 171 | # component id, just to get a `None` message at the resampling interval. |
135 | 172 | builder.push_component_metric( |
136 | 173 | NON_EXISTING_COMPONENT_ID, nones_are_zeros=True |
137 | 174 | ) |
138 | | - _logger.warning( |
139 | | - "Unable to find any consumers in the component graph. " |
140 | | - "Subscribing to the resampling actor with a non-existing " |
141 | | - "component id, so that `0` values are sent from the formula." |
| 175 | + return builder.build() |
| 176 | + |
| 177 | + for idx, component in enumerate(consumer_components): |
| 178 | + if idx > 0: |
| 179 | + builder.push_oper("+") |
| 180 | + |
| 181 | + builder.push_component_metric( |
| 182 | + component.component_id, |
| 183 | + nones_are_zeros=component.category != ComponentCategory.METER, |
142 | 184 | ) |
143 | 185 |
|
144 | 186 | return builder.build() |
0 commit comments