Skip to content

Commit 2066636

Browse files
Merge pull request #274 from Mohammad-Tayyab-Frequenz/add-production-self-share
Add production self share
2 parents e589fc2 + e0ac73a commit 2066636

6 files changed

Lines changed: 109 additions & 14 deletions

File tree

RELEASE_NOTES.md

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,7 @@
1111
## New Features
1212

1313
<!-- Here goes the main new features and examples or instructions on how to use them -->
14-
- `build_color_map` now sources default colors from the in-code color dictionary rather than a YAML file.
14+
- Added production self-usage metrics to reporting flows and aggregations (`production_self_usage`, `production_self_share`).
1515

1616
## Bug Fixes
1717

18-
- Added `normalize_date_for_report` to return current time for today, midnight for past dates, and reject future dates.

src/frequenz/lib/notebooks/reporting/metrics/reporting_metrics.py

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -183,7 +183,7 @@ def production_self_consumption(
183183
return result
184184

185185

186-
def production_self_share(
186+
def production_self_usage(
187187
production: pd.Series, consumption: pd.Series, production_is_positive: bool = False
188188
) -> pd.Series:
189189
"""Calculate the self-consumption share of total consumption.
@@ -210,6 +210,40 @@ def production_self_share(
210210
return share
211211

212212

213+
def production_self_share(
214+
production: pd.Series, consumption: pd.Series, production_is_positive: bool = False
215+
) -> pd.Series:
216+
"""Compute the self-consumption share relative to total production.
217+
218+
Calculates the fraction of total production that is directly self-consumed.
219+
The denominator is the positive production portion (after applying the
220+
sign convention), and values are masked to ``NaN`` where production is
221+
zero or negative to avoid invalid divisions.
222+
223+
Args:
224+
production:
225+
Series of production power values (e.g., kW).
226+
consumption:
227+
Series of consumption power values (same unit as ``production``).
228+
production_is_positive:
229+
Whether production values are already positive. If ``False``,
230+
the production series is inverted before clipping to its
231+
positive (productive) portion.
232+
233+
Returns:
234+
Series of self-consumption share values (typically between 0 and 1).
235+
Returns ``NaN`` for timestamps where total production is zero or negative.
236+
"""
237+
production_self_use = production_self_consumption(
238+
production, consumption, production_is_positive=production_is_positive
239+
)
240+
denom = asset_production(
241+
production, production_is_positive=production_is_positive
242+
).astype("float64")
243+
denom = denom.mask(denom <= 0) # NaN when production <= 0
244+
return production_self_use.astype("float64") / denom
245+
246+
213247
def consumption(
214248
grid: pd.Series,
215249
production: pd.Series | None = None,

src/frequenz/lib/notebooks/reporting/schema_mapping.yaml

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -94,15 +94,25 @@ columns:
9494
de: "Menge der Energie, die direkt vor Ort verbraucht wird."
9595
implementation: "reporting_metrics.py::prod_self_consumption"
9696

97-
prod_self_share:
98-
raw: "prod_self_share"
97+
production_self_share:
98+
raw: "production_self_share"
99+
display:
100+
en: "Self-Production Share"
101+
de: "Autarkiegrad"
102+
description:
103+
en: "Share of self-used production to total production."
104+
de: "Anteil der Eigenproduktion am Gesamtverbrauch."
105+
implementation: "reporting_metrics.py::production_self_share"
106+
107+
production_self_usage:
108+
raw: "production_self_usage"
99109
display:
100110
en: "Self-Consumption Share"
101111
de: "Eigenverbrauchsanteil"
102112
description:
103-
en: "Share of total consumption covered by production."
104-
de: "Anteil des Gesamtverbrauchs, der durch Produktion gedeckt wird."
105-
implementation: "reporting_metrics.py::prod_self_share"
113+
en: "Share of self-used production to total consumption."
114+
de: "Anteil der selbstgenutzten Produktion am Gesamtverbrauch."
115+
implementation: "reporting_metrics.py::production_self_usage"
106116

107117

108118
# Generated and used within the notebook for reporting metrics

src/frequenz/lib/notebooks/reporting/utils/helpers.py

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@
5656
production_excess_in_bat,
5757
production_self_consumption,
5858
production_self_share,
59+
production_self_usage,
5960
)
6061
from frequenz.lib.notebooks.reporting.utils.colors import COLOR_DICT
6162

@@ -315,6 +316,7 @@ def get_energy_report_columns(
315316
"production_self_use",
316317
"grid_feed_in",
317318
"production_self_share",
319+
"production_self_usage",
318320
]
319321

320322
# Columns available ONLY when battery exists
@@ -375,7 +377,10 @@ def add_energy_flows(
375377
- "production_excess_in_bat": Portion of excess stored in the battery.
376378
- "grid_feed_in": Portion of excess fed into the grid.
377379
- "production_self_use": Self-consumed portion of production.
378-
- "production_self_share": Share of consumption covered by self-production.
380+
- "production_self_share": Share of production that is self-consumed
381+
(self-consumed / total production).
382+
- "production_self_usage": Share of consumption covered by
383+
self-production (self-consumed / total consumption).
379384
"""
380385
df_flows = df.copy()
381386

@@ -475,9 +480,15 @@ def add_energy_flows(
475480
df_flows["consumption_total"],
476481
production_is_positive=True,
477482
)
483+
df_flows["production_self_usage"] = production_self_usage(
484+
df_flows["production_total"],
485+
df_flows["consumption_total"],
486+
production_is_positive=True,
487+
)
478488
else:
479489
df_flows["production_self_use"] = 0.0
480490
df_flows["production_self_share"] = 0.0
491+
df_flows["production_self_usage"] = 0.0
481492

482493
# Add grid consumption column - grid is later renamed as grid_consumption
483494
if "grid" not in df_flows.columns:

src/frequenz/lib/notebooks/reporting/utils/reporting_nb_functions.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -519,6 +519,10 @@ def aggregate_metrics( # pylint: disable=too-many-locals
519519
Total site consumption (kWh)
520520
- ``prod_self_consumption_share``
521521
Fraction of site consumption covered by self-production (0-1)
522+
- ``production_self_share``
523+
Fraction of total production that is self-consumed (0-1)
524+
- ``production_self_usage``
525+
Fraction of site consumption covered by self-production (0-1)
522526
- ``peak``
523527
Maximum grid import power (kW)
524528
- ``peak_date``
@@ -581,6 +585,12 @@ def aggregate_metrics( # pylint: disable=too-many-locals
581585
else 0
582586
)
583587

588+
results["prod_self_production_share"] = (
589+
prod_self_consumption_sum / total_production_sum
590+
if total_production_sum > 0
591+
else 0
592+
)
593+
584594
# Get the grid_consumption series for peak calculation (kW)
585595
grid_consumption_series = energy_report_df.get("grid_consumption", zeros)
586596

tests/test_reporting_metrics.py

Lines changed: 36 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -77,16 +77,47 @@ def test_production_self_consumption_warns_on_negative_values() -> None:
7777
assert_series_equal(result, expected)
7878

7979

80-
def test_production_self_share_masks_zero_or_negative_consumption() -> None:
81-
"""Self-consumption share returns NaN where consumption is not positive."""
82-
production = pd.Series([-4.0, -2.0], index=pd.RangeIndex(2))
83-
consumption = pd.Series([3.0, 0.0], index=production.index)
80+
def test_production_self_share_masks_zero_or_negative_production() -> None:
81+
"""Self-consumption share returns NaN where production is not positive."""
82+
production = pd.Series([-4.0, 0.0], index=pd.RangeIndex(2))
83+
consumption = pd.Series([3.0, 3.0], index=production.index)
8484

8585
result = metrics.production_self_share(production, consumption)
86-
expected = pd.Series([1.0, float("nan")], index=production.index)
86+
expected = pd.Series([0.75, float("nan")], index=production.index)
87+
assert_series_equal(result, expected)
88+
89+
90+
def test_production_self_usage_masks_zero_or_negative_consumption() -> None:
91+
"""Self-usage returns NaN where consumption is not positive."""
92+
production = pd.Series([-4.0, -2.0], index=pd.RangeIndex(2))
93+
consumption = pd.Series([0.0, -1.0], index=production.index)
94+
95+
with pytest.warns(UserWarning):
96+
result = metrics.production_self_usage(production, consumption)
97+
expected = pd.Series([float("nan"), float("nan")], index=production.index)
8798
assert_series_equal(result, expected)
8899

89100

101+
def test_production_self_usage_computes_expected_values() -> None:
102+
"""Self-usage computes expected ratios with PSC inputs."""
103+
production = pd.Series([-10.0, -5.0], index=pd.RangeIndex(2))
104+
consumption = pd.Series([8.0, 2.0], index=production.index)
105+
106+
usage = metrics.production_self_usage(production, consumption)
107+
expected = pd.Series([1.0, 1.0], index=production.index)
108+
assert_series_equal(usage, expected)
109+
110+
111+
def test_production_self_share_computes_expected_values() -> None:
112+
"""Self-share computes expected ratios with PSC inputs."""
113+
production = pd.Series([-10.0, -5.0], index=pd.RangeIndex(2))
114+
consumption = pd.Series([8.0, 2.0], index=production.index)
115+
116+
share = metrics.production_self_share(production, consumption)
117+
expected = pd.Series([0.8, 0.4], index=production.index)
118+
assert_series_equal(share, expected)
119+
120+
90121
def test_consumption_infers_total_and_sets_series_name() -> None:
91122
"""Grid, production, and battery power are combined to infer consumption."""
92123
grid = pd.Series([5, 4], index=pd.RangeIndex(2))

0 commit comments

Comments
 (0)