From c40c46aeda2a5887f6bc7bdc40f34513a24ea8e0 Mon Sep 17 00:00:00 2001 From: Javier Lopez Lorente Date: Fri, 8 May 2026 16:21:32 +0200 Subject: [PATCH] Add support for availability losses in PVSystem class --- solarfarmer/models/pvsystem/pvsystem.py | 10 +++++++ tests/test_pvsystem.py | 37 +++++++++++++++++++++++++ 2 files changed, 47 insertions(+) diff --git a/solarfarmer/models/pvsystem/pvsystem.py b/solarfarmer/models/pvsystem/pvsystem.py index 4f4eb69..458a16f 100644 --- a/solarfarmer/models/pvsystem/pvsystem.py +++ b/solarfarmer/models/pvsystem/pvsystem.py @@ -126,6 +126,10 @@ class PVSystem: `dc_ohmic_loss` and `ac_ohmic_loss`. transformer_stages: int Number of transformer stages. 0 for ideal behaviour or 1 for one stage (default is 1). + plant_unavailability : float + Plant availability loss (per unit) (default 0.0, i.e., no unavailability loss). + grid_unavailability : float + Grid availability loss (per unit) (default 0.0, i.e., no unavailability loss). Auxiliary Files --------------- @@ -252,6 +256,8 @@ class PVSystem: bifacial: bool = False inverter_type: InverterType | None = InverterType.CENTRAL transformer_stages: int = 1 + plant_unavailability: float = 0.0 + grid_unavailability: float = 0.0 # Effects and settings mounting_height: float | None = None # Changed from: mounting_height: float @@ -804,6 +810,8 @@ def describe(self, verbose=False) -> None: print( f"Transformer Stages: {self.transformer_stages} (0=Ideal/NoLoss, 1=MV/HV transformer)" ) + print(f"Plant Unavailability: {self.plant_unavailability} (per unit)") + print(f"Grid Unavailability: {self.grid_unavailability} (per unit)") if verbose: # Losses @@ -1117,6 +1125,8 @@ def construct_plant(pvplant: PVSystem) -> str: ) calculation_options.apply_spectral_mismatch_modifier = pvplant.enable_spectral_modeling calculation_options.calculate_dhi = pvplant.calculate_dhi_from_ghi + calculation_options.system_availability_loss = pvplant.plant_unavailability + calculation_options.grid_availability_loss = pvplant.grid_unavailability # Build the full inputs model inputs = EnergyCalculationInputs( diff --git a/tests/test_pvsystem.py b/tests/test_pvsystem.py index fc1ecce..c8c8060 100644 --- a/tests/test_pvsystem.py +++ b/tests/test_pvsystem.py @@ -515,6 +515,43 @@ def test_aux_loss_present_in_payload(self, bern_2d_racks_inputs): assert payload["pvPlant"]["auxiliaryLosses"]["simpleLossFactor"] == 0.01 +class TestPVSystemUnavailability: + """Tests for plant_unavailability and grid_unavailability properties.""" + + def test_unavailability_defaults_are_zero(self): + """plant_unavailability and grid_unavailability must default to 0.0.""" + plant = PVSystem() + + assert plant.plant_unavailability == 0.0 + assert plant.grid_unavailability == 0.0 + + def test_unavailability_custom_values_are_stored(self): + """Custom unavailability values must be stored on the instance.""" + plant = PVSystem(plant_unavailability=0.03, grid_unavailability=0.01) + + assert plant.plant_unavailability == 0.03 + assert plant.grid_unavailability == 0.01 + + def test_unavailability_propagates_to_payload(self, bern_2d_racks_inputs): + """Non-zero unavailability values must appear in energyCalculationOptions in the payload.""" + plant = PVSystem( + latitude=46.9, + longitude=7.4, + plant_unavailability=0.03, + grid_unavailability=0.01, + ) + plant.pan_files = { + "CanadianSolar_CS6U-330M_APP": f"{bern_2d_racks_inputs}/CanadianSolar_CS6U-330M_APP.PAN" + } + plant.ond_files = {"Sungrow_SG125HV_APP": f"{bern_2d_racks_inputs}/Sungrow_SG125HV_APP.OND"} + + payload = json.loads(construct_plant(plant)) + opts = payload["energyCalculationOptions"] + + assert opts["systemAvailabilityLoss"] == 0.03 + assert opts["gridAvailabilityLoss"] == 0.01 + + class TestPVSystemBifacial: """Tests for bifacial parameter initialization (Phase 1 bug fix)."""