Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 17 additions & 1 deletion docs/solar_pv.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
6 changes: 6 additions & 0 deletions examples/03_wind_and_solar/hercules_input.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
49 changes: 38 additions & 11 deletions hercules/plant_components/solar_pysam_pvwatts.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
Expand Down
92 changes: 92 additions & 0 deletions tests/solar_pysam_pvwatts_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading