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}