1use rayon::prelude::*;
2use crate::{solarposition, atmosphere, clearsky, irradiance, temperature, iam, inverter};
3
4pub fn solar_position_batch(
11 location: &crate::location::Location,
12 times: &[chrono::DateTime<chrono_tz::Tz>],
13) -> Result<(Vec<f64>, Vec<f64>, Vec<f64>), spa::SpaError> {
14 let results: Result<Vec<_>, _> = times.par_iter()
15 .map(|t| solarposition::get_solarposition(location, *t))
16 .collect();
17 let results = results?;
18 let zenith = results.iter().map(|r| r.zenith).collect();
19 let azimuth = results.iter().map(|r| r.azimuth).collect();
20 let elevation = results.iter().map(|r| r.elevation).collect();
21 Ok((zenith, azimuth, elevation))
22}
23
24pub fn airmass_relative_batch(zenith: &[f64]) -> Vec<f64> {
30 zenith.par_iter()
31 .map(|z| atmosphere::get_relative_airmass(*z))
32 .collect()
33}
34
35pub fn airmass_absolute_batch(airmass_relative: &[f64], pressure: f64) -> Vec<f64> {
37 airmass_relative.par_iter()
38 .map(|am| atmosphere::get_absolute_airmass(*am, pressure))
39 .collect()
40}
41
42pub fn ineichen_batch(
49 zenith: &[f64],
50 airmass_absolute: &[f64],
51 linke_turbidity: f64,
52 altitude: f64,
53) -> (Vec<f64>, Vec<f64>, Vec<f64>) {
54 assert_eq!(zenith.len(), airmass_absolute.len(), "zenith and airmass_absolute must have the same length");
55 let results: Vec<_> = zenith.par_iter()
56 .zip(airmass_absolute.par_iter())
57 .map(|(z, am)| clearsky::ineichen(*z, *am, linke_turbidity, altitude, 1364.0))
58 .collect();
59 let ghi = results.iter().map(|r| r.ghi).collect();
60 let dni = results.iter().map(|r| r.dni).collect();
61 let dhi = results.iter().map(|r| r.dhi).collect();
62 (ghi, dni, dhi)
63}
64
65pub fn bird_batch(
67 zenith: &[f64],
68 airmass_relative: &[f64],
69 aod380: f64,
70 aod500: f64,
71 precipitable_water: f64,
72) -> (Vec<f64>, Vec<f64>, Vec<f64>) {
73 assert_eq!(zenith.len(), airmass_relative.len(), "zenith and airmass_relative must have the same length");
74 let results: Vec<_> = zenith.par_iter()
75 .zip(airmass_relative.par_iter())
76 .map(|(z, am)| clearsky::bird_default(*z, *am, aod380, aod500, precipitable_water))
77 .collect();
78 let ghi = results.iter().map(|r| r.ghi).collect();
79 let dni = results.iter().map(|r| r.dni).collect();
80 let dhi = results.iter().map(|r| r.dhi).collect();
81 (ghi, dni, dhi)
82}
83
84pub fn aoi_batch(
90 surface_tilt: f64,
91 surface_azimuth: f64,
92 solar_zenith: &[f64],
93 solar_azimuth: &[f64],
94) -> Vec<f64> {
95 assert_eq!(solar_zenith.len(), solar_azimuth.len(), "solar_zenith and solar_azimuth must have the same length");
96 solar_zenith.par_iter()
97 .zip(solar_azimuth.par_iter())
98 .map(|(z, a)| irradiance::aoi(surface_tilt, surface_azimuth, *z, *a))
99 .collect()
100}
101
102pub fn extra_radiation_batch(day_of_year: &[i32]) -> Vec<f64> {
104 day_of_year.par_iter()
105 .map(|d| irradiance::get_extra_radiation(*d))
106 .collect()
107}
108
109pub fn erbs_batch(
111 ghi: &[f64],
112 zenith: &[f64],
113 day_of_year: &[u32],
114 dni_extra: &[f64],
115) -> (Vec<f64>, Vec<f64>) {
116 let n = ghi.len();
117 assert_eq!(zenith.len(), n, "zenith len mismatch");
118 assert_eq!(day_of_year.len(), n, "day_of_year len mismatch");
119 assert_eq!(dni_extra.len(), n, "dni_extra len mismatch");
120 let results: Vec<_> = ghi.par_iter()
121 .zip(zenith.par_iter())
122 .zip(day_of_year.par_iter())
123 .zip(dni_extra.par_iter())
124 .map(|(((g, z), d), e)| irradiance::erbs(*g, *z, *d, *e))
125 .collect();
126 let dni = results.iter().map(|r| r.0).collect();
127 let dhi = results.iter().map(|r| r.1).collect();
128 (dni, dhi)
129}
130
131pub fn disc_batch(
133 ghi: &[f64],
134 solar_zenith: &[f64],
135 day_of_year: &[i32],
136 pressure: Option<f64>,
137) -> (Vec<f64>, Vec<f64>, Vec<f64>) {
138 let n = ghi.len();
139 assert_eq!(solar_zenith.len(), n, "solar_zenith len mismatch");
140 assert_eq!(day_of_year.len(), n, "day_of_year len mismatch");
141 let results: Vec<_> = ghi.par_iter()
142 .zip(solar_zenith.par_iter())
143 .zip(day_of_year.par_iter())
144 .map(|((g, z), d)| irradiance::disc(*g, *z, *d, pressure))
145 .collect();
146 let dni = results.iter().map(|r| r.dni).collect();
147 let kt = results.iter().map(|r| r.kt).collect();
148 let am = results.iter().map(|r| r.airmass).collect();
149 (dni, kt, am)
150}
151
152#[allow(clippy::too_many_arguments)]
154pub fn perez_batch(
155 surface_tilt: f64,
156 surface_azimuth: f64,
157 dhi: &[f64],
158 dni: &[f64],
159 dni_extra: &[f64],
160 solar_zenith: &[f64],
161 solar_azimuth: &[f64],
162 airmass: &[f64],
163 aoi_vals: &[f64],
164) -> Vec<f64> {
165 let n = dhi.len();
166 assert_eq!(dni.len(), n, "dni len mismatch");
167 assert_eq!(dni_extra.len(), n, "dni_extra len mismatch");
168 assert_eq!(solar_zenith.len(), n, "solar_zenith len mismatch");
169 assert_eq!(solar_azimuth.len(), n, "solar_azimuth len mismatch");
170 assert_eq!(airmass.len(), n, "airmass len mismatch");
171 assert_eq!(aoi_vals.len(), n, "aoi_vals len mismatch");
172 (0..n).into_par_iter()
173 .map(|i| irradiance::perez(
174 surface_tilt, surface_azimuth,
175 dhi[i], dni[i], dni_extra[i],
176 solar_zenith[i], solar_azimuth[i],
177 airmass[i], aoi_vals[i],
178 ))
179 .collect()
180}
181
182#[allow(clippy::too_many_arguments)]
184pub fn total_irradiance_batch(
185 surface_tilt: f64,
186 surface_azimuth: f64,
187 solar_zenith: &[f64],
188 solar_azimuth: &[f64],
189 dni: &[f64],
190 ghi: &[f64],
191 dhi: &[f64],
192 albedo: f64,
193 model: irradiance::DiffuseModel,
194 dni_extra: &[f64],
195 airmass: &[f64],
196) -> Vec<irradiance::PoaComponents> {
197 let n = solar_zenith.len();
198 assert_eq!(solar_azimuth.len(), n, "solar_azimuth len mismatch");
199 assert_eq!(dni.len(), n, "dni len mismatch");
200 assert_eq!(ghi.len(), n, "ghi len mismatch");
201 assert_eq!(dhi.len(), n, "dhi len mismatch");
202 assert_eq!(dni_extra.len(), n, "dni_extra len mismatch");
203 assert_eq!(airmass.len(), n, "airmass len mismatch");
204 (0..n).into_par_iter()
205 .map(|i| irradiance::get_total_irradiance(
206 surface_tilt, surface_azimuth,
207 solar_zenith[i], solar_azimuth[i],
208 dni[i], ghi[i], dhi[i],
209 albedo, model,
210 Some(dni_extra[i]),
211 Some(airmass[i]),
212 ))
213 .collect()
214}
215
216#[allow(clippy::too_many_arguments)]
222pub fn sapm_cell_temperature_batch(
223 poa_global: &[f64],
224 temp_air: &[f64],
225 wind_speed: &[f64],
226 a: f64, b: f64, delta_t: f64, irrad_ref: f64,
227) -> (Vec<f64>, Vec<f64>) {
228 let n = poa_global.len();
229 assert_eq!(temp_air.len(), n, "temp_air len mismatch");
230 assert_eq!(wind_speed.len(), n, "wind_speed len mismatch");
231 let results: Vec<_> = (0..n).into_par_iter()
232 .map(|i| temperature::sapm_cell_temperature(
233 poa_global[i], temp_air[i], wind_speed[i], a, b, delta_t, irrad_ref,
234 ))
235 .collect();
236 let cell = results.iter().map(|r| r.0).collect();
237 let module = results.iter().map(|r| r.1).collect();
238 (cell, module)
239}
240
241pub fn faiman_batch(
243 poa_global: &[f64],
244 temp_air: &[f64],
245 wind_speed: &[f64],
246 u0: f64,
247 u1: f64,
248) -> Vec<f64> {
249 let n = poa_global.len();
250 assert_eq!(temp_air.len(), n, "temp_air len mismatch");
251 assert_eq!(wind_speed.len(), n, "wind_speed len mismatch");
252 (0..n).into_par_iter()
253 .map(|i| temperature::faiman(poa_global[i], temp_air[i], wind_speed[i], u0, u1))
254 .collect()
255}
256
257pub fn iam_physical_batch(aoi: &[f64], n: f64, k: f64, l: f64) -> Vec<f64> {
263 aoi.par_iter()
264 .map(|a| iam::physical(*a, n, k, l))
265 .collect()
266}
267
268pub fn iam_ashrae_batch(aoi: &[f64], b0: f64) -> Vec<f64> {
270 aoi.par_iter()
271 .map(|a| iam::ashrae(*a, b0))
272 .collect()
273}
274
275pub fn pvwatts_ac_batch(
281 pdc: &[f64],
282 pdc0: f64,
283 eta_inv_nom: f64,
284 eta_inv_ref: f64,
285) -> Vec<f64> {
286 pdc.par_iter()
287 .map(|p| inverter::pvwatts_ac(*p, pdc0, eta_inv_nom, eta_inv_ref))
288 .collect()
289}
290
291#[derive(Debug, Clone)]
297pub struct WeatherSeries {
298 pub times: Vec<chrono::DateTime<chrono_tz::Tz>>,
299 pub ghi: Vec<f64>,
300 pub dni: Vec<f64>,
301 pub dhi: Vec<f64>,
302 pub temp_air: Vec<f64>,
303 pub wind_speed: Vec<f64>,
304 pub albedo: Option<Vec<f64>>,
305}
306
307#[derive(Debug, Clone)]
309pub struct SimulationSeries {
310 pub solar_zenith: Vec<f64>,
311 pub solar_azimuth: Vec<f64>,
312 pub airmass: Vec<f64>,
313 pub aoi: Vec<f64>,
314 pub poa_global: Vec<f64>,
315 pub poa_direct: Vec<f64>,
316 pub poa_diffuse: Vec<f64>,
317 pub cell_temperature: Vec<f64>,
318 pub effective_irradiance: Vec<f64>,
319 pub dc_power: Vec<f64>,
320 pub ac_power: Vec<f64>,
321}
322
323impl SimulationSeries {
324 pub fn total_energy_wh(&self) -> f64 {
326 self.ac_power.iter().filter(|p| **p > 0.0).sum()
327 }
328
329 pub fn peak_power(&self) -> f64 {
331 self.ac_power.iter().cloned().fold(0.0_f64, f64::max)
332 }
333
334 pub fn capacity_factor(&self, system_capacity_w: f64) -> f64 {
336 let hours = self.ac_power.len() as f64;
337 if hours == 0.0 || system_capacity_w == 0.0 { return 0.0; }
338 self.total_energy_wh() / (system_capacity_w * hours)
339 }
340}
341
342pub struct BatchModelChain {
348 pub location: crate::location::Location,
349 pub surface_tilt: f64,
350 pub surface_azimuth: f64,
351 pub system_capacity_dc: f64,
352 pub gamma_pdc: f64,
354 pub inverter_capacity: f64,
355 pub inverter_efficiency: f64,
356 pub albedo: f64,
357 pub transposition_model: irradiance::DiffuseModel,
358}
359
360impl BatchModelChain {
361 pub fn pvwatts(
363 location: crate::location::Location,
364 surface_tilt: f64,
365 surface_azimuth: f64,
366 system_capacity_dc: f64,
367 ) -> Self {
368 Self {
369 location,
370 surface_tilt,
371 surface_azimuth,
372 system_capacity_dc,
373 gamma_pdc: -0.004,
374 inverter_capacity: system_capacity_dc,
375 inverter_efficiency: 0.96,
376 albedo: 0.2,
377 transposition_model: irradiance::DiffuseModel::Perez,
378 }
379 }
380
381 pub fn with_gamma_pdc(mut self, gamma_pdc: f64) -> Self {
383 self.gamma_pdc = gamma_pdc;
384 self
385 }
386
387 pub fn with_inverter(mut self, capacity: f64, efficiency: f64) -> Self {
389 self.inverter_capacity = capacity;
390 self.inverter_efficiency = efficiency;
391 self
392 }
393
394 pub fn with_albedo(mut self, albedo: f64) -> Self {
396 self.albedo = albedo;
397 self
398 }
399
400 pub fn with_transposition(mut self, model: irradiance::DiffuseModel) -> Self {
402 self.transposition_model = model;
403 self
404 }
405
406 pub fn run(&self, weather: &WeatherSeries) -> Result<SimulationSeries, spa::SpaError> {
410 let n = weather.times.len();
411 assert_eq!(weather.ghi.len(), n);
412 assert_eq!(weather.dni.len(), n, "dni len mismatch");
413 assert_eq!(weather.dhi.len(), n, "dhi len mismatch");
414 assert_eq!(weather.temp_air.len(), n, "temp_air len mismatch");
415 assert_eq!(weather.wind_speed.len(), n, "wind_speed len mismatch");
416 if let Some(albedos) = &weather.albedo {
417 assert_eq!(albedos.len(), n, "albedo len mismatch");
418 }
419
420 let pressure = atmosphere::alt2pres(self.location.altitude);
421 let albedo_default = self.albedo;
422
423 let results: Result<Vec<_>, _> = (0..n).into_par_iter().map(|i| {
425 let solpos = solarposition::get_solarposition(&self.location, weather.times[i])?;
427
428 let am_rel = atmosphere::get_relative_airmass(solpos.zenith);
430 let am_abs = if am_rel.is_nan() || am_rel <= 0.0 {
431 0.0
432 } else {
433 atmosphere::get_absolute_airmass(am_rel, pressure)
434 };
435
436 let aoi_val = irradiance::aoi(
438 self.surface_tilt, self.surface_azimuth,
439 solpos.zenith, solpos.azimuth,
440 );
441
442 let doy = weather.times[i].format("%j").to_string().parse::<i32>().unwrap_or(1);
444 let dni_extra = irradiance::get_extra_radiation(doy);
445
446 let poa = irradiance::get_total_irradiance(
448 self.surface_tilt, self.surface_azimuth,
449 solpos.zenith, solpos.azimuth,
450 weather.dni[i], weather.ghi[i], weather.dhi[i],
451 weather.albedo.as_ref().map_or(albedo_default, |a| a[i]),
452 self.transposition_model,
453 Some(dni_extra),
454 if am_rel.is_nan() { None } else { Some(am_rel) },
455 );
456
457 let iam_val = iam::physical(aoi_val, 1.526, 4.0, 0.002);
460
461 let spectral_modifier = 1.0;
463 let eff_irrad = ((poa.poa_direct * iam_val + poa.poa_diffuse) * spectral_modifier).max(0.0);
464
465 let t_cell = weather.temp_air[i] + poa.poa_global * (45.0 - 20.0) / 800.0;
468
469 let pdc = self.system_capacity_dc * (eff_irrad / 1000.0)
471 * (1.0 + self.gamma_pdc * (t_cell - 25.0));
472 let pdc = pdc.max(0.0);
473
474 let pac = inverter::pvwatts_ac(
476 pdc, self.system_capacity_dc,
477 self.inverter_efficiency, 0.9637,
478 );
479
480 Ok((solpos.zenith, solpos.azimuth, am_abs, aoi_val,
481 poa.poa_global, poa.poa_direct, poa.poa_diffuse,
482 t_cell, eff_irrad, pdc, pac))
483 }).collect();
484
485 let results = results?;
486
487 Ok(SimulationSeries {
488 solar_zenith: results.iter().map(|r| r.0).collect(),
489 solar_azimuth: results.iter().map(|r| r.1).collect(),
490 airmass: results.iter().map(|r| r.2).collect(),
491 aoi: results.iter().map(|r| r.3).collect(),
492 poa_global: results.iter().map(|r| r.4).collect(),
493 poa_direct: results.iter().map(|r| r.5).collect(),
494 poa_diffuse: results.iter().map(|r| r.6).collect(),
495 cell_temperature: results.iter().map(|r| r.7).collect(),
496 effective_irradiance: results.iter().map(|r| r.8).collect(),
497 dc_power: results.iter().map(|r| r.9).collect(),
498 ac_power: results.iter().map(|r| r.10).collect(),
499 })
500 }
501}