result <- pvwatts(lat = 33.45, lon = -112.07) # Phoenix, AZPVWatts Deep Dive
What is PVWatts?
PVWatts is NREL’s calculator for estimating the energy production of grid-connected photovoltaic systems. The pvwatts package provides programmatic access to PVWatts v8 via its REST API.
Key Features
- TMY Weather Data: Uses Typical Meteorological Year data from NSRDB
- Validated Models: Industry-standard algorithms for solar geometry, module performance, and system losses
- Global Coverage: Works for most locations worldwide
- High Resolution: ~4km x 4km grid resolution
The pvwatts() Function
Basic Usage
Return Structure
pvwatts() returns an S7 object of class pvwatts_result. Access properties with @:
# Available properties
result@monthly # 12-row tibble
result@annual # 1-row tibble
result@location # Weather station info
result@inputs # Query parameters
result@source # "NREL PVWatts v8"
result@timestamp # Query timeMonthly Data
12 rows with generation and irradiance by month:
result@monthly
#> # A tibble: 12 x 7
#> month month_name generation_kwh dc_generation_kwh poa_irradiance_kwh_m2
#> <int> <chr> <dbl> <dbl> <dbl>
#> 1 1 Jan 120543. 126041. 148.
#> 2 2 Feb 124102. 129760. 157.
#> 3 3 Mar 170283. 178006. 213.
#> ...
#> ghi_kwh_m2_day generation_factor
#> <dbl> <dbl>
#> 1 4.40 0.0578
#> 2 5.27 0.0595
#> 3 6.52 0.0816Key columns:
| Column | Description |
|---|---|
generation_kwh |
AC generation for the month |
dc_generation_kwh |
DC generation before inverter |
poa_irradiance_kwh_m2 |
Plane-of-array irradiance |
ghi_kwh_m2_day |
Global horizontal irradiance (daily avg) |
generation_factor |
Fraction of annual generation (sums to 1.0) |
Annual Data
Summary statistics:
result@annual
#> # A tibble: 1 x 5
#> generation_kwh yield_kwh_per_kwp yield_p90_kwh_per_kwp capacity_factor_pct
#> <dbl> <dbl> <dbl> <dbl>
#> 1 2086789. 2087 1953 23.8
#> ghi_kwh_m2_day
#> <dbl>
#> 1 7.28| Column | Description |
|---|---|
generation_kwh |
Total annual AC generation |
yield_kwh_per_kwp |
Specific yield (P50) |
yield_p90_kwh_per_kwp |
P90 yield (assumes 5% inter-annual variability) |
capacity_factor_pct |
Capacity factor as percentage |
ghi_kwh_m2_day |
Annual average daily GHI |
Location Info
Weather station metadata:
result@location
#> # A tibble: 1 x 8
#> city state country lat lon elevation_m tz_offset weather_source
#> <chr> <chr> <chr> <dbl> <dbl> <dbl> <dbl> <chr>
#> 1 Phoenix Arizona United S~ 33.4 -112. 331 -7 NSRDB PSM V3...System Configuration Parameters
Array Type
The mounting configuration significantly affects production:
# Fixed ground mount (open rack)
pvwatts(lat = 33.45, lon = -112.07, array_type = "fixed")@annual$yield_kwh_per_kwp
#> [1] 1812
# Fixed roof mount (less airflow = hotter = less efficient)
pvwatts(lat = 33.45, lon = -112.07, array_type = "fixed-roof")@annual$yield_kwh_per_kwp
#> [1] 1752
# Single-axis tracking
pvwatts(lat = 33.45, lon = -112.07, array_type = "1-axis")@annual$yield_kwh_per_kwp
#> [1] 2087
# Single-axis with backtracking (reduces row-to-row shading)
pvwatts(lat = 33.45, lon = -112.07, array_type = "1-axis-backtrack")@annual$yield_kwh_per_kwp
#> [1] 2076
# Dual-axis tracking (highest yield, rarely used at scale)
pvwatts(lat = 33.45, lon = -112.07, array_type = "2-axis")@annual$yield_kwh_per_kwp
#> [1] 2218Typical boost from tracking:
- 1-axis over fixed: +15-25%
- 2-axis over 1-axis: +5-8% (rarely worth the cost)
Tilt Angle
For fixed systems, tilt affects yield:
# Phoenix (33.45 latitude)
options(pvwatts.silent = TRUE)
# Flat (0) - worst for fixed
pvwatts(lat = 33.45, lon = -112.07, array_type = "fixed", tilt = 0)@annual$yield_kwh_per_kwp
#> [1] 1623
# Latitude tilt (33) - typical optimum
pvwatts(lat = 33.45, lon = -112.07, array_type = "fixed", tilt = 33)@annual$yield_kwh_per_kwp
#> [1] 1812
# Steep (45) - better for winter, worse overall
pvwatts(lat = 33.45, lon = -112.07, array_type = "fixed", tilt = 45)@annual$yield_kwh_per_kwp
#> [1] 1756Rule of thumb: Optimal fixed tilt is approximately latitude +/- 10 degrees.
For tracking systems, tilt is typically 0 (horizontal axis).
DC/AC Ratio (Inverter Loading Ratio)
The ratio of DC panel capacity to AC inverter capacity:
# Conservative (1.0 - no clipping)
pvwatts(lat = 33.45, lon = -112.07, dc_ac_ratio = 1.0)@annual$yield_kwh_per_kwp
#> [1] 2087
# Typical utility-scale (1.2-1.3)
pvwatts(lat = 33.45, lon = -112.07, dc_ac_ratio = 1.25)@annual$yield_kwh_per_kwp
#> [1] 2075
# Aggressive (1.4 - more clipping)
pvwatts(lat = 33.45, lon = -112.07, dc_ac_ratio = 1.4)@annual$yield_kwh_per_kwp
#> [1] 2042Higher DC/AC ratio = more clipping losses but lower $/W inverter cost.
Note: PVWatts reports yield per kWp DC, so higher DC/AC shows lower kWh/kWp but produces more total energy per inverter.
System Losses
The losses parameter combines multiple loss factors:
# Low losses (new, clean, well-designed system)
pvwatts(lat = 33.45, lon = -112.07, losses = 10)@annual$yield_kwh_per_kwp
#> [1] 2180
# Typical losses (NREL default)
pvwatts(lat = 33.45, lon = -112.07, losses = 14)@annual$yield_kwh_per_kwp
#> [1] 2087
# High losses (older system, dusty site, shading)
pvwatts(lat = 33.45, lon = -112.07, losses = 20)@annual$yield_kwh_per_kwp
#> [1] 1949Loss components (NREL defaults sum to ~14%):
| Factor | Typical | Range |
|---|---|---|
| Soiling | 2% | 1-5% |
| Shading | 3% | 0-10% |
| Snow | 0% | 0-5% |
| Mismatch | 2% | 1-3% |
| Wiring | 2% | 1-3% |
| Connections | 0.5% | 0-1% |
| Light-induced degradation | 1.5% | 1-2% |
| Nameplate | 1% | 0-2% |
| Availability | 3% | 1-5% |
Module Type
# Standard crystalline silicon (most common)
pvwatts(lat = 33.45, lon = -112.07, module_type = "standard")@annual$yield_kwh_per_kwp
#> [1] 2087
# Premium/high-efficiency (better temperature coefficient)
pvwatts(lat = 33.45, lon = -112.07, module_type = "premium")@annual$yield_kwh_per_kwp
#> [1] 2126
# Thin film (CdTe - lower efficiency, better in heat)
pvwatts(lat = 33.45, lon = -112.07, module_type = "thin-film")@annual$yield_kwh_per_kwp
#> [1] 2050Bifacial Modules
Bifacial panels capture reflected light from the ground:
# Monofacial (default)
pvwatts(lat = 33.45, lon = -112.07, bifaciality = 0)@annual$yield_kwh_per_kwp
#> [1] 2087
# Typical bifacial (0.65-0.75 bifaciality)
pvwatts(lat = 33.45, lon = -112.07, bifaciality = 0.70, albedo = 0.20)@annual$yield_kwh_per_kwp
#> [1] 2150
# Bifacial with high-albedo ground (white gravel/sand)
pvwatts(lat = 33.45, lon = -112.07, bifaciality = 0.70, albedo = 0.40)@annual$yield_kwh_per_kwp
#> [1] 2225Albedo values:
| Surface | Albedo |
|---|---|
| Grass | 0.15-0.25 |
| Bare soil | 0.10-0.20 |
| Concrete | 0.25-0.35 |
| White gravel | 0.35-0.45 |
| Snow | 0.60-0.80 |
Comparing Locations
Regional Comparison
options(pvwatts.silent = TRUE)
locations <- list(
"Phoenix, AZ" = c(33.45, -112.07),
"Austin, TX" = c(30.27, -97.74),
"Charlotte, NC" = c(35.23, -80.84),
"Boston, MA" = c(42.36, -71.06)
)
results <- purrr::map_dbl(locations, function(coords) {
pvwatts(lat = coords[1], lon = coords[2])@annual$yield_kwh_per_kwp
})
sort(results, decreasing = TRUE)
#> Phoenix, AZ Austin, TX Charlotte, NC Boston, MA
#> 2087 1735 1520 1402Using Location Presets
# Get all available locations
locs <- list_locations(quiet = TRUE)
# Query a few
results <- purrr::map(c("phoenix_az", "austin_tx", "boston_ma"), function(name) {
loc <- preset_location(name)
pvwatts(lat = loc$lat, lon = loc$lon)
})
purrr::map_dbl(results, ~ .x@annual$yield_kwh_per_kwp)
#> [1] 2087 1735 1402Batch Queries
Multiple Locations
# Define project sites
sites <- tibble::tribble(
~name, ~lat, ~lon,
"Site A", 30.27, -97.74,
"Site B", 31.50, -98.20,
"Site C", 32.80, -96.80
)
# Query all sites
options(pvwatts.silent = TRUE)
sites$yield <- purrr::map_dbl(
1:nrow(sites),
~ pvwatts(lat = sites$lat[.x], lon = sites$lon[.x])@annual$yield_kwh_per_kwp
)
sites
#> # A tibble: 3 x 4
#> name lat lon yield
#> <chr> <dbl> <dbl> <dbl>
#> 1 Site A 30.3 -97.7 1735
#> 2 Site B 31.5 -98.2 1812
#> 3 Site C 32.8 -96.8 1698Sensitivity Analysis
# How does yield vary with losses?
loss_levels <- seq(10, 20, by = 2)
yields <- purrr::map_dbl(loss_levels, function(loss) {
pvwatts(lat = 30.27, lon = -97.74, losses = loss)@annual$yield_kwh_per_kwp
})
data.frame(losses_pct = loss_levels, yield = yields)
#> losses_pct yield
#> 1 10 1810
#> 2 12 1772
#> 3 14 1735
#> 4 16 1697
#> 5 18 1659
#> 6 20 1621Best Practices
1. Use Realistic Parameters
# Good: Realistic utility-scale parameters
pvwatts(
lat = 30.27, lon = -97.74,
array_type = "1-axis-backtrack",
dc_ac_ratio = 1.3,
losses = 14,
bifaciality = 0.70,
albedo = 0.20,
gcr = 0.35
)2. Match Your Technology
| Project Type | Recommended Settings |
|---|---|
| Utility 1-axis | array_type = "1-axis-backtrack", dc_ac_ratio = 1.3, gcr = 0.35 |
| Fixed ground | array_type = "fixed", tilt = latitude, dc_ac_ratio = 1.2 |
| Commercial roof | array_type = "fixed-roof", tilt = 10, dc_ac_ratio = 1.1 |
| Carport | array_type = "fixed", tilt = 5-10, dc_ac_ratio = 1.15 |
3. Use P90 for Financing
result <- pvwatts(lat = 30.27, lon = -97.74)
# P50 for expected case
result@annual$yield_kwh_per_kwp
#> [1] 1735
# P90 for bank case / debt sizing
result@annual$yield_p90_kwh_per_kwp
#> [1] 16244. Cache Results
For repeated queries to the same location:
# Query once, extract what you need
result <- pvwatts(lat = 30.27, lon = -97.74)
# Use multiple times
yield <- result@annual$yield_kwh_per_kwp
seasonality <- result@monthly$generation_factor
capacity_factor <- result@annual$capacity_factor_pctAPI Rate Limits
NREL allows 1,000 requests per hour per API key. pvwatts includes built-in throttling:
# Default: 30 requests per minute
options(pvwatts.throttle_rate = 30)
# For batch jobs, you can adjust
options(pvwatts.throttle_rate = 50) # More aggressive
options(pvwatts.throttle_rate = 10) # More conservativeTroubleshooting
“API key not found”
# Check your .Renviron
usethis::edit_r_environ()
# Should contain:
# NREL_API_KEY=your_key_here
# Restart R after editing“Invalid latitude/longitude”
# Latitude must be -90 to 90
# Longitude must be -180 to 180
# Wrong: degrees-minutes-seconds
pvwatts(lat = "30 16'N", lon = "-97 44'W") # Error!
# Right: decimal degrees
pvwatts(lat = 30.27, lon = -97.74) # WorksTimeout Errors
# Increase timeout for slow connections
options(pvwatts.timeout = 60) # 60 seconds (default: 30)