2323import pandas as pd
2424
2525from frequenz .lib .notebooks .reporting .metrics .reporting_metrics import (
26+ asset_production ,
27+ consumption ,
2628 grid_feed_in ,
2729 production_excess ,
2830 production_excess_in_bat ,
3133)
3234
3335
34- def _get_numeric_series (df : pd .DataFrame , col : str | None ) -> pd .Series :
36+ def _get_numeric_series (
37+ df : pd .DataFrame , col : str | None , * , clip_non_negative : bool = False
38+ ) -> pd .Series :
3539 """Safely extract a numeric Series or return zeros if missing.
3640
3741 Ensures consistent numeric handling even when the requested column
@@ -41,16 +45,25 @@ def _get_numeric_series(df: pd.DataFrame, col: str | None) -> pd.Series:
4145 Args:
4246 df: Input DataFrame from which to extract the column.
4347 col: Column name to retrieve. If None or missing, zeros are returned.
48+ clip_non_negative: Clip the resulting series at zero (lower bound).
4449
4550 Returns:
46- A float64 Series with non-negative values, matching the input index.
51+ A float64 Series aligned to the input index, optionally clipped to be
52+ non-negative.
4753 """
4854 if col is None :
49- return pd .Series (0.0 , index = df .index , dtype = "float64" )
50- return df .reindex (columns = [col ], fill_value = 0 )[col ].astype ("float64" ).clip (lower = 0 )
55+ series = pd .Series (0.0 , index = df .index , dtype = "float64" )
56+ else :
57+ series = df .reindex (columns = [col ], fill_value = 0 )[col ].astype ("float64" )
58+
59+ if clip_non_negative :
60+ series = series .clip (lower = 0 )
61+ return series
5162
5263
53- def _sum_cols (df : pd .DataFrame , cols : list [str ] | None ) -> pd .Series :
64+ def _sum_cols (
65+ df : pd .DataFrame , cols : list [str ] | None , * , clip_non_negative : bool = False
66+ ) -> pd .Series :
5467 """Safely sum multiple numeric columns into a single Series.
5568
5669 Ensures robust aggregation even when some columns are missing or None.
@@ -59,24 +72,30 @@ def _sum_cols(df: pd.DataFrame, cols: list[str] | None) -> pd.Series:
5972 Args:
6073 df: Input DataFrame containing the columns to be summed.
6174 cols: list of column names to sum. If empty, returns a zero-filled Series.
75+ clip_non_negative: Clip individual series at zero before summing.
6276
6377 Returns:
6478 A float64 Series representing the elementwise sum of all specified columns.
65- Missing or invalid columns are treated as zeros.
79+ Missing or invalid columns are treated as zeros. The result is
80+ non-negative when ``clip_non_negative`` is True.
6681 """
6782 if not cols :
6883 return pd .Series (0.0 , index = df .index , dtype = "float64" )
6984
70- # Safely extract each column as a numeric, non-negative Series, then sum row-wise
71- series_list = [_get_numeric_series (df , c ) for c in cols ]
85+ # Safely extract each column as a numeric Series
86+ # (clipped to non-negative if clip_non_negative=True), then sum row-wise
87+ series_list = [
88+ _get_numeric_series (df , c , clip_non_negative = clip_non_negative ) for c in cols
89+ ]
7290 return pd .concat (series_list , axis = 1 ).sum (axis = 1 ).astype ("float64" )
7391
7492
75- # pylint: disable=too-many-arguments, too-many-locals
93+ # pylint: disable=too-many-arguments, too-many-locals, too-many-positional-arguments
7694def add_energy_flows (
7795 df : pd .DataFrame ,
7896 production_cols : list [str ] | None = None ,
7997 consumption_cols : list [str ] | None = None ,
98+ grid_cols : list [str ] | None = None ,
8099 battery_charge_col : str | None = None ,
81100 production_is_positive : bool = False ,
82101) -> pd .DataFrame :
@@ -91,6 +110,7 @@ def add_energy_flows(
91110 battery charge data.
92111 production_cols: list of column names representing production sources.
93112 consumption_cols: list of column names representing consumption sources.
113+ grid_cols: list of column names representing grid import/export.
94114 battery_charge_col: optional column name for battery charging power. If None,
95115 battery-related flows are set to zero.
96116 production_is_positive: Whether production values are already positive.
@@ -106,15 +126,71 @@ def add_energy_flows(
106126 """
107127 df_flows = df .copy ()
108128
109- # Total production and consumption (returns pandas series with 0.0 for missing cols)
110- df_flows ["production_total" ] = _sum_cols (df_flows , production_cols )
111- df_flows ["consumption_total" ] = _sum_cols (df_flows , consumption_cols )
112-
113- # Surplus vs. consumption
129+ # Normalize production, grid and consumption columns by removing None entries
130+ resolved_production_cols = [
131+ col for col in (production_cols or []) if col is not None
132+ ]
133+ resolved_consumption_cols = [
134+ col for col in (consumption_cols or []) if col is not None
135+ ]
136+ resolved_grid_cols = [col for col in (grid_cols or []) if col is not None ]
137+
138+ # Compute total asset production
139+ asset_production_cols : list [str ] = []
140+ for col in resolved_production_cols :
141+ series = _get_numeric_series (
142+ df_flows ,
143+ col ,
144+ )
145+ if col not in df_flows :
146+ df_flows [col ] = series
147+ asset_series = asset_production (
148+ series ,
149+ production_is_positive = production_is_positive ,
150+ )
151+ asset_col_name = f"{ col } _asset_production"
152+ df_flows [asset_col_name ] = asset_series
153+ asset_production_cols .append (asset_col_name )
154+
155+ df_flows ["production_total" ] = _sum_cols (df_flows , asset_production_cols )
156+
157+ # Compute total consumption
158+ if not resolved_consumption_cols :
159+ # Use existing 'consumption' column if present
160+ if "consumption" in df_flows .columns :
161+ resolved_consumption_cols = ["consumption" ]
162+
163+ # Infer from grid/production if grid columns exist
164+ elif resolved_grid_cols :
165+ for col in resolved_grid_cols :
166+ if col not in df_flows :
167+ df_flows [col ] = _get_numeric_series (
168+ df_flows ,
169+ col ,
170+ clip_non_negative = True ,
171+ )
172+ inferred_consumption = consumption (
173+ df_flows , asset_production_cols , resolved_grid_cols
174+ ).astype ("float64" )
175+ inferred_name = str (inferred_consumption .name or "consumption" )
176+ df_flows [inferred_name ] = inferred_consumption
177+ resolved_consumption_cols = [inferred_name ]
178+
179+ # Fallback — create a zero-filled consumption column
180+ else :
181+ inferred_consumption = pd .Series (
182+ 0.0 , index = df_flows .index , dtype = "float64" , name = "consumption"
183+ )
184+ df_flows [inferred_consumption .name ] = inferred_consumption
185+ resolved_consumption_cols = [str (inferred_consumption .name )]
186+
187+ df_flows ["consumption_total" ] = _sum_cols (df_flows , resolved_consumption_cols )
188+
189+ # Surplus vs. consumption (production is already positive because of the above cleaning)
114190 df_flows ["production_excess" ] = production_excess (
115191 df_flows ["production_total" ],
116192 df_flows ["consumption_total" ],
117- production_is_positive = production_is_positive ,
193+ production_is_positive = True ,
118194 )
119195
120196 # Battery charging power (optional)
@@ -123,15 +199,15 @@ def add_energy_flows(
123199 df_flows ["production_total" ],
124200 df_flows ["consumption_total" ],
125201 bat_in ,
126- production_is_positive = production_is_positive ,
202+ production_is_positive = True ,
127203 )
128204
129205 # Split excess into battery vs. grid
130206 df_flows ["grid_feed_in" ] = grid_feed_in (
131207 df_flows ["production_total" ],
132208 df_flows ["consumption_total" ],
133209 bat_in ,
134- production_is_positive = production_is_positive ,
210+ production_is_positive = True ,
135211 )
136212
137213 # If no production columns exist, set self-consumption metrics to zero
@@ -141,12 +217,12 @@ def add_energy_flows(
141217 df_flows ["production_self_use" ] = production_self_consumption (
142218 df_flows ["production_total" ],
143219 df_flows ["consumption_total" ],
144- production_is_positive = production_is_positive ,
220+ production_is_positive = True ,
145221 )
146222 df_flows ["production_self_share" ] = production_self_share (
147223 df_flows ["production_total" ],
148224 df_flows ["consumption_total" ],
149- production_is_positive = production_is_positive ,
225+ production_is_positive = True ,
150226 )
151227 else :
152228 df_flows ["production_self_use" ] = 0.0
0 commit comments