From 6a2b5e38f6147024ab0f00f09072cfd788c0461e Mon Sep 17 00:00:00 2001 From: Genevieve Starke Date: Tue, 14 Apr 2026 18:17:07 -0400 Subject: [PATCH 1/6] Update Hercules solar model to accept pysam_option dictionary --- .../03_wind_and_solar/hercules_input.yaml | 21 ++++++++- .../plant_components/solar_pysam_pvwatts.py | 44 +++++++++++++------ 2 files changed, 50 insertions(+), 15 deletions(-) diff --git a/examples/03_wind_and_solar/hercules_input.yaml b/examples/03_wind_and_solar/hercules_input.yaml index 0946e928..379993fd 100644 --- a/examples/03_wind_and_solar/hercules_input.yaml +++ b/examples/03_wind_and_solar/hercules_input.yaml @@ -35,11 +35,28 @@ solar_farm: # The name of component object 1 lon: -105.1778 elev: 1829 system_capacity: 30000 # kW (30 MW) - tilt: 0 # degrees - losses: 0 + pysam_options: + array_type: 3.0 # single axis backtracking + azimuth: 180.0 + dc_ac_ratio: 1.0 # Force to 1.0 + losses: 0 + module_type: 0.0 # standard crystalline silicon (hardcoded) + tilt: 0 # degrees log_channels: - power +# solar_farm: # The name of component object 1 +# component_type: SolarPySAMPVWatts +# solar_input_filename: ../inputs/solar_input.ftr +# lat: 39.7442 +# lon: -105.1778 +# elev: 1829 +# system_capacity: 30000 # kW (30 MW) +# tilt: 0 # degrees +# losses: 0 +# log_channels: +# - power + initial_conditions: power: 30000 # kW diff --git a/hercules/plant_components/solar_pysam_pvwatts.py b/hercules/plant_components/solar_pysam_pvwatts.py index 60e15d2c..a50f1e84 100644 --- a/hercules/plant_components/solar_pysam_pvwatts.py +++ b/hercules/plant_components/solar_pysam_pvwatts.py @@ -43,20 +43,38 @@ 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) - 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"], + if h_dict[self.component_name].get("pysam_options") is not None: + print( + "PySAM model options provided in input are being used to define the PVWatts system." + ) + if "losses" in h_dict[self.component_name] or "tilt" in h_dict[self.component_name]: + print( + "Warning: 'losses' and 'tilt' parameters in the input will be ignored " + "since PySAM model options are provided." + ) + sys_design = { + "ModelParams": {"SystemDesign": h_dict[self.component_name]["pysam_options"]}, + } + + # Ensure system capacity is set from upper level of input + # TODO: Should losses and tilt also be forced to be set from + # upper level of input if provided? + sys_design["ModelParams"]["SystemDesign"]["system_capacity"] = system_capacity + else: + print("Hercules default solar parameters are being used to define the PVWatts system.") + 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"], + }, }, - }, - } - + } self.model_params = sys_design["ModelParams"] def _create_system_model(self): From c1170b5794f10cc8f7005aca608df067885f0a33 Mon Sep 17 00:00:00 2001 From: Genevieve Starke Date: Wed, 15 Apr 2026 15:40:18 -0400 Subject: [PATCH 2/6] Update input handling --- .../03_wind_and_solar/hercules_input.yaml | 25 ++----- .../plant_components/solar_pysam_pvwatts.py | 65 +++++++++++-------- 2 files changed, 44 insertions(+), 46 deletions(-) diff --git a/examples/03_wind_and_solar/hercules_input.yaml b/examples/03_wind_and_solar/hercules_input.yaml index 379993fd..79a214d5 100644 --- a/examples/03_wind_and_solar/hercules_input.yaml +++ b/examples/03_wind_and_solar/hercules_input.yaml @@ -35,28 +35,17 @@ solar_farm: # The name of component object 1 lon: -105.1778 elev: 1829 system_capacity: 30000 # kW (30 MW) + tilt: 0 # degrees + losses: 0 pysam_options: - array_type: 3.0 # single axis backtracking - azimuth: 180.0 - dc_ac_ratio: 1.0 # Force to 1.0 - losses: 0 - module_type: 0.0 # standard crystalline silicon (hardcoded) - tilt: 0 # degrees + 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 (hardcoded) log_channels: - power -# solar_farm: # The name of component object 1 -# component_type: SolarPySAMPVWatts -# solar_input_filename: ../inputs/solar_input.ftr -# lat: 39.7442 -# lon: -105.1778 -# elev: 1829 -# system_capacity: 30000 # kW (30 MW) -# tilt: 0 # degrees -# losses: 0 -# log_channels: -# - power - initial_conditions: power: 30000 # kW diff --git a/hercules/plant_components/solar_pysam_pvwatts.py b/hercules/plant_components/solar_pysam_pvwatts.py index a50f1e84..cfeeb589 100644 --- a/hercules/plant_components/solar_pysam_pvwatts.py +++ b/hercules/plant_components/solar_pysam_pvwatts.py @@ -43,38 +43,47 @@ 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) - if h_dict[self.component_name].get("pysam_options") is not None: + # 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, # Force to 1.0 + "module_type": 0.0, # standard crystalline silicon (hardcoded) + } + + # 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"]) print( "PySAM model options provided in input are being used to define the PVWatts system." ) - if "losses" in h_dict[self.component_name] or "tilt" in h_dict[self.component_name]: - print( - "Warning: 'losses' and 'tilt' parameters in the input will be ignored " - "since PySAM model options are provided." + 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." ) - sys_design = { - "ModelParams": {"SystemDesign": h_dict[self.component_name]["pysam_options"]}, - } - - # Ensure system capacity is set from upper level of input - # TODO: Should losses and tilt also be forced to be set from - # upper level of input if provided? - sys_design["ModelParams"]["SystemDesign"]["system_capacity"] = system_capacity - else: - print("Hercules default solar parameters are being used to define the PVWatts system.") - 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"], - }, - }, - } + + model_dict = ( + top_level_dict + | hercules_defaults + | h_dict[self.component_name].get("pysam_options", {}).get("SystemDesign", {}) + ) + + sys_design = { + "ModelParams": {"SystemDesign": model_dict}, + } + self.model_params = sys_design["ModelParams"] def _create_system_model(self): From 53ba2fd8e0c975789fc6937393e8678000f3e78e Mon Sep 17 00:00:00 2001 From: Genevieve Starke Date: Wed, 15 Apr 2026 15:47:07 -0400 Subject: [PATCH 3/6] Update for print statement to info logger --- hercules/plant_components/solar_pysam_pvwatts.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hercules/plant_components/solar_pysam_pvwatts.py b/hercules/plant_components/solar_pysam_pvwatts.py index cfeeb589..5f4e8ced 100644 --- a/hercules/plant_components/solar_pysam_pvwatts.py +++ b/hercules/plant_components/solar_pysam_pvwatts.py @@ -63,7 +63,7 @@ def _setup_model_parameters(self, h_dict): # 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"]) - print( + 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) From f6ee06206b66f19b626d3a72b9af88f3548ff6bd Mon Sep 17 00:00:00 2001 From: Genevieve Starke Date: Fri, 17 Apr 2026 10:25:53 -0400 Subject: [PATCH 4/6] Update solar model based on comments --- hercules/plant_components/solar_pysam_pvwatts.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/hercules/plant_components/solar_pysam_pvwatts.py b/hercules/plant_components/solar_pysam_pvwatts.py index 5f4e8ced..7fe7a07b 100644 --- a/hercules/plant_components/solar_pysam_pvwatts.py +++ b/hercules/plant_components/solar_pysam_pvwatts.py @@ -56,8 +56,8 @@ def _setup_model_parameters(self, h_dict): hercules_defaults = { "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 (hardcoded) + "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. @@ -75,8 +75,8 @@ def _setup_model_parameters(self, h_dict): ) model_dict = ( - top_level_dict - | hercules_defaults + hercules_defaults + | top_level_dict | h_dict[self.component_name].get("pysam_options", {}).get("SystemDesign", {}) ) From 323266dd92e8ac010dbe1b60c752eba525d64b73 Mon Sep 17 00:00:00 2001 From: Genevieve Starke Date: Fri, 17 Apr 2026 11:26:22 -0400 Subject: [PATCH 5/6] Add tests for new solar parameter functionality --- tests/solar_pysam_pvwatts_test.py | 92 +++++++++++++++++++++++++++++++ 1 file changed, 92 insertions(+) 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 From 8ee4e66633eda77edc94bce40cc3fa18de2d9815 Mon Sep 17 00:00:00 2001 From: Genevieve Starke Date: Fri, 17 Apr 2026 11:38:45 -0400 Subject: [PATCH 6/6] Update docs with input parameters --- docs/solar_pv.md | 18 +++++++++++++++++- examples/03_wind_and_solar/hercules_input.yaml | 2 +- 2 files changed, 18 insertions(+), 2 deletions(-) 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 79a214d5..fdbfce46 100644 --- a/examples/03_wind_and_solar/hercules_input.yaml +++ b/examples/03_wind_and_solar/hercules_input.yaml @@ -42,7 +42,7 @@ solar_farm: # The name of component object 1 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 (hardcoded) + module_type: 0.0 # standard crystalline silicon log_channels: - power