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 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" diff --git a/solarfarmer/models/pvsystem/pvsystem.py b/solarfarmer/models/pvsystem/pvsystem.py index 458a16f..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). @@ -268,7 +270,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 +1530,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( diff --git a/tests/test_construct_plant.py b/tests/test_construct_plant.py index 8e27e47..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 @@ -170,6 +174,67 @@ 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): + """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.""" + 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.""" + 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). + """ + 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."""