1use chrono::DateTime;
2use chrono_tz::Tz;
3use crate::location::Location;
4use crate::pvsystem::PVSystem;
5use crate::solarposition::get_solarposition;
6use crate::irradiance::{
7 aoi, get_total_irradiance, get_extra_radiation, poa_direct, erbs,
8 DiffuseModel, PoaComponents,
9};
10use crate::atmosphere::{get_relative_airmass, get_absolute_airmass, alt2pres};
11use crate::iam;
12use crate::temperature;
13use crate::inverter;
14
15#[derive(Debug, Clone, Copy, PartialEq, Eq)]
21pub enum DCModel {
22 PVWatts,
24}
25
26#[derive(Debug, Clone, Copy, PartialEq, Eq)]
28pub enum ACModel {
29 PVWatts,
31 Sandia,
33 ADR,
35}
36
37#[derive(Debug, Clone, Copy, PartialEq, Eq)]
39pub enum AOIModel {
40 Physical,
42 ASHRAE,
44 SAPM,
46 MartinRuiz,
48 NoLoss,
50}
51
52#[derive(Debug, Clone, Copy, PartialEq, Eq)]
54pub enum SpectralModel {
55 NoLoss,
57}
58
59#[derive(Debug, Clone, Copy, PartialEq, Eq)]
61#[allow(non_camel_case_types)]
62pub enum TemperatureModel {
63 SAPM,
65 PVSyst,
67 Faiman,
69 Fuentes,
71 NOCT_SAM,
73 PVWatts,
75}
76
77#[derive(Debug, Clone, Copy, PartialEq, Eq)]
79pub enum TranspositionModel {
80 Isotropic,
82 HayDavies,
84 Perez,
86 Klucher,
88 Reindl,
90}
91
92#[derive(Debug, Clone, Copy, PartialEq, Eq)]
94pub enum LossesModel {
95 PVWatts,
97 NoLoss,
99}
100
101#[derive(Debug, Clone)]
107pub struct ModelChainConfig {
108 pub dc_model: DCModel,
109 pub ac_model: ACModel,
110 pub aoi_model: AOIModel,
111 pub spectral_model: SpectralModel,
112 pub temperature_model: TemperatureModel,
113 pub transposition_model: TranspositionModel,
114 pub losses_model: LossesModel,
115}
116
117#[derive(Debug, Clone)]
123pub struct WeatherInput {
124 pub time: DateTime<Tz>,
126 pub ghi: Option<f64>,
128 pub dni: Option<f64>,
130 pub dhi: Option<f64>,
132 pub temp_air: f64,
134 pub wind_speed: f64,
136 pub albedo: Option<f64>,
138}
139
140#[derive(Debug, Clone)]
142pub struct POAInput {
143 pub time: DateTime<Tz>,
145 pub poa_direct: f64,
147 pub poa_diffuse: f64,
149 pub poa_global: f64,
151 pub temp_air: f64,
153 pub wind_speed: f64,
155 pub aoi: f64,
157}
158
159#[derive(Debug, Clone)]
161pub struct EffectiveIrradianceInput {
162 pub time: DateTime<Tz>,
164 pub effective_irradiance: f64,
166 pub poa_global: f64,
168 pub temp_air: f64,
170 pub wind_speed: f64,
172}
173
174#[derive(Debug, Clone, PartialEq)]
180pub struct ModelChainResult {
181 pub solar_zenith: f64,
183 pub solar_azimuth: f64,
185 pub airmass: f64,
187 pub aoi: f64,
189 pub poa_global: f64,
191 pub poa_direct: f64,
192 pub poa_diffuse: f64,
193 pub aoi_modifier: f64,
195 pub spectral_modifier: f64,
197 pub effective_irradiance: f64,
199 pub cell_temperature: f64,
201 pub dc_power: f64,
203 pub ac_power: f64,
205}
206
207#[derive(Debug, Clone, PartialEq)]
209pub struct SimulationResult {
210 pub poa_global: f64,
211 pub temp_cell: f64,
212 pub dc_power: f64,
213 pub ac_power: f64,
214}
215
216pub struct ModelChain {
222 pub system: PVSystem,
223 pub location: Location,
224 pub surface_tilt: f64,
225 pub surface_azimuth: f64,
226 pub inverter_pac0: f64,
227 pub inverter_eta: f64,
228 pub config: ModelChainConfig,
229}
230
231impl ModelChain {
232 pub fn new(
234 system: PVSystem,
235 location: Location,
236 surface_tilt: f64,
237 surface_azimuth: f64,
238 inverter_pac0: f64,
239 inverter_eta: f64,
240 ) -> Self {
241 Self {
242 system,
243 location,
244 surface_tilt,
245 surface_azimuth,
246 inverter_pac0,
247 inverter_eta,
248 config: ModelChainConfig {
249 dc_model: DCModel::PVWatts,
250 ac_model: ACModel::PVWatts,
251 aoi_model: AOIModel::ASHRAE,
252 spectral_model: SpectralModel::NoLoss,
253 temperature_model: TemperatureModel::PVWatts,
254 transposition_model: TranspositionModel::Isotropic,
255 losses_model: LossesModel::NoLoss,
256 },
257 }
258 }
259
260 pub fn with_config(
262 system: PVSystem,
263 location: Location,
264 surface_tilt: f64,
265 surface_azimuth: f64,
266 inverter_pac0: f64,
267 inverter_eta: f64,
268 config: ModelChainConfig,
269 ) -> Self {
270 Self {
271 system,
272 location,
273 surface_tilt,
274 surface_azimuth,
275 inverter_pac0,
276 inverter_eta,
277 config,
278 }
279 }
280
281 pub fn with_pvwatts(
285 system: PVSystem,
286 location: Location,
287 surface_tilt: f64,
288 surface_azimuth: f64,
289 inverter_pac0: f64,
290 inverter_eta: f64,
291 ) -> Self {
292 Self {
293 system,
294 location,
295 surface_tilt,
296 surface_azimuth,
297 inverter_pac0,
298 inverter_eta,
299 config: ModelChainConfig {
300 dc_model: DCModel::PVWatts,
301 ac_model: ACModel::PVWatts,
302 aoi_model: AOIModel::Physical,
303 spectral_model: SpectralModel::NoLoss,
304 temperature_model: TemperatureModel::PVWatts,
305 transposition_model: TranspositionModel::Perez,
306 losses_model: LossesModel::PVWatts,
307 },
308 }
309 }
310
311 pub fn with_sapm(
315 system: PVSystem,
316 location: Location,
317 surface_tilt: f64,
318 surface_azimuth: f64,
319 inverter_pac0: f64,
320 inverter_eta: f64,
321 ) -> Self {
322 Self {
323 system,
324 location,
325 surface_tilt,
326 surface_azimuth,
327 inverter_pac0,
328 inverter_eta,
329 config: ModelChainConfig {
330 dc_model: DCModel::PVWatts,
331 ac_model: ACModel::PVWatts,
332 aoi_model: AOIModel::ASHRAE,
333 spectral_model: SpectralModel::NoLoss,
334 temperature_model: TemperatureModel::SAPM,
335 transposition_model: TranspositionModel::HayDavies,
336 losses_model: LossesModel::NoLoss,
337 },
338 }
339 }
340
341 pub fn run_model(
347 &self,
348 time: DateTime<Tz>,
349 _ghi: f64,
350 dni: f64,
351 dhi: f64,
352 temp_air: f64,
353 _wind_speed: f64,
354 ) -> Result<SimulationResult, spa::SpaError> {
355 let solpos = get_solarposition(&self.location, time)?;
356 let incidence = aoi(self.surface_tilt, self.surface_azimuth, solpos.zenith, solpos.azimuth);
357 let iam_mult = iam::ashrae(incidence, 0.05);
358 let poa_diffuse_val = crate::irradiance::isotropic(self.surface_tilt, dhi);
359 let poa_dir = poa_direct(incidence, dni);
360 let poa_global = poa_dir * iam_mult + poa_diffuse_val;
361 let temp_cell = temp_air + poa_global * (45.0 - 20.0) / 800.0;
362 let pdc = self.system.get_dc_power_total(poa_global, temp_cell);
363 let pdc0 = self.system.get_nameplate_dc_total();
364 let eta_inv_nom = self.inverter_eta;
365 let eta_inv_ref = 0.9637;
366 let pac = inverter::pvwatts_ac(pdc, pdc0, eta_inv_nom, eta_inv_ref);
367
368 Ok(SimulationResult {
369 poa_global,
370 temp_cell,
371 dc_power: pdc,
372 ac_power: pac,
373 })
374 }
375
376 pub fn run_model_from_weather(
386 &self,
387 weather: &WeatherInput,
388 ) -> Result<ModelChainResult, spa::SpaError> {
389 let (ghi, dni, dhi) = self.resolve_irradiance(weather)?;
391 let albedo = weather.albedo.unwrap_or(0.25);
392
393 let solpos = get_solarposition(&self.location, weather.time)?;
395
396 let am_rel = get_relative_airmass(solpos.zenith);
398 let pressure = alt2pres(self.location.altitude);
399 let am_abs = if am_rel.is_nan() {
400 0.0
401 } else {
402 get_absolute_airmass(am_rel, pressure)
403 };
404
405 let aoi_val = aoi(self.surface_tilt, self.surface_azimuth, solpos.zenith, solpos.azimuth);
407
408 let day_of_year = weather.time.format("%j").to_string().parse::<i32>().unwrap_or(1);
410 let dni_extra = get_extra_radiation(day_of_year);
411
412 let diffuse_model = match self.config.transposition_model {
414 TranspositionModel::Isotropic => DiffuseModel::Isotropic,
415 TranspositionModel::HayDavies => DiffuseModel::HayDavies,
416 TranspositionModel::Perez => DiffuseModel::Perez,
417 TranspositionModel::Klucher => DiffuseModel::Klucher,
418 TranspositionModel::Reindl => DiffuseModel::Reindl,
419 };
420
421 let poa = get_total_irradiance(
422 self.surface_tilt,
423 self.surface_azimuth,
424 solpos.zenith,
425 solpos.azimuth,
426 dni,
427 ghi,
428 dhi,
429 albedo,
430 diffuse_model,
431 Some(dni_extra),
432 if am_rel.is_nan() { None } else { Some(am_rel) },
433 );
434
435 self.compute_from_poa(
437 solpos.zenith,
438 solpos.azimuth,
439 am_abs,
440 aoi_val,
441 &poa,
442 weather.temp_air,
443 weather.wind_speed,
444 )
445 }
446
447 pub fn run_model_from_poa(
451 &self,
452 input: &POAInput,
453 ) -> Result<ModelChainResult, spa::SpaError> {
454 let solpos = get_solarposition(&self.location, input.time)?;
455 let am_rel = get_relative_airmass(solpos.zenith);
456 let pressure = alt2pres(self.location.altitude);
457 let am_abs = if am_rel.is_nan() { 0.0 } else { get_absolute_airmass(am_rel, pressure) };
458
459 let poa = PoaComponents {
460 poa_global: input.poa_global,
461 poa_direct: input.poa_direct,
462 poa_diffuse: input.poa_diffuse,
463 poa_sky_diffuse: input.poa_diffuse,
464 poa_ground_diffuse: 0.0,
465 };
466
467 self.compute_from_poa(
468 solpos.zenith,
469 solpos.azimuth,
470 am_abs,
471 input.aoi,
472 &poa,
473 input.temp_air,
474 input.wind_speed,
475 )
476 }
477
478 pub fn run_model_from_effective_irradiance(
482 &self,
483 input: &EffectiveIrradianceInput,
484 ) -> Result<ModelChainResult, spa::SpaError> {
485 let solpos = get_solarposition(&self.location, input.time)?;
486 let am_rel = get_relative_airmass(solpos.zenith);
487 let pressure = alt2pres(self.location.altitude);
488 let am_abs = if am_rel.is_nan() { 0.0 } else { get_absolute_airmass(am_rel, pressure) };
489
490 let temp_cell = self.calc_cell_temperature(
491 input.poa_global,
492 input.temp_air,
493 input.wind_speed,
494 );
495 let pdc = self.calc_dc_power(input.effective_irradiance, temp_cell);
496 let pac = self.calc_ac_power(pdc);
497
498 Ok(ModelChainResult {
499 solar_zenith: solpos.zenith,
500 solar_azimuth: solpos.azimuth,
501 airmass: am_abs,
502 aoi: 0.0,
503 poa_global: input.poa_global,
504 poa_direct: 0.0,
505 poa_diffuse: 0.0,
506 aoi_modifier: 1.0,
507 spectral_modifier: 1.0,
508 effective_irradiance: input.effective_irradiance,
509 cell_temperature: temp_cell,
510 dc_power: pdc,
511 ac_power: pac,
512 })
513 }
514
515 pub fn complete_irradiance(
521 &self,
522 weather: &WeatherInput,
523 ) -> Result<(f64, f64, f64), spa::SpaError> {
524 self.resolve_irradiance(weather)
525 }
526
527 fn resolve_irradiance(
532 &self,
533 weather: &WeatherInput,
534 ) -> Result<(f64, f64, f64), spa::SpaError> {
535 match (weather.ghi, weather.dni, weather.dhi) {
536 (Some(ghi), Some(dni), Some(dhi)) => Ok((ghi, dni, dhi)),
537 (Some(ghi), _, _) => {
538 let solpos = get_solarposition(&self.location, weather.time)?;
540 let day_of_year = weather.time.format("%j").to_string().parse::<i32>().unwrap_or(1);
541 let dni_extra = get_extra_radiation(day_of_year);
542 let (dni, dhi) = erbs(ghi, solpos.zenith, day_of_year as u32, dni_extra);
543 Ok((ghi, dni, dhi))
544 }
545 (None, Some(dni), Some(dhi)) => {
546 let solpos = get_solarposition(&self.location, weather.time)?;
548 let cos_z = solpos.zenith.to_radians().cos().max(0.0);
549 let ghi = dni * cos_z + dhi;
550 Ok((ghi, dni, dhi))
551 }
552 _ => {
553 Ok((0.0, 0.0, 0.0))
555 }
556 }
557 }
558
559 fn compute_from_poa(
560 &self,
561 solar_zenith: f64,
562 solar_azimuth: f64,
563 airmass: f64,
564 aoi_val: f64,
565 poa: &PoaComponents,
566 temp_air: f64,
567 wind_speed: f64,
568 ) -> Result<ModelChainResult, spa::SpaError> {
569 let aoi_modifier = self.calc_aoi_modifier(aoi_val);
571
572 let spectral_modifier = self.calc_spectral_modifier();
574
575 let effective_irradiance = (poa.poa_direct * aoi_modifier + poa.poa_diffuse)
577 * spectral_modifier;
578
579 let temp_cell = self.calc_cell_temperature(poa.poa_global, temp_air, wind_speed);
581
582 let pdc = self.calc_dc_power(effective_irradiance, temp_cell);
584
585 let pac = self.calc_ac_power(pdc);
587
588 Ok(ModelChainResult {
589 solar_zenith,
590 solar_azimuth,
591 airmass,
592 aoi: aoi_val,
593 poa_global: poa.poa_global,
594 poa_direct: poa.poa_direct,
595 poa_diffuse: poa.poa_diffuse,
596 aoi_modifier,
597 spectral_modifier,
598 effective_irradiance,
599 cell_temperature: temp_cell,
600 dc_power: pdc,
601 ac_power: pac,
602 })
603 }
604
605 fn calc_aoi_modifier(&self, aoi_val: f64) -> f64 {
606 match self.config.aoi_model {
607 AOIModel::Physical => iam::physical(aoi_val, 1.526, 4.0, 0.002),
608 AOIModel::ASHRAE => iam::ashrae(aoi_val, 0.05),
609 AOIModel::MartinRuiz => iam::martin_ruiz(aoi_val, 0.16),
610 AOIModel::SAPM => {
611 iam::sapm(aoi_val, 1.0, -0.002438, 3.103e-4, -1.246e-5, 2.112e-7, -1.359e-9)
612 }
613 AOIModel::NoLoss => 1.0,
614 }
615 }
616
617 fn calc_spectral_modifier(&self) -> f64 {
618 match self.config.spectral_model {
619 SpectralModel::NoLoss => 1.0,
620 }
621 }
622
623 fn calc_cell_temperature(&self, poa_global: f64, temp_air: f64, wind_speed: f64) -> f64 {
624 match self.config.temperature_model {
625 TemperatureModel::SAPM => {
626 let (temp_cell, _) = temperature::sapm_cell_temperature(
627 poa_global, temp_air, wind_speed, -3.56, -0.075, 3.0, 1000.0,
628 );
629 temp_cell
630 }
631 TemperatureModel::PVSyst => {
632 temperature::pvsyst_cell_temperature(
633 poa_global, temp_air, wind_speed, 29.0, 0.0, 0.15, 0.9,
634 )
635 }
636 TemperatureModel::Faiman => {
637 temperature::faiman(poa_global, temp_air, wind_speed, 25.0, 6.84)
638 }
639 TemperatureModel::Fuentes => {
640 temperature::fuentes(poa_global, temp_air, wind_speed, 45.0)
641 }
642 TemperatureModel::NOCT_SAM => {
643 temperature::noct_sam_default(poa_global, temp_air, wind_speed, 45.0, 0.15)
644 }
645 TemperatureModel::PVWatts => {
646 temp_air + poa_global * (45.0 - 20.0) / 800.0
647 }
648 }
649 }
650
651 fn calc_dc_power(&self, effective_irradiance: f64, temp_cell: f64) -> f64 {
652 match self.config.dc_model {
653 DCModel::PVWatts => self.system.get_dc_power_total(effective_irradiance, temp_cell),
654 }
655 }
656
657 fn calc_ac_power(&self, pdc: f64) -> f64 {
658 let pdc0 = self.system.get_nameplate_dc_total();
659 let eta_inv_nom = self.inverter_eta;
660 let eta_inv_ref = 0.9637;
661 match self.config.ac_model {
662 ACModel::PVWatts => inverter::pvwatts_ac(pdc, pdc0, eta_inv_nom, eta_inv_ref),
663 ACModel::Sandia => inverter::pvwatts_ac(pdc, pdc0, eta_inv_nom, eta_inv_ref), ACModel::ADR => inverter::pvwatts_ac(pdc, pdc0, eta_inv_nom, eta_inv_ref), }
666 }
667}