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
37 changes: 37 additions & 0 deletions docs/_static/solar_power_flow.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
9 changes: 5 additions & 4 deletions docs/h_dict.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,15 +44,16 @@ Any top-level `h_dict` entry whose value is a dict containing a `component_type`
### Solar Farm
| `component_type` | str | "SolarPySAMPVWatts" |
| **For SolarPySAMPVWatts:** |
| `pysam_model` | str | "pvwatts" |
| `solar_input_filename` | str | Solar data file path |
| `system_capacity` | float | DC system capacity in kW as defined by PVWatts - under Standard Test Conditions|
| `system_capacity` | float | DC system capacity in kW (PVWatts STC) |
| `tilt` | float | Array tilt angle in degrees (required) |
| `losses` | float | System losses, % (0–100); see [Solar PV](solar_pv.md) |
| `pysam_options` | dict | Optional; e.g. `SystemDesign: {dc_ac_ratio, array_type, ...}` — see [Solar PV](solar_pv.md) |
| `lat` | float | Latitude |
| `lon` | float | Longitude |
| `elev` | float | Elevation in meters |
| `log_channels` | list | List of channels to log (e.g., ["power", "dni", "poa", "aoi"]) |
| `initial_conditions` | dict | Initial power, DNI, POA |
| `log_channels` | list | Channels to log (e.g. `power`, `ac_power_available`, `dc_power_available`, `dni`, `poa`, `aoi`) — see [Solar PV](solar_pv.md) |
| `initial_conditions` | dict | Initial `power`, `dni`, `poa` placeholders; modeled values are not all applied on init, and `power` is updated to the modeled AC value on the first `step()` |

### Battery
| Key | Type | Description | Default |
Expand Down
4 changes: 3 additions & 1 deletion docs/output_files.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,9 @@ hercules_output.h5
│ │ ├── wind_farm.wind_direction_mean # Farm-average wind direction
│ │ ├── wind_farm.turbine_powers.000 # Turbine 0 power (if logged)
│ │ ├── wind_farm.turbine_powers.001 # Turbine 1 power (if logged)
│ │ ├── solar_farm.power # Solar farm power output
│ │ ├── solar_farm.power # Solar farm AC power after control (kW)
│ │ ├── solar_farm.ac_power_available # Post-inverter AC potential (kW) (if logged)
│ │ ├── solar_farm.dc_power_available # Pre-inverter DC potential (kW) (if logged)
│ │ ├── solar_farm.dni # Direct normal irradiance (if logged)
│ │ ├── solar_farm.poa # Plane-of-array irradiance (if logged)
│ │ ├── battery.power # Battery power (if present)
Expand Down
80 changes: 50 additions & 30 deletions docs/solar_pv.md
Original file line number Diff line number Diff line change
@@ -1,31 +1,58 @@
# Solar PV

The solar PV modules use the [PySAM](https://nrel-pysam.readthedocs.io/en/main/overview.html) package for the National Laboratory of the Rockies's System Advisor Model (SAM) to predict the power output of the solar PV plant.
Hercules uses NLR [PySAM](https://nrel-pysam.readthedocs.io/en/main/overview.html) to drive NLR [System Advisor Model (SAM)](https://sam.nlr.gov) PV technology models.

Presently only one solar simulator is available
The only solar implementation currently in Hercules is:

1. **`SolarPySAMPVWatts`** - Uses the [PVWatts model](https://sam.nrel.gov/photovoltaic.html) in [`Pvwattsv8`](https://nrel-pysam.readthedocs.io/en/main/modules/Pvwattsv8.html), which calculates estimated PV electrical output with configurable efficiency and loss parameters. This model is less detailed but more time-efficient, making it suitable for longer duration simulations (approximately 1 year). Set `component_type: SolarPySAMPVWatts` in the component's YAML section. The section key is a user-chosen `component_name` (e.g. `solar_farm`); see [Component Names, Types, and Categories](component_types.md) for details.
1. **`SolarPySAMPVWatts`** [PVWatts](https://sam.nlr.gov/photovoltaic.html) via PySAM [`Pvwattsv8`](https://nrel-pysam.readthedocs.io/en/main/modules/Pvwattsv8.html). It is fast and suitable for long runs (e.g. about one year). Set `component_type: SolarPySAMPVWatts` in the component YAML. The section key is a user-chosen `component_name` (e.g. `solar_farm`); see [Component Names, Types, and Categories](component_types.md).



## Inputs

Both models require an input weather file:
1. A CSV file that specifies the weather conditions (e.g. NonAnnualSimulation-sample_data-interpolated-daytime.csv). This file should include:
- timestamp (see [timing](timing.md) for time format requirements). Each `time_utc` timestamp marks the **start of a reporting period**; irradiance and weather values on that row are treated as period averages. See [Time Interpretation](timing.md#time-interpretation-inputs-vs-internal-values) for how Hercules converts these to instantaneous values.
- direct normal irradiance (DNI)
- diffuse horizontal irradiance (DHI)
- global horizontal irradiance (GHI)
- wind speed
- air temperature (dry bulb temperature)
The solar component requires a weather time-series file. Supported formats are CSV, pickle (`.p`), Feather (`.f`/`.ftr`), and Parquet. The file should include:

- A `time_utc` column (see [timing](timing.md) for time format requirements). Each `time_utc` value marks the **start of a reporting period**; irradiance and weather on that row are period averages. See [Time Interpretation](timing.md#time-interpretation-inputs-vs-internal-values) for how Hercules converts them to instants.
- DNI, DHI, and GHI in columns whose names include the usual “Direct Normal…”, “Diffuse Horizontal…”, and “Global Horizontal…” substrings (see the solar module’s column lookup)
- Wind speed
- Air temperature (dry-bulb)


The system location (latitude, longitude, and elevation) is specified in the input `yaml` file.


## Power Flow

The solar component models three distinct power quantities at each time step,
corresponding to successive stages along the plant's electrical path:

```{image} _static/solar_power_flow.svg
:alt: Solar power flow from arrays through inverters to the controller
:width: 700px
:align: center
```

- **`dc_power_available`** (kW): the full **DC potential** of the arrays, before
the inverters. This is the modeled DC output (e.g. PVWatts `Outputs.dc`,
W → kW) and reflects irradiance, temperature, and DC-side losses, but not
inverter behavior or control.
- **`ac_power_available`** (kW): the **post-inverter AC potential** of the
plant. It includes inverter inefficiency and AC clipping at the inverter
nameplate (per PVWatts `Outputs.ac`, W → kW), but does not include any
Hercules control-based curtailment.
- **`power`** (kW): the **delivered AC** power after the Hercules controller.
When the controller imposes an AC setpoint below `ac_power_available`,
`power` reflects the curtailed value; otherwise `power` equals
`ac_power_available`.

## Outputs

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.
At each time step, `h_dict[component_name]` is updated with `power`,
`ac_power_available`, and `dc_power_available` (all in kW), as well as the
weather/geometry diagnostics (`dni`, `poa`, `aoi`). All three power quantities
can be selected for HDF5 logging via `log_channels` (see below), though the `power` variable is always logged by default.

The YAML **`system_capacity`** is the **DC** array capacity at STC (kW), as in PVWatts. Inverter sizing and AC clipping follow PVWatts `SystemDesign` (including `dc_ac_ratio`); defaults can be changed under `pysam_options` (see below).

The PVWatts model is configured with the following default parameters for utility-scale installations:
- **Module type**: Standard crystalline silicon (module_type = 0)
Expand All @@ -44,19 +71,21 @@ solar_farm:
SystemDesign:
array_type: 3.0 # single axis backtracking
azimuth: 170.0
dc_ac_ratio: 1.0 # Force to 1.0
dc_ac_ratio: 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.
The **`tilt`** is the array tilt angle in degrees, measured from horizontal, and must be specified in the input configuration file. Together with **`system_capacity`** and **`losses`** (see below), it is one of the three required top-level keys for the solar component; all other PVWatts parameters fall back to Hercules defaults or can be overridden under `pysam_options`.

## Logging Configuration

The `log_channels` parameter controls which outputs are written to the HDF5 output file. This is a list of channel names. The `power` channel is always logged, even if not explicitly specified.

**Available Channels:**
- `power`: DC power output in kW (always logged)
- `power`: delivered AC plant power in kW after control (always logged; same quantity as in `h_dict` after the step)
- `ac_power_available`: post-inverter AC potential in kW, before control curtailment (add to the list to include in the HDF5 output)
- `dc_power_available`: pre-inverter DC potential of the arrays in kW (add to the list to include in the HDF5 output)
- `poa`: Plane-of-array irradiance in W/m²
- `dni`: Direct normal irradiance in W/m²
- `aoi`: Angle of incidence in degrees
Expand All @@ -76,24 +105,15 @@ solar_farm:

If `log_channels` is not specified, only `power` will be logged.

## Efficiency and Loss Parameters

Although the pysam model `SolarPySAMPVWatts` model, technically includes efficiency terms:

- **`inv_eff`** - Inverter efficiency as a percentage (0-99.5). (No longer used in Hercules)
- **`losses`** - System losses as a percentage (0-100). Default recommended value: `0` (no losses). This parameter affects the DC power generated by the PV panels, before any conversion to AC by the inverter.
## Efficiency and loss parameters

The example folder `03_wind_and_solar` specifies:
- use of the `SolarPySAMPVWatts` model with `component_type: "SolarPySAMPVWatts"`
- weather conditions on May 10, 2018 measured at NLR's Flatirons Campus
- latitude, longitude, and elevation of Golden, CO
- system design information for a 100 MW single-axis PV tracking system (with backtracking)
- inverter efficiency of 99.5% and system losses of 0%
PVWatts `SolarPySAMPVWatts` includes lumped and inverter-related loss terms. The loss/efficiency parameters exposed in typical Hercules YAML are:

The system capacity can be changed in the `.yaml` file, but the DC/AC ratio is fixed at 1.0.
- **`losses`**: system losses as a percentage (0–100). Affects the modeled **DC** side before the inverter in PVWatts. A common default is `0`.
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you think it's slightly weird here not to mention tilt? Since it's the other parameter set at the top level of the solar dict?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch, re-arranging the docs a little so tilt gets more intentionally described. But arranging just a little so tilt doesn't get listed under losses (see above)

- **`pysam_options` → `SystemDesign`**: e.g. `dc_ac_ratio`, `array_type`, `azimuth`, `module_type` (see PySAM / SAM documentation for the full set).

For examples using the detailed `SolarPySAMPVSam` model, see the test files in the `tests/` directory.
The `examples/03_wind_and_solar` case uses `SolarPySAMPVWatts` with a 30 MW DC STC `system_capacity`, `losses: 0`, and default single-axis backtracking. Location and weather are set in that example’s input YAML and resource files. Override `dc_ac_ratio` in `pysam_options` if you need a nameplate/clip point different from the default.


## References
PySAM. National Laboratory of the Rockies. Golden, CO. https://github.com/nrel/pysam
PySAM (NLR). https://github.com/NatLabRockies/pysam
4 changes: 3 additions & 1 deletion examples/hercules_input_example.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,9 @@ wind_farm:
# losses: 0 # System losses as a percentage (0-100, default: 0)
# log_file_name: outputs/log_solar_farm.log # Path to solar farm log file (default: outputs/log_solar_farm.log)
# log_channels: # List of output channels to log (power is always logged even if not specified)
# - power # DC power output in kW (always logged)
# - power # Delivered AC power in kW after control (always logged)
# - ac_power_available # Post-inverter AC potential in kW (before control curtailment)
# - dc_power_available # Pre-inverter DC potential of the arrays in kW
# - poa # Plane-of-array irradiance in W/m²
# - dni # Direct normal irradiance in W/m²
# - aoi # Angle of incidence in degrees
Expand Down
46 changes: 26 additions & 20 deletions hercules/plant_components/solar_pysam_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,10 @@


class SolarPySAMBase(ComponentBase):
"""Base class for PySAM-based solar simulators.
"""Base class for PySAM-based solar (PV) simulators.

This class provides common functionality for both PVSam and PVWatts models,
including weather data processing, solar resource assignment, and control logic.

Note PVSam is no longer supported in Hercules.
Subclasses run a PySAM model, load weather, and apply AC power setpoints. Weather
handling and stepping live here; model-specific precompute is in the subclass.
"""

component_category = "generator"
Expand All @@ -36,12 +34,10 @@ def __init__(self, h_dict, component_name):
# Save the system capacity (in kW - PVWatts DC system capacity)
self.system_capacity = h_dict[self.component_name]["system_capacity"]

# Save the target dc/ac ratio (Force to 1.0)
self.target_dc_ac_ratio = 1.0

# Save the initial condition
self.power = h_dict[self.component_name]["initial_conditions"]["power"]
self.dc_power = h_dict[self.component_name]["initial_conditions"]["power"]
self.ac_power_available = h_dict[self.component_name]["initial_conditions"]["power"]
self.dc_power_available = h_dict[self.component_name]["initial_conditions"]["power"]
self.dni = h_dict[self.component_name]["initial_conditions"]["dni"]
self.poa = h_dict[self.component_name]["initial_conditions"]["poa"]
self.aoi = 0
Expand Down Expand Up @@ -172,23 +168,23 @@ def get_initial_conditions_and_meta_data(self, h_dict):
# This is a bit of a hack but need this to exist
h_dict[self.component_name]["capacity"] = self.system_capacity
h_dict[self.component_name]["power"] = self.power
h_dict[self.component_name]["dc_power"] = self.dc_power
h_dict[self.component_name]["ac_power_available"] = self.ac_power_available
h_dict[self.component_name]["dc_power_available"] = self.dc_power_available
h_dict[self.component_name]["dni"] = self.dni
h_dict[self.component_name]["poa"] = self.poa
h_dict[self.component_name]["aoi"] = self.aoi

# Log the start time UTC if available
if hasattr(self, "starttime_utc"):
h_dict[self.component_name]["starttime_utc"] = self.starttime_utc
h_dict[self.component_name]["starttime_utc"] = self.starttime_utc

return h_dict

def control(self, power_setpoint):
"""Controls the PV plant power output to meet a specified setpoint.

This low-level controller enforces power setpoints for the PV plant by
applying uniform curtailment across the entire plant. Note that DC power
output is not controlled as it is not utilized elsewhere in the code.
This low-level controller enforces AC power setpoints by uniform curtailment
of ``self.power``. The pre-control AC potential remains in
``ac_power_available`` and the pre-inverter DC potential remains in
``dc_power_available`` (both exposed in ``h_dict`` after each step).

Args:
power_setpoint (float, optional): Desired total PV plant output in kW.
Expand All @@ -207,11 +203,19 @@ def control(self, power_setpoint):
def _update_outputs(self, h_dict):
"""Update the h_dict with outputs.

``ac_power_available`` is the post-inverter AC potential (kW) and
``dc_power_available`` is the pre-inverter DC potential (kW) for the
current step. Both are reported before any control-based curtailment.
``dc_power_available`` is populated when the subclass precomputes
``dc_power_available_array``.

Args:
h_dict (dict): Dictionary containing simulation state.
"""
# Update the h_dict with outputs
h_dict[self.component_name]["power"] = self.power
h_dict[self.component_name]["ac_power_available"] = self.ac_power_available
h_dict[self.component_name]["dc_power_available"] = self.dc_power_available
h_dict[self.component_name]["dni"] = self.dni
h_dict[self.component_name]["poa"] = self.poa
h_dict[self.component_name]["aoi"] = self.aoi
Expand All @@ -238,8 +242,7 @@ def _get_step_outputs(self, step):
def step(self, h_dict):
"""Execute one simulation step.

This is the common step implementation that works for both PVWatts and PVSAM.
Subclasses only need to implement _precompute_power_array() and _get_step_outputs().
Subclasses must implement _precompute_power_array() and _get_step_outputs().

Args:
h_dict (dict): Dictionary containing current simulation state.
Expand All @@ -252,8 +255,11 @@ def step(self, h_dict):
if self.verbose:
self.logger.info(f"step = {step} (of {self.n_steps})")

# Get the pre-computed uncurtailed power for this step (already in kW)
self.power = self.power_uncurtailed[step]
# Get the pre-computed available (pre-control) power for this step (already in kW)
self.ac_power_available = self.ac_power_available_array[step]
self.power = self.ac_power_available
if hasattr(self, "dc_power_available_array"):
self.dc_power_available = self.dc_power_available_array[step]

# Apply control
power_setpoint = h_dict[self.component_name]["power_setpoint"]
Expand Down
Loading
Loading