PVWatts Deep Dive

Complete guide to the PVWatts API integration
Author

Alejandro Hagan

Published

March 2, 2026

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

result <- pvwatts(lat = 33.45, lon = -112.07)  # Phoenix, AZ

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 time

Monthly 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.0816

Key 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] 2218

Typical 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] 1756

Rule 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] 2042

Higher 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] 1949

Loss 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] 2050

Bifacial 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] 2225

Albedo 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         1402

Using 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 1402

Batch 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  1698

Sensitivity 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  1621

Best 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] 1624

4. 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_pct

API 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 conservative

Troubleshooting

“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)  # Works

Timeout Errors

# Increase timeout for slow connections
options(pvwatts.timeout = 60)  # 60 seconds (default: 30)