Skip to main content

pvlib/
soiling.rs

1/// Simplified soiling loss mass accumulation model (inspired by Hsu et al).
2///
3/// Estimates daily soiling accumulation based on particulate matter concentrations,
4/// assuming natural rain washing.
5///
6/// # Arguments
7/// * `rainfall` - Daily rainfall in mm.
8/// * `cleaning_threshold` - Rainfall threshold to clean the panels (e.g., 5.0 mm).
9/// * `tilt` - Surface tilt in degrees.
10/// * `pm2_5` - PM 2.5 concentration in ug/m^3.
11/// * `pm10` - PM 10 concentration in ug/m^3.
12///
13/// # Returns
14/// Soiling mass accumulation fraction for the day.
15pub fn accumulation_model(rainfall: f64, cleaning_threshold: f64, tilt: f64, pm2_5: f64, pm10: f64) -> f64 {
16    if rainfall >= cleaning_threshold {
17        return 0.0; // Cleaned by rain
18    }
19
20    // Rough empirical accumulation rate combined with tilt gravity slide-off
21    let accumulation_rate = (0.001 * pm2_5 + 0.002 * pm10) / 100.0;
22
23    // Closer to horizontal accumulates more
24    let tilt_factor = tilt.clamp(0.0, 90.0).to_radians().cos();
25
26    accumulation_rate * tilt_factor
27}
28
29/// HSU soiling model - single timestep mass accumulation update.
30///
31/// Computes the soiling ratio using the Fixed Velocity model from Humboldt State
32/// University (HSU). The soiling ratio ranges from 0 to 1, where 1 means no soiling.
33///
34/// This is a single-timestep function. To simulate over time, accumulate mass across
35/// timesteps, resetting to 0 when rainfall exceeds the cleaning threshold, then
36/// convert accumulated mass to soiling ratio with `hsu_soiling_ratio`.
37///
38/// # Arguments
39/// * `pm2_5` - PM2.5 concentration [g/m^3].
40/// * `pm10` - PM10 concentration [g/m^3].
41/// * `depo_veloc_2_5` - Deposition velocity for PM2.5 [m/s], default 0.0009.
42/// * `depo_veloc_10` - Deposition velocity for PM10 [m/s], default 0.004.
43/// * `surface_tilt` - Module tilt from horizontal [degrees].
44/// * `dt_sec` - Timestep duration [seconds].
45///
46/// # Returns
47/// Mass deposited on the tilted surface during this timestep [g/m^2].
48///
49/// # References
50/// Coello, M. and Boyle, L. (2019). "Simple Model For Predicting Time Series
51/// Soiling of Photovoltaic Panels." IEEE Journal of Photovoltaics.
52pub fn hsu_mass_rate(
53    pm2_5: f64,
54    pm10: f64,
55    depo_veloc_2_5: f64,
56    depo_veloc_10: f64,
57    surface_tilt: f64,
58    dt_sec: f64,
59) -> f64 {
60    let horiz_mass = (pm2_5 * depo_veloc_2_5 + (pm10 - pm2_5).max(0.0) * depo_veloc_10) * dt_sec;
61    horiz_mass * surface_tilt.to_radians().cos()
62}
63
64/// Converts accumulated soiling mass to a soiling ratio using the HSU model.
65///
66/// soiling_ratio = 1 - 0.3437 * erf(0.17 * accum_mass^0.8473)
67///
68/// # Arguments
69/// * `accumulated_mass` - Total accumulated soiling mass on the surface [g/m^2].
70///
71/// # Returns
72/// Soiling ratio (0 to 1), where 1 means perfectly clean.
73pub fn hsu_soiling_ratio(accumulated_mass: f64) -> f64 {
74    // Approximate erf using Abramowitz and Stegun formula 7.1.26
75    let x = 0.17 * accumulated_mass.powf(0.8473);
76    1.0 - 0.3437 * erf_approx(x)
77}
78
79/// HSU soiling model convenience function for a single timestep.
80///
81/// Returns the soiling ratio given current accumulated mass and whether
82/// rainfall has cleaned the panels.
83///
84/// # Arguments
85/// * `rainfall` - Rainfall accumulated in the current period [mm].
86/// * `cleaning_threshold` - Rainfall needed to clean panels [mm].
87/// * `surface_tilt` - Module tilt [degrees].
88/// * `pm2_5` - PM2.5 concentration [g/m^3].
89/// * `pm10` - PM10 concentration [g/m^3].
90/// * `depo_veloc_2_5` - PM2.5 deposition velocity [m/s], default 0.0009.
91/// * `depo_veloc_10` - PM10 deposition velocity [m/s], default 0.004.
92/// * `dt_sec` - Timestep duration [seconds].
93/// * `previous_mass` - Accumulated mass from previous timestep [g/m^2].
94///
95/// # Returns
96/// A tuple of (soiling_ratio, new_accumulated_mass).
97pub fn hsu(
98    rainfall: f64,
99    cleaning_threshold: f64,
100    surface_tilt: f64,
101    pm2_5: f64,
102    pm10: f64,
103    depo_veloc_2_5: f64,
104    depo_veloc_10: f64,
105    dt_sec: f64,
106    previous_mass: f64,
107) -> (f64, f64) {
108    let is_cleaned = rainfall >= cleaning_threshold;
109    let mass_this_step = hsu_mass_rate(pm2_5, pm10, depo_veloc_2_5, depo_veloc_10, surface_tilt, dt_sec);
110
111    let accum_mass = if is_cleaned {
112        mass_this_step // reset after cleaning
113    } else {
114        previous_mass + mass_this_step
115    };
116
117    (hsu_soiling_ratio(accum_mass), accum_mass)
118}
119
120/// Kimber soiling model - single timestep update.
121///
122/// Linear soiling accumulation with rain cleaning resets. Soiling builds up
123/// at a daily rate unless rainfall exceeds the cleaning threshold.
124///
125/// # Arguments
126/// * `rainfall` - Rainfall accumulated in the current period [mm].
127/// * `cleaning_threshold` - Daily rainfall needed to clean panels [mm], default 6.0.
128/// * `soiling_loss_rate` - Fraction of energy lost per day of soiling [unitless], default 0.0015.
129/// * `max_soiling` - Maximum soiling loss fraction [unitless], default 0.3.
130/// * `timestep_days` - Duration of the current timestep as a fraction of a day.
131/// * `previous_soiling` - Soiling loss from previous timestep (0 to max_soiling).
132/// * `is_grace_period` - True if within the grace period after a rain event (ground too damp for soiling).
133///
134/// # Returns
135/// Updated soiling loss fraction (0.0 to max_soiling).
136///
137/// # References
138/// Kimber, A. et al. (2006). "The Effect of Soiling on Large Grid-Connected
139/// Photovoltaic Systems in California and the Southwest Region of the United States."
140/// IEEE 4th World Conference on Photovoltaic Energy Conference.
141pub fn kimber(
142    rainfall: f64,
143    cleaning_threshold: f64,
144    soiling_loss_rate: f64,
145    max_soiling: f64,
146    timestep_days: f64,
147    previous_soiling: f64,
148    is_grace_period: bool,
149) -> f64 {
150    // Rain event cleans the panels
151    if rainfall >= cleaning_threshold {
152        return 0.0;
153    }
154
155    // During grace period (ground damp), no soiling accumulation
156    if is_grace_period {
157        return 0.0;
158    }
159
160    // Accumulate soiling
161    let new_soiling = previous_soiling + soiling_loss_rate * timestep_days;
162    new_soiling.min(max_soiling)
163}
164
165/// Approximate error function using Abramowitz and Stegun formula 7.1.26.
166/// Maximum error ~1.5e-7.
167fn erf_approx(x: f64) -> f64 {
168    let sign = if x >= 0.0 { 1.0 } else { -1.0 };
169    let x = x.abs();
170
171    let p = 0.3275911;
172    let a1 = 0.254829592;
173    let a2 = -0.284496736;
174    let a3 = 1.421413741;
175    let a4 = -1.453152027;
176    let a5 = 1.061405429;
177
178    let t = 1.0 / (1.0 + p * x);
179    let t2 = t * t;
180    let t3 = t2 * t;
181    let t4 = t3 * t;
182    let t5 = t4 * t;
183
184    let result = 1.0 - (a1 * t + a2 * t2 + a3 * t3 + a4 * t4 + a5 * t5) * (-x * x).exp();
185    sign * result
186}