diff --git a/docs/solar_pv.md b/docs/solar_pv.md index ae84d729..f30d8cf7 100644 --- a/docs/solar_pv.md +++ b/docs/solar_pv.md @@ -27,12 +27,28 @@ The system location (latitude, longitude, and elevation) is specified in the inp The solar module output is the DC power (`power`) in kW of the PV plant at each timestep. Using DC power makes the parameters `inv_eff` and `dc_to_ac_ratio` irrelevant. The `system_capacity` parameter represents the DC system capacity under Standard Test Conditions. -The PVWatts model is configured with the following hardcoded parameters for utility-scale installations: +The PVWatts model is configured with the following default parameters for utility-scale installations: - **Module type**: Standard crystalline silicon (module_type = 0) - **Array type**: Single-axis tracking with backtracking (array_type = 3) - **Azimuth**: 180° (due south) - **DC/AC ratio**: 1.0 +These parameters can be changed by using a `pysam_options` input dictionary in the yaml, shown below: +```yaml +solar_farm: + component_type: SolarPySAMPVWatts + system_capacity: 30000 # kW (30 MW) + tilt: 0 # degrees + losses: 0 + pysam_options: + SystemDesign: + array_type: 3.0 # single axis backtracking + azimuth: 170.0 + dc_ac_ratio: 1.0 # Force to 1.0 + module_type: 0.0 # standard crystalline silicon +``` +You can specify some or all of these parameters and the `pysam_options` parameters will always overwrite the defaults. These parameters represent the minimum parameters needed to define the solar model. For an exhaustive list of additional parameters you can set using this method, see [this page](https://h2integrate.readthedocs.io/en/stable/technology_models/pvwattsv8_solar_pv.html). + The array tilt angle must be specified in the input configuration file. ## Logging Configuration diff --git a/examples/03_wind_and_solar/hercules_input.yaml b/examples/03_wind_and_solar/hercules_input.yaml index 0946e928..fdbfce46 100644 --- a/examples/03_wind_and_solar/hercules_input.yaml +++ b/examples/03_wind_and_solar/hercules_input.yaml @@ -37,6 +37,12 @@ solar_farm: # The name of component object 1 system_capacity: 30000 # kW (30 MW) tilt: 0 # degrees losses: 0 + pysam_options: + SystemDesign: + array_type: 3.0 # single axis backtracking + azimuth: 180.0 + dc_ac_ratio: 1.0 # Force to 1.0 + module_type: 0.0 # standard crystalline silicon log_channels: - power diff --git a/hercules/plant_components/solar_pysam_pvwatts.py b/hercules/plant_components/solar_pysam_pvwatts.py index 60e15d2c..7fe7a07b 100644 --- a/hercules/plant_components/solar_pysam_pvwatts.py +++ b/hercules/plant_components/solar_pysam_pvwatts.py @@ -43,18 +43,45 @@ def _setup_model_parameters(self, h_dict): # This represents the DC system capacity under Standard Test Conditions system_capacity = h_dict[self.component_name]["system_capacity"] # (in kW) + # These values are always provided at the top level of the solar model input. + top_level_dict = { + "losses": h_dict[self.component_name]["losses"], + "tilt": h_dict[self.component_name]["tilt"], + "system_capacity": system_capacity, + } + top_level_set = set(top_level_dict.keys()) + + # These values are the Hercules defaults for the PVWatts model and will be used if + # not provided in the PySAM options in the input. + hercules_defaults = { + "array_type": 3.0, # single axis backtracking + "azimuth": 180.0, + "dc_ac_ratio": 1.0, # default is 1.0 so there are no inverter losses. + "module_type": 0.0, # standard crystalline silicon + } + + # Check if any PySAM options for SystemDesign are provided in the input. + if h_dict[self.component_name].get("pysam_options", {}).get("SystemDesign") is not None: + pysam_options_set = set(h_dict[self.component_name]["pysam_options"]["SystemDesign"]) + self.logger.info( + "PySAM model options provided in input are being used to define the PVWatts system." + ) + common_keys = pysam_options_set.intersection(top_level_set) + if len(common_keys) > 0: + raise ValueError( + f"Error: The following parameters are provided in both the top-level input\ + and the PySAM options: {common_keys}. Please remove these parameters\ + from the PySAM options." + ) + + model_dict = ( + hercules_defaults + | top_level_dict + | h_dict[self.component_name].get("pysam_options", {}).get("SystemDesign", {}) + ) + sys_design = { - "ModelParams": { - "SystemDesign": { - "array_type": 3.0, # single axis backtracking - "azimuth": 180.0, - "dc_ac_ratio": 1.0, # Force to 1.0 - "losses": h_dict[self.component_name]["losses"], - "module_type": 0.0, # standard crystalline silicon (hardcoded) - "system_capacity": system_capacity, - "tilt": h_dict[self.component_name]["tilt"], - }, - }, + "ModelParams": {"SystemDesign": model_dict}, } self.model_params = sys_design["ModelParams"] diff --git a/tests/solar_pysam_pvwatts_test.py b/tests/solar_pysam_pvwatts_test.py index cf62a78c..32d00106 100644 --- a/tests/solar_pysam_pvwatts_test.py +++ b/tests/solar_pysam_pvwatts_test.py @@ -25,6 +25,98 @@ def test_init(): assert SPS.aoi == 0 +def test_init_defaults(): + # testing the `init` function: reading the inputs from input dictionary + # and using defaults for missing PySAM options + test_h_dict = copy.deepcopy(h_dict_solar_pvwatts) + # Remove PySAM options to test defaults + if "pysam_options" in test_h_dict["solar_farm"]: + del test_h_dict["solar_farm"]["pysam_options"] + + SPS = SolarPySAMPVWatts(test_h_dict, "solar_farm") + + # Test that Hercules defaults are used when pysam_options are missing + assert SPS.model_params["SystemDesign"]["array_type"] == 3.0 # single axis backtracking + assert SPS.model_params["SystemDesign"]["azimuth"] == 180.0 + assert ( + SPS.model_params["SystemDesign"]["dc_ac_ratio"] == 1.0 + ) # default is 1.0 so there are no inverter losses. + assert SPS.model_params["SystemDesign"]["module_type"] == 0.0 # standard crystalline silicon + + +def test_init_pysam_options(): + # testing the `init` function: reading the inputs from input dictionary + # and using provided PySAM options instead of defaults + test_h_dict = copy.deepcopy(h_dict_solar_pvwatts) + # Add custom PySAM options to test that they are read correctly + test_h_dict["solar_farm"]["pysam_options"] = { + "SystemDesign": { + "array_type": 1.0, # fixed open rack + "azimuth": 170.0, + "dc_ac_ratio": 1.5, + "module_type": 1.0, # premium crystalline silicon + } + } + + SPS = SolarPySAMPVWatts(test_h_dict, "solar_farm") + + # Test that provided PySAM options are used instead of defaults + assert SPS.model_params["SystemDesign"]["array_type"] == 1.0 # fixed open rack + assert SPS.model_params["SystemDesign"]["azimuth"] == 170.0 + assert SPS.model_params["SystemDesign"]["dc_ac_ratio"] == 1.5 + assert SPS.model_params["SystemDesign"]["module_type"] == 1.0 # premium crystalline silicon + + +def test_init_invalid_pysam_options(): + # testing the `init` function: handling invalid PySAM options + test_h_dict = copy.deepcopy(h_dict_solar_pvwatts) + # Add invalid PySAM options to test error handling + test_h_dict["solar_farm"]["pysam_options"] = { + "SystemDesign": { + "array_type": 1.0, # Invalid array type + "azimuth": 170.0, + "dc_ac_ratio": 1.5, + "module_type": 1.0, # premium crystalline silicon + "losses": 0.1, + } + } + + try: + SolarPySAMPVWatts(test_h_dict, "solar_farm") + # If no error is raised, the test should fail + assert False, "Expected ValueError for invalid pysam_options entry." + except ValueError as e: + assert ( + str(e) + == "Error: The following parameters are provided in both the top-level input\ + and the PySAM options: {'losses'}. Please remove these parameters\ + from the PySAM options." + ) + + +def test_init_partial_pysam_options(): + # testing the `init` function: handling partial PySAM options (some provided, some defaults) + test_h_dict = copy.deepcopy(h_dict_solar_pvwatts) + # Add partial PySAM options to test that provided options are used and missing ones default + test_h_dict["solar_farm"]["pysam_options"] = { + "SystemDesign": { + "array_type": 1.0, # fixed open rack + "azimuth": 170.0, + # dc_ac_ratio and module_type are not provided, should use defaults + } + } + + SPS = SolarPySAMPVWatts(test_h_dict, "solar_farm") + + # Test that provided PySAM options are used and missing ones default + assert SPS.model_params["SystemDesign"]["array_type"] == 1.0 # fixed open rack + assert SPS.model_params["SystemDesign"]["azimuth"] == 170.0 + assert ( + SPS.model_params["SystemDesign"]["dc_ac_ratio"] == 1.0 + ) # default is 1.0 so there are no inverter losses. + assert SPS.model_params["SystemDesign"]["module_type"] == 0.0 # standard crystalline silicon + + def test_return_outputs(): # testing the function `return_outputs` # outputs after initialization - all outputs should reflect input dict