From 60a429cfb9706d501ab44773f2f218fb1cfd2271 Mon Sep 17 00:00:00 2001 From: Ian Tse Date: Mon, 4 May 2026 13:41:42 -0700 Subject: [PATCH 1/3] Expose bifacial performance ratio through properties, API, and console output Co-authored-by: Copilot --- .../models/energy_calculation_results.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/solarfarmer/models/energy_calculation_results.py b/solarfarmer/models/energy_calculation_results.py index 30b9915..f4ada7c 100644 --- a/solarfarmer/models/energy_calculation_results.py +++ b/solarfarmer/models/energy_calculation_results.py @@ -3,6 +3,7 @@ import calendar import io import json +import math import warnings from dataclasses import dataclass from pathlib import Path @@ -169,6 +170,14 @@ def performance_ratio(self) -> float: """Performance ratio for year 1 (0–1).""" return self.get_performance(project_year=1).get("performance_ratio", float("nan")) + @property + def performance_ratio_bifacial(self) -> float: + """IEC 61724-1:2021 bifacial performance ratio for year 1 (0–1). + + Equals :attr:`performance_ratio` for monofacial systems. + """ + return self.get_performance(project_year=1).get("performance_ratio_bifacial", float("nan")) + @property def energy_yield_kWh_per_kWp(self) -> float: """Specific energy yield for year 1 in kWh/kWp.""" @@ -593,6 +602,13 @@ def performance(self, project_year: int = 1) -> None: f"{data['performance_ratio']:.4f}", ], ] + pr_bifacial = data.get("performance_ratio_bifacial") + if pr_bifacial is not None and not math.isclose( + pr_bifacial, data["performance_ratio"], rel_tol=1e-9 + ): + table_annual_results.append( + ["Performance Ratio (bifacial)", f"{pr_bifacial:.4f}"] + ) print( "-" * 55 + "\n" @@ -701,6 +717,8 @@ def get_performance(self, project_year: int = 1) -> dict[str, int | float]: - 'energy_yield': Specific energy yield (kWh/kWp) - 'net_energy': Net energy production (MWh/year) - 'performance_ratio': Performance ratio (0-1) + - 'performance_ratio_bifacial': IEC 61724-1:2021 bifacial performance ratio (0-1). + Equals ``performance_ratio`` for monofacial systems. Examples -------- @@ -737,6 +755,7 @@ def get_performance(self, project_year: int = 1) -> dict[str, int | float]: "energy_yield": yield_results[ANNUAL_ENERGY_YIELD], "net_energy": yield_results[ANNUAL_NET_ENERGY], "performance_ratio": yield_results[ANNUAL_PERFORMANCE_RATIO], + "performance_ratio_bifacial": yield_results.get(ANNUAL_PERFORMANCE_RATIO_BIFACIAL), } def get_annual_results_table( From 97f30f54fa81d74a774182ecb507827cadb7ff9e Mon Sep 17 00:00:00 2001 From: Ian Tse Date: Mon, 4 May 2026 13:42:00 -0700 Subject: [PATCH 2/3] Add unit tests Co-authored-by: Copilot --- tests/test_energy_calculation_results.py | 57 ++++++++++++++++++++++++ 1 file changed, 57 insertions(+) diff --git a/tests/test_energy_calculation_results.py b/tests/test_energy_calculation_results.py index cfb1198..2861bec 100644 --- a/tests/test_energy_calculation_results.py +++ b/tests/test_energy_calculation_results.py @@ -402,6 +402,7 @@ def test_get_performance_expected_keys(self, results): "energy_yield", "net_energy", "performance_ratio", + "performance_ratio_bifacial", } assert expected_keys == set(results.get_performance(project_year=1).keys()) @@ -759,6 +760,10 @@ def test_performance_ratio(self, results): """performance_ratio should return year-1 PR.""" assert results.performance_ratio == results.get_performance()["performance_ratio"] + def test_performance_ratio_bifacial(self, results): + """performance_ratio_bifacial should return year-1 bifacial PR.""" + assert results.performance_ratio_bifacial == results.get_performance()["performance_ratio_bifacial"] + def test_energy_yield_kWh_per_kWp(self, results): """energy_yield_kWh_per_kWp should return year-1 specific yield.""" assert results.energy_yield_kWh_per_kWp == results.get_performance()["energy_yield"] @@ -775,4 +780,56 @@ def test_empty_results_return_nan(self): ) assert math.isnan(results.net_energy_MWh) assert math.isnan(results.performance_ratio) + assert math.isnan(results.performance_ratio_bifacial) assert math.isnan(results.energy_yield_kWh_per_kWp) + + +class TestPerformancePrinting: + """Test conditional bifacial PR row in performance().""" + + def _make_results(self, pr: float, pr_bifacial: float) -> CalculationResults: + """Build a minimal CalculationResults with the given PR values.""" + annual_data = [ + { + "year": 2023, + "energyYieldResults": { + "averageTemperature": 12.0, + "ghi": 1200.0, + "gi": 1400.0, + "globalEffectiveIrradiance": 1350.0, + "energyYield": 1100.0, + "netEnergy": 200000.0, + "performanceRatio": pr, + "performanceRatioBifacial": pr_bifacial, + }, + "annualEffects": {}, + } + ] + return CalculationResults( + ModelChainResponse=ModelChainResponse(Name="test"), + AnnualData=annual_data, + MonthlyData=[], + CalculationAttributes=None, + ) + + def test_bifacial_row_shown_when_pr_differs(self, capsys): + """Bifacial PR row must appear when it differs from standard PR.""" + results = self._make_results(pr=0.82, pr_bifacial=0.85) + results.performance() + captured = capsys.readouterr().out + assert "Performance Ratio (bifacial)" in captured + assert "0.8500" in captured + + def test_bifacial_row_suppressed_for_monofacial(self, capsys): + """Bifacial PR row must not appear when both PR values are equal (monofacial).""" + results = self._make_results(pr=0.82, pr_bifacial=0.82) + results.performance() + captured = capsys.readouterr().out + assert "Performance Ratio (bifacial)" not in captured + + def test_bifacial_row_suppressed_for_near_equal_values(self, capsys): + """Floating-point near-equal values must not produce a spurious bifacial row.""" + results = self._make_results(pr=0.82, pr_bifacial=0.82 + 1e-12) + results.performance() + captured = capsys.readouterr().out + assert "Performance Ratio (bifacial)" not in captured From 178accc9d708d6b57632faae55155feaeed84770 Mon Sep 17 00:00:00 2001 From: Ian Tse Date: Mon, 4 May 2026 13:46:05 -0700 Subject: [PATCH 3/3] linting --- solarfarmer/models/energy_calculation_results.py | 4 +--- tests/test_energy_calculation_results.py | 5 ++++- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/solarfarmer/models/energy_calculation_results.py b/solarfarmer/models/energy_calculation_results.py index f4ada7c..c43d199 100644 --- a/solarfarmer/models/energy_calculation_results.py +++ b/solarfarmer/models/energy_calculation_results.py @@ -606,9 +606,7 @@ def performance(self, project_year: int = 1) -> None: if pr_bifacial is not None and not math.isclose( pr_bifacial, data["performance_ratio"], rel_tol=1e-9 ): - table_annual_results.append( - ["Performance Ratio (bifacial)", f"{pr_bifacial:.4f}"] - ) + table_annual_results.append(["Performance Ratio (bifacial)", f"{pr_bifacial:.4f}"]) print( "-" * 55 + "\n" diff --git a/tests/test_energy_calculation_results.py b/tests/test_energy_calculation_results.py index 2861bec..410db79 100644 --- a/tests/test_energy_calculation_results.py +++ b/tests/test_energy_calculation_results.py @@ -762,7 +762,10 @@ def test_performance_ratio(self, results): def test_performance_ratio_bifacial(self, results): """performance_ratio_bifacial should return year-1 bifacial PR.""" - assert results.performance_ratio_bifacial == results.get_performance()["performance_ratio_bifacial"] + assert ( + results.performance_ratio_bifacial + == results.get_performance()["performance_ratio_bifacial"] + ) def test_energy_yield_kWh_per_kWp(self, results): """energy_yield_kWh_per_kWp should return year-1 specific yield."""