A Rust port of pvlib-python — the open-source solar photovoltaic modeling library.
pvlib-rust provides the same algorithms, accuracy, and modeling capabilities as pvlib-python, with Rust's performance and safety guarantees. It covers the full PV simulation pipeline: from solar position and clear-sky irradiance through module temperature and single-diode physics to DC/AC power output.
Indicative numbers on a modern x86_64 laptop, measured with the in-tree
benches/batch_tmy.rs harness. Values depend on CPU, rayon thread count,
and the specific Python configuration compared against.
| Operation | pvlib-python (vectorized) | pvlib-rust | Typical ratio |
|---|---|---|---|
| TMY year (8760 hours) | ~0.3-1.5 s | tens of ms | ~10-50x |
| Single timestep | ~0.1-0.5 ms | ~0.5-5 us | ~100-1000x |
| Parallelism | Manual (multiprocessing) | Automatic (rayon) | Built-in |
Run cargo bench to reproduce on your hardware; please open an issue
with findings rather than relying on the table.
- 170+ public functions across 24 modules
- 337 tests with end-to-end validation
- Batch processing — rayon-parallelized, full TMY year (8760 hours) in ~4 ms
- Full simulation pipeline via
ModelChain(scalar) andBatchModelChain(time series) - Builder pattern API for ergonomic configuration
- Auto GHI decomposition — handles weather APIs that only provide GHI (auto Erbs DNI/DHI)
- Bifacial support — rear-side gain modeling with configurable bifaciality factor
- System losses — built-in DC derating for wiring, soiling, mismatch
- NaN-safe — gracefully handles missing values from upstream Python/Polars pipelines
- UTC convenience API —
WeatherSeries::from_utc()andsolar_position_batch_utc()for weather API integration - Multiple model options at each step (5 transposition, 6 temperature, 5 IAM, 3 inverter)
- Weather file I/O (TMY3, EPW) and PVGIS API client
- IV curve fitting and single-diode model parameter extraction (Bishop88, De Soto)
#[inline]annotations on all 99 hot-path functions for optimal loop performance- No unsafe code
Add to your Cargo.toml:
[dependencies]
pvlib-rust = "0.1.6"
# Or from git for latest:
# pvlib = { package = "pvlib-rust", git = "https://github.com/p-vbordei/pvlib-rust" }Simulate an entire year in milliseconds using rayon-parallelized batch processing:
use chrono::TimeZone;
use chrono_tz::US::Mountain;
use pvlib::batch::{BatchModelChain, WeatherSeries};
use pvlib::location::Location;
use pvlib::irradiance::DiffuseModel;
let location = Location::new(39.74, -105.18, Mountain, 1830.0, "Golden, CO");
// Builder pattern — configure system, losses, bifacial, auto-decomposition
let mc = BatchModelChain::pvwatts(location, 30.0, 180.0, 5000.0)
.with_gamma_pdc(-0.004)
.with_inverter(5000.0, 0.96)
.with_albedo(0.2)
.with_transposition(DiffuseModel::Perez)
.with_auto_decomposition(true) // Auto Erbs GHI→DNI/DHI when missing
.with_system_losses(0.14) // 14% DC derating (wiring, soiling, etc.)
.with_bifacial(0.7, 0.25); // Bifacial: 70% rear efficiency, 25% ground albedo
// Weather data — one value per hour (8760 for a full year)
let weather = WeatherSeries {
times, // Vec<DateTime<Tz>>
ghi, // Vec<f64> — global horizontal irradiance [W/m2]
dni, // Vec<f64> — direct normal irradiance (zeros OK with auto-decomposition)
dhi, // Vec<f64> — diffuse horizontal irradiance (zeros OK with auto-decomposition)
temp_air, // Vec<f64> — ambient temperature [C]
wind_speed, // Vec<f64> — wind speed [m/s]
albedo: None, // Optional<Vec<f64>> — ground albedo
};
// Full year simulation — ~4ms on modern hardware
let results = mc.run(&weather).unwrap();
println!("Annual energy: {:.0} kWh", results.total_energy_wh() / 1000.0);
println!("Peak power: {:.0} W", results.peak_power());
println!("Capacity factor: {:.1}%", results.capacity_factor(5000.0) * 100.0);
// Access per-timestep results
for i in 0..results.ac_power.len() {
// results.solar_zenith[i], results.solar_elevation[i], results.poa_global[i],
// results.cell_temperature[i], results.dc_power[i], results.ac_power[i]
}For weather API data (typically UTC timestamps as NaiveDateTime):
use pvlib::batch::{BatchModelChain, WeatherSeries, solar_position_batch_utc};
// Create WeatherSeries from UTC NaiveDateTime timestamps
let weather = WeatherSeries::from_utc(
×tamps, // &[NaiveDateTime] — UTC
"US/Eastern", // IANA timezone name
ghi, dni, dhi, temp_air, wind_speed,
).unwrap();
// Or get just solar position without a full simulation
let (zenith, azimuth, elevation) = solar_position_batch_utc(
39.74, -105.18, 1830.0, ×tamps,
).unwrap();Individual batch functions are also available for custom pipelines:
use pvlib::batch;
// All batch functions accept &[f64] and return Vec<f64>
// Automatically parallelized across CPU cores via rayon
let am = batch::airmass_relative_batch(&zenith_vec);
let (ghi, dni, dhi) = batch::ineichen_batch(&zenith_vec, &am, 3.0, 1830.0);
let (dni, dhi) = batch::erbs_batch(&ghi, &zenith, &doy, &dni_extra);
let iam = batch::iam_physical_batch(&aoi, 1.526, 4.0, 0.002);
let pac = batch::pvwatts_ac_batch(&pdc, 5000.0, 0.96, 0.9637);For real-time or embedded use cases:
use chrono::TimeZone;
use chrono_tz::US::Mountain;
use pvlib::location::Location;
use pvlib::pvsystem::{PVSystem, Array, FixedMount};
use pvlib::modelchain::{ModelChain, WeatherInput};
let location = Location::new(39.74, -105.18, Mountain, 1830.0, "Golden, CO");
let array = Array {
mount: Box::new(FixedMount { surface_tilt: 30.0, surface_azimuth: 180.0 }),
nameplate_dc: 5000.0,
gamma_pdc: -0.004,
modules_per_string: 10,
strings: 2,
albedo: 0.2,
};
let system = PVSystem::new(vec![array], 5000.0);
let mc = ModelChain::with_pvwatts(system, location, 30.0, 180.0, 5000.0, 0.96);
let weather = WeatherInput {
time: Mountain.with_ymd_and_hms(2024, 6, 21, 12, 0, 0).unwrap(),
ghi: Some(900.0),
dni: Some(800.0),
dhi: Some(150.0),
temp_air: 30.0,
wind_speed: 3.0,
albedo: Some(0.2),
};
let result = mc.run_model_from_weather(&weather).unwrap();
println!("DC: {:.0} W, AC: {:.0} W, Tcell: {:.1} C",
result.dc_power, result.ac_power, result.cell_temperature);Use individual module functions for maximum control:
use pvlib::{atmosphere, clearsky, irradiance, temperature, iam, inverter};
// Solar position (NREL SPA algorithm)
let location = pvlib::location::Location::new(39.74, -105.18,
chrono_tz::US::Mountain, 1830.0, "Golden, CO");
let time = chrono_tz::US::Mountain.with_ymd_and_hms(2024, 6, 21, 12, 0, 0).unwrap();
let solpos = location.get_solarposition(time).unwrap();
// Airmass
let am_rel = atmosphere::get_relative_airmass(solpos.zenith);
let pressure = atmosphere::alt2pres(1830.0);
let am_abs = atmosphere::get_absolute_airmass(am_rel, pressure);
// Clear sky irradiance (Ineichen model)
let cs = location.get_clearsky(time, "ineichen");
// Irradiance decomposition (GHI -> DNI + DHI)
let (dni, dhi) = irradiance::erbs(cs.ghi, solpos.zenith, 172, 1366.1);
// Angle of incidence on tilted surface
let aoi = irradiance::aoi(30.0, 180.0, solpos.zenith, solpos.azimuth);
// Incidence angle modifier (Fresnel reflection losses)
let iam_val = iam::physical(aoi, 1.526, 4.0, 0.002);
// Cell temperature (SAPM model)
let (t_cell, _) = temperature::sapm_cell_temperature(
800.0, 30.0, 3.0, -3.56, -0.075, 3.0, 1000.0);
// AC power (PVWatts inverter)
let pac = inverter::pvwatts_ac(4000.0, 5000.0, 0.96, 0.9637);| Module | Description | Key Functions |
|---|---|---|
solarposition |
Solar position (SPA + analytical) | get_solarposition, declination_spencer71, hour_angle, solar_zenith_analytical, sun_rise_set_transit_geometric |
atmosphere |
Atmospheric properties | get_relative_airmass, alt2pres, pres2alt, gueymard94_pw, kasten96_lt, tdew_from_rh, windspeed_powerlaw |
clearsky |
Clear sky irradiance models | ineichen, haurwitz, bird, simplified_solis, detect_clearsky |
irradiance |
Transposition & decomposition | aoi, perez, haydavies, klucher, reindl, isotropic, king, erbs, disc, dirindex, boland, dirint, get_total_irradiance |
location |
Location with convenience methods | get_solarposition, get_clearsky, get_airmass, lookup_altitude |
| Module | Description | Key Functions |
|---|---|---|
temperature |
Cell/module temperature (6 models) | sapm_cell_temperature, pvsyst_cell_temperature, faiman, fuentes, noct_sam, ross, generic_linear |
iam |
Incidence angle modifiers (5 models) | ashrae, physical, martin_ruiz, schlick, sapm, martin_ruiz_diffuse, schlick_diffuse |
pvsystem |
PV system & single-diode params | calcparams_desoto, calcparams_cec, calcparams_pvsyst, sapm, sapm_effective_irradiance |
singlediode |
Single-diode equation solvers | bishop88, bishop88_mpp, bishop88_i_from_v, bishop88_v_from_i, estimate_voc |
inverter |
Inverter models (3 models + multi-MPPT) | pvwatts_ac, sandia, adr, pvwatts_multi, sandia_multi |
tracking |
Single-axis tracker with backtracking | singleaxis, calc_surface_orientation, calc_axis_tilt, calc_cross_axis_tilt |
modelchain |
End-to-end simulation pipeline | run_model_from_weather, run_model_from_poa, run_model_from_effective_irradiance, complete_irradiance |
| Module | Description | Key Functions |
|---|---|---|
batch |
Rayon-parallelized batch operations | BatchModelChain (with auto-decomposition, bifacial, system losses), WeatherSeries::from_utc, solar_position_batch, solar_position_batch_utc, ineichen_batch, erbs_batch, disc_batch, perez_batch, total_irradiance_batch, sapm_cell_temperature_batch, pvwatts_ac_batch |
| Module | Description | Key Functions |
|---|---|---|
bifacial |
Bifacial irradiance | get_irradiance_infinite_sheds |
shading |
Row-to-row shading | masking_angle, ground_angle, masking_angle_passias, projected_solar_zenith_angle, shaded_fraction1d |
soiling |
Soiling losses | hsu, kimber, accumulation_model |
snow |
Snow coverage & losses | fully_covered_nrel, coverage_nrel, dc_loss_nrel |
spectrum |
Spectral mismatch | spectral_mismatch_modifier, spectral_factor_sapm, spectral_factor_caballero |
scaling |
Geographic smoothing (WVM) | wvm_smoothing |
albedo |
Surface albedo | inland_water_dvoracek, surface_albedo |
pvarray |
Module efficiency models | pvefficiency_adr, huld |
transformer |
Transformer losses | simple_efficiency |
| Module | Description | Key Functions |
|---|---|---|
iotools |
Weather file I/O & APIs | read_tmy3, read_epw, get_pvgis_tmy, get_pvgis_hourly, get_pvgis_horizon, retrieve_sam |
ivtools |
IV curve fitting | fit_sandia_simple, fit_desoto, rectify_iv_curve |
Both ModelChain (scalar) and BatchModelChain (time series) support configurable model selection:
| Step | Available Models |
|---|---|
| Transposition | Isotropic, Hay-Davies, Perez, Klucher, Reindl |
| Temperature | SAPM, PVsyst, Faiman, Fuentes, NOCT_SAM, PVWatts |
| IAM (AOI) | Physical (Fresnel), ASHRAE, Martin-Ruiz, SAPM, No loss |
| DC Model | PVWatts |
| AC Model | PVWatts, Sandia, ADR |
| Losses | PVWatts, No loss |
Factory constructors:
ModelChain::with_pvwatts()/BatchModelChain::pvwatts()— PVWatts DC/AC, Physical AOI, Perez transpositionModelChain::with_sapm()— SAPM temperature, ASHRAE AOI, Hay-Davies transpositionModelChain::with_config()— fully custom model selection
let data = pvlib::iotools::read_tmy3("weather.csv").unwrap();
println!("Location: {} ({}, {})",
data.metadata.name.as_deref().unwrap_or("Unknown"),
data.metadata.latitude, data.metadata.longitude);
for record in &data.records {
println!("GHI: {} W/m2, Temp: {} C", record.ghi, record.temp_air);
}let data = pvlib::iotools::read_epw("weather.epw").unwrap();// TMY data for any location worldwide
let tmy = pvlib::iotools::get_pvgis_tmy(45.0, 8.0, None, None, None).unwrap();
// Hourly radiation data
let hourly = pvlib::iotools::get_pvgis_hourly(45.0, 8.0, 2020, 2023,
false, None, None, None).unwrap();
// Horizon profile
let horizon = pvlib::iotools::get_pvgis_horizon(45.0, 8.0).unwrap();This library implements algorithms from peer-reviewed solar energy literature:
- Solar Position: NREL Solar Position Algorithm (Reda & Andreas, 2004), Spencer (1971)
- Clear Sky: Ineichen & Perez (2002), Bird & Hulstrom (1981), Haurwitz (1945), Simplified Solis (Ineichen, 2008)
- Irradiance: Perez et al. (1990), Hay & Davies (1980), Erbs et al. (1982), DISC (Maxwell, 1987), Boland et al. (2008)
- Temperature: SAPM (King et al., 2004), PVsyst, Faiman (2008), Fuentes (1987), Ross (1980), NOCT SAM
- IAM: Martin & Ruiz (2001), De Soto et al. (2006), Schlick approximation, ASHRAE
- Single Diode: Bishop (1988), De Soto et al. (2006), Newton-Raphson solver
- Inverter: PVWatts (Dobos, 2014), Sandia (King et al., 2007), ADR (Driesse, 2023)
- Tracking: Anderson & Mikofski (2020), Marion & Dobos (2013)
- Soiling: HSU (Coello & Boyle, 2019), Kimber et al. (2006)
- Bifacial: Infinite sheds model (Mikofski et al., 2019)
- Spectrum: Caballero et al. (2018), First Solar spectral correction
pvlib-rust covers the core simulation pipeline of pvlib-python with significant performance advantages:
| Aspect | pvlib-python | pvlib-rust |
|---|---|---|
| Speed | ~0.3-1.5s / TMY year (vectorized) | tens of ms / TMY year |
| Parallelism | Manual multiprocessing | Automatic via rayon |
| Type safety | Runtime errors | Compile-time checks |
| Memory | pandas DataFrame overhead | Zero-copy slices |
| Deployment | Requires Python runtime | Single static binary |
| Data structures | pandas DataFrames | Native Rust structs/Vec |
| IO sources | 20+ weather data adapters | TMY3, EPW, PVGIS, SAM |
| Spectral models | SPECTRL2 + 6 mismatch models | 3 mismatch models |
cargo build # Build the library
cargo test # Run all 337 tests
cargo test --release # Run tests with optimizations (faster batch)
cargo clippy # Lint checks
cargo doc --open # Generate and view API documentation
cargo bench # Run benchmarks (if configured)Licensed under the Apache License, Version 2.0. See the LICENSE file for details.
This project is a Rust port of pvlib-python, developed by the pvlib community. All credit for the underlying algorithms and models goes to the original authors and the pvlib-python contributors.