From 40ef76e0ae2c187d57ac412cafb9d11960ff8443 Mon Sep 17 00:00:00 2001 From: Ian Tse Date: Mon, 29 Jun 2026 13:24:24 -0700 Subject: [PATCH 1/7] fix: user specified lid_loss has priority over pan file field --- solarfarmer/models/pvsystem/pvsystem.py | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/solarfarmer/models/pvsystem/pvsystem.py b/solarfarmer/models/pvsystem/pvsystem.py index 458a16f..0e0e66f 100644 --- a/solarfarmer/models/pvsystem/pvsystem.py +++ b/solarfarmer/models/pvsystem/pvsystem.py @@ -268,7 +268,7 @@ class PVSystem: ac_ohmic_loss: float | None = None # Default depends on inverter type module_mismatch: float | None = MODULE_MISMATCH_FACTOR module_quality_factor: float | None = 0.0 - lid_loss: float | None = 0.0 + lid_loss: float | None = None module_iam_model_override: str | None = None constant_heat_coefficient: float | None = CONSTANT_HEAT_COEFFICIENT convective_heat_coefficient: float | None = CONVECTIVE_HEAT_COEFFICIENT @@ -1528,11 +1528,16 @@ def generate_pan_file_supplements( plant: PVSystem, module_info: dict, bifaciality_factor: float ) -> dict[str, Any]: """Generate PAN file supplements based on the PVSystem data and module information.""" - # LID loss value is taken from the PAN file if available, otherwise from the PVSystem default - try: - lid_loss = float(module_info["data"]["LIDLoss"]) / 100 # Convert percentage to per unit - except KeyError: + # plant.lid_loss takes priority when explicitly set (not None). + # Otherwise fall back to the LIDLoss key in the PAN file. + # If neither is present, lid_loss is None and the SF API defaults to 0.0. + if plant.lid_loss is not None: lid_loss = plant.lid_loss + else: + try: + lid_loss = float(module_info["data"]["LIDLoss"]) / 100 # Convert percentage to per unit + except KeyError: + lid_loss = None pan_file_supplements = {} pan_file_supplements[module_info["pan_filename"]] = PanFileSupplements( From 21f3540a932923da9671f7ebfc014457ecfbf4d4 Mon Sep 17 00:00:00 2001 From: Ian Tse Date: Mon, 29 Jun 2026 13:24:45 -0700 Subject: [PATCH 2/7] test: add unit tests --- tests/test_construct_plant.py | 67 +++++++++++++++++++++++++++++++++++ 1 file changed, 67 insertions(+) diff --git a/tests/test_construct_plant.py b/tests/test_construct_plant.py index 8e27e47..3d20fda 100644 --- a/tests/test_construct_plant.py +++ b/tests/test_construct_plant.py @@ -170,6 +170,73 @@ def test_multi_dot_ond_filename_produces_correct_spec_id(self, bern_2d_racks_inp multi_dot.unlink() +class TestGeneratePanFileSupplements: + """Contract tests for generate_pan_file_supplements(). + + Priority rule: plant.lid_loss (when not None) > PAN file LIDLoss > None. + """ + + @pytest.fixture + def module_info_with_lid(self, bern_2d_racks_inputs): + """module_info dict built from CanadianSolar PAN (LIDLoss=3.00).""" + from solarfarmer.models.pvsystem.pvsystem import get_module_info_from_pan + + p = PVSystem(latitude=46.95, longitude=7.44) + p.pan_files = { + "CanadianSolar_CS6U-330M_APP": f"{bern_2d_racks_inputs}/CanadianSolar_CS6U-330M_APP.PAN" + } + return get_module_info_from_pan(p) + + @pytest.fixture + def module_info_without_lid(self, module_info_with_lid, tmp_path): + """module_info dict built from a PAN file with the LIDLoss key stripped out.""" + import copy + + info = copy.deepcopy(module_info_with_lid) + info["data"].pop("LIDLoss", None) + return info + + def test_plant_lid_loss_overrides_pan_file(self, module_info_with_lid): + """When plant.lid_loss is set, it wins over the PAN file value.""" + from solarfarmer.models.pvsystem.pvsystem import generate_pan_file_supplements + + plant = PVSystem(latitude=0, longitude=0) + plant.lid_loss = 0.01 # PAN has LIDLoss=3.00 (0.03 per unit) + supplements = generate_pan_file_supplements( + plant, module_info_with_lid, bifaciality_factor=0.0 + ) + pan_key = module_info_with_lid["pan_filename"] + assert supplements[pan_key].lid_loss == pytest.approx(0.01) + + def test_pan_lid_loss_used_when_plant_lid_loss_not_set(self, module_info_with_lid): + """When plant.lid_loss is None, the PAN file LIDLoss is used.""" + from solarfarmer.models.pvsystem.pvsystem import generate_pan_file_supplements + + plant = PVSystem(latitude=0, longitude=0) + assert plant.lid_loss is None # verify default + supplements = generate_pan_file_supplements( + plant, module_info_with_lid, bifaciality_factor=0.0 + ) + pan_key = module_info_with_lid["pan_filename"] + assert supplements[pan_key].lid_loss == pytest.approx(0.03) # 3.00 / 100 + + def test_lid_loss_is_none_when_neither_plant_nor_pan_provides_value( + self, module_info_without_lid + ): + """When neither plant nor PAN file has LIDLoss, supplements.lid_loss is None. + + The SF API then defaults to 0.0 via panData.LIDLoss (C# double default). + """ + from solarfarmer.models.pvsystem.pvsystem import generate_pan_file_supplements + + plant = PVSystem(latitude=0, longitude=0) + supplements = generate_pan_file_supplements( + plant, module_info_without_lid, bifaciality_factor=0.0 + ) + pan_key = module_info_without_lid["pan_filename"] + assert supplements[pan_key].lid_loss is None + + class TestListPathInput: """Verify pan_files and ond_files accept list[Path] in addition to dict.""" From 1788c074d33bd005c2fa9cdc71e57bd7a631fdb6 Mon Sep 17 00:00:00 2001 From: Ian Tse Date: Mon, 29 Jun 2026 13:29:38 -0700 Subject: [PATCH 3/7] version bump --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 4806909..a5b27d3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "dnv-solarfarmer" -version = "0.4.0" +version = "0.5.0" description = "Python SDK for SolarFarmer, a bankable solar PV design and energy yield assessment tool by DNV." readme = "README.md" requires-python = ">=3.10" From b3e1a056faf02b46ce194ffa1bea2e0912c817eb Mon Sep 17 00:00:00 2001 From: Ian Tse Date: Mon, 29 Jun 2026 13:40:29 -0700 Subject: [PATCH 4/7] update PVSystem docstring --- solarfarmer/models/pvsystem/pvsystem.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/solarfarmer/models/pvsystem/pvsystem.py b/solarfarmer/models/pvsystem/pvsystem.py index 0e0e66f..ca6ece3 100644 --- a/solarfarmer/models/pvsystem/pvsystem.py +++ b/solarfarmer/models/pvsystem/pvsystem.py @@ -162,7 +162,9 @@ class PVSystem: module_quality_factor: Optional[float] = None Module quality factor (per unit) (default is 0.0, i.e., no quality loss). lid_loss: Optional[float] = None - Loss due to light-induced degradation (LID) (per unit) (default is 0.0, i.e., no LID loss). + Loss due to light-induced degradation (LID) (per unit). When set, overrides the + LIDLoss value in the PAN file. When ``None`` (default), the value from the PAN + file is used. If the PAN file also has no LIDLoss key, the SF API defaults to 0.0. module_iam_model_override: Optional[IAMModelTypeForOverride] = None Override for the module IAM model used in the calculation (default is None, which will use the values from the module's PAN file). From 5f8332265d5bf2a5e039f212fd88cd258de5d0e7 Mon Sep 17 00:00:00 2001 From: Ian Tse Date: Mon, 29 Jun 2026 13:40:52 -0700 Subject: [PATCH 5/7] test: remove unused path --- tests/test_construct_plant.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_construct_plant.py b/tests/test_construct_plant.py index 3d20fda..e0e19c9 100644 --- a/tests/test_construct_plant.py +++ b/tests/test_construct_plant.py @@ -188,7 +188,7 @@ def module_info_with_lid(self, bern_2d_racks_inputs): return get_module_info_from_pan(p) @pytest.fixture - def module_info_without_lid(self, module_info_with_lid, tmp_path): + def module_info_without_lid(self, module_info_with_lid): """module_info dict built from a PAN file with the LIDLoss key stripped out.""" import copy From 1d10434bf8f2e462cc4db1a1affad3413d7b5f6e Mon Sep 17 00:00:00 2001 From: Ian Tse Date: Mon, 29 Jun 2026 13:43:09 -0700 Subject: [PATCH 6/7] doc: update changelog --- CHANGELOG.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index ebd9fc4..7172e75 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,16 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [0.5.0] - 2026-06-29 + +### Changed + +- `PVSystem.lid_loss` default changed from `0.0` to `None`. When `None`, the LIDLoss value is read from the PAN file. If you relied on the old default to override the PAN file, set `plant.lid_loss = 0.0` explicitly. + +### Fixed + +- `PVSystem.lid_loss` now correctly overrides the PAN file's `LIDLoss` value when explicitly set (previously the PAN file always took precedence). + ## [0.4.0] - 2026-06-08 ### Added From 27b453b6c651d508d7a36561a0ba277edb994b37 Mon Sep 17 00:00:00 2001 From: Ian Tse Date: Mon, 29 Jun 2026 14:04:47 -0700 Subject: [PATCH 7/7] clean up test --- tests/test_construct_plant.py | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/tests/test_construct_plant.py b/tests/test_construct_plant.py index e0e19c9..40bae29 100644 --- a/tests/test_construct_plant.py +++ b/tests/test_construct_plant.py @@ -4,7 +4,11 @@ from solarfarmer import PVSystem from solarfarmer.models import PVPlant -from solarfarmer.models.pvsystem.pvsystem import construct_plant, design_plant +from solarfarmer.models.pvsystem.pvsystem import ( + construct_plant, + design_plant, + generate_pan_file_supplements, +) @pytest.fixture @@ -198,8 +202,6 @@ def module_info_without_lid(self, module_info_with_lid): def test_plant_lid_loss_overrides_pan_file(self, module_info_with_lid): """When plant.lid_loss is set, it wins over the PAN file value.""" - from solarfarmer.models.pvsystem.pvsystem import generate_pan_file_supplements - plant = PVSystem(latitude=0, longitude=0) plant.lid_loss = 0.01 # PAN has LIDLoss=3.00 (0.03 per unit) supplements = generate_pan_file_supplements( @@ -210,8 +212,6 @@ def test_plant_lid_loss_overrides_pan_file(self, module_info_with_lid): def test_pan_lid_loss_used_when_plant_lid_loss_not_set(self, module_info_with_lid): """When plant.lid_loss is None, the PAN file LIDLoss is used.""" - from solarfarmer.models.pvsystem.pvsystem import generate_pan_file_supplements - plant = PVSystem(latitude=0, longitude=0) assert plant.lid_loss is None # verify default supplements = generate_pan_file_supplements( @@ -227,8 +227,6 @@ def test_lid_loss_is_none_when_neither_plant_nor_pan_provides_value( The SF API then defaults to 0.0 via panData.LIDLoss (C# double default). """ - from solarfarmer.models.pvsystem.pvsystem import generate_pan_file_supplements - plant = PVSystem(latitude=0, longitude=0) supplements = generate_pan_file_supplements( plant, module_info_without_lid, bifaciality_factor=0.0