1use std::f64::consts::PI;
2use crate::atmosphere;
3
4const PEREZ_COEFFICIENTS: [[f64; 6]; 8] = [
8 [-0.0083117, 0.5877277, -0.0620636, -0.0596012, 0.0721249, -0.0220216],
9 [0.1299457, 0.6825954, -0.1513752, -0.0189325, 0.0659650, -0.0288748],
10 [0.3296958, 0.4868735, -0.2210958, 0.0554140, -0.0639588, -0.0260542],
11 [0.5682053, 0.1874990, -0.2951290, 0.1088631, -0.1519229, -0.0139754],
12 [0.8730280, -0.3920403, -0.3616149, 0.2255647, -0.4620442, 0.0012448],
13 [1.1326077, -1.2367284, -0.4118494, 0.2877813, -0.8230357, 0.0558225],
14 [1.0601591, -1.5999137, -0.3589221, 0.2642124, -1.1272340, 0.1310694],
15 [0.6777470, -0.3272588, -0.2504286, 0.1561313, -1.3765031, 0.2506212],
16];
17
18#[inline]
20pub fn aoi(surface_tilt: f64, surface_azimuth: f64, solar_zenith: f64, solar_azimuth: f64) -> f64 {
21 let tilt_rad = surface_tilt.to_radians();
22 let surf_az_rad = surface_azimuth.to_radians();
23 let zen_rad = solar_zenith.to_radians();
24 let sol_az_rad = solar_azimuth.to_radians();
25
26 let cos_aoi = zen_rad.cos() * tilt_rad.cos()
27 + zen_rad.sin() * tilt_rad.sin() * (sol_az_rad - surf_az_rad).cos();
28
29 let cos_aoi = cos_aoi.clamp(-1.0, 1.0);
30 cos_aoi.acos().to_degrees()
31}
32
33#[inline]
35pub fn get_extra_radiation(dayofyear: i32) -> f64 {
36 let b = 2.0 * PI * ((dayofyear - 1) as f64) / 365.0;
37 let rover_r0_sqrd = 1.00011
38 + 0.034221 * b.cos()
39 + 0.00128 * b.sin()
40 + 0.000719 * (2.0 * b).cos()
41 + 0.000077 * (2.0 * b).sin();
42 1366.1 * rover_r0_sqrd
43}
44
45#[inline]
47pub fn isotropic(surface_tilt: f64, dhi: f64) -> f64 {
48 dhi * (1.0 + surface_tilt.to_radians().cos()) / 2.0
49}
50
51#[inline]
57pub fn haydavies(surface_tilt: f64, _surface_azimuth: f64, dhi: f64, dni: f64, dni_extra: f64, solar_zenith: f64, _solar_azimuth: f64, aoi_in: f64) -> f64 {
58 let mut a = 0.0;
59 if dni_extra > 0.0 {
60 a = dni / dni_extra;
61 }
62 let a = a.clamp(0.0, 1.0);
63 let mut cos_z = solar_zenith.to_radians().cos();
64 if cos_z < 85.0_f64.to_radians().cos() { cos_z = 85.0_f64.to_radians().cos(); }
65
66 let cos_aoi = aoi_in.to_radians().cos().max(0.0);
67 let r_b = cos_aoi / cos_z;
68
69 dhi * ((1.0 - a) * (1.0 + surface_tilt.to_radians().cos()) / 2.0 + a * r_b)
70}
71
72#[inline]
78pub fn klucher(surface_tilt: f64, _surface_azimuth: f64, dhi: f64, ghi: f64, solar_zenith: f64, _solar_azimuth: f64, aoi_in: f64) -> f64 {
79 let mut f = 0.0;
80 if ghi > 0.0 {
81 let frac = dhi / ghi;
82 f = 1.0 - frac * frac;
83 }
84 let f = f.clamp(0.0, 1.0);
85
86 let _cos_z = solar_zenith.to_radians().cos();
87 let cos_aoi = aoi_in.to_radians().cos().max(0.0);
88 let tilt_rad = surface_tilt.to_radians();
89
90 let term1 = 1.0 + f * (tilt_rad / 2.0).sin().powi(3);
91 let term2 = 1.0 + f * cos_aoi.powi(2) * (solar_zenith.to_radians().sin()).powi(3);
92
93 dhi * ((1.0 + tilt_rad.cos()) / 2.0) * term1 * term2
94}
95
96#[inline]
103pub fn perez(surface_tilt: f64, _surface_azimuth: f64, dhi: f64, dni: f64, dni_extra: f64, solar_zenith: f64, _solar_azimuth: f64, airmass: f64, aoi_in: f64) -> f64 {
104 let mut cos_z = solar_zenith.to_radians().cos();
105 if cos_z < 85.0_f64.to_radians().cos() { cos_z = 85.0_f64.to_radians().cos(); }
106 let cos_aoi = aoi_in.to_radians().cos().max(0.0); let _a = (dni_extra * 1e-6).max(1.0); let delta = dhi * airmass / dni_extra;
111
112 let mut epsilon = 1.0;
113 if dhi > 0.0 {
114 epsilon = ((dhi + dni) / dhi + 1.041 * solar_zenith.to_radians().powi(3)) /
115 (1.0 + 1.041 * solar_zenith.to_radians().powi(3));
116 }
117
118 let bin = if epsilon < 1.065 { 0 }
119 else if epsilon < 1.230 { 1 }
120 else if epsilon < 1.500 { 2 }
121 else if epsilon < 1.950 { 3 }
122 else if epsilon < 2.800 { 4 }
123 else if epsilon < 4.500 { 5 }
124 else if epsilon < 6.200 { 6 }
125 else { 7 };
126
127 let coeffs = PEREZ_COEFFICIENTS[bin];
128 let mut f1 = coeffs[0] + coeffs[1] * delta + coeffs[2] * solar_zenith.to_radians();
129 f1 = f1.max(0.0);
130 let f2 = coeffs[3] + coeffs[4] * delta + coeffs[5] * solar_zenith.to_radians();
131
132 let a_perez = cos_aoi;
133 let b_perez = cos_z;
134
135 dhi * ((1.0 - f1) * (1.0 + surface_tilt.to_radians().cos()) / 2.0 + f1 * a_perez / b_perez + f2 * surface_tilt.to_radians().sin())
136}
137
138#[inline]
145pub fn erbs(ghi: f64, zenith: f64, _day_of_year: u32, dni_extra: f64) -> (f64, f64) {
146 if ghi <= 0.0 || zenith >= 87.0 { return (0.0, ghi); }
147 let mut cos_z = zenith.to_radians().cos();
148 if cos_z < 85.0_f64.to_radians().cos() { cos_z = 85.0_f64.to_radians().cos(); }
149
150 let kt = ghi / (dni_extra * cos_z);
151
152 let kd = if kt <= 0.22 {
153 1.0 - 0.09 * kt
154 } else if kt <= 0.80 {
155 0.9511 - 0.1604 * kt + 4.388 * kt.powi(2) - 16.638 * kt.powi(3) + 12.336 * kt.powi(4)
156 } else {
157 0.165
158 };
159
160 let dhi = ghi * kd.clamp(0.0, 1.0);
161 let dni = (ghi - dhi) / cos_z;
162 if dni < 0.0 { return (0.0, ghi); }
163
164 (dni, dhi)
165}
166
167#[inline]
174pub fn boland(ghi: f64, zenith: f64, dni_extra: f64) -> (f64, f64) {
175 if ghi <= 0.0 || zenith >= 90.0 { return (0.0, 0.0); }
176 let cos_z = zenith.to_radians().cos().max(85.0_f64.to_radians().cos());
177
178 let kt = ghi / (dni_extra * cos_z);
179
180 let a_coeff = 8.645;
183 let b_coeff = 0.613;
184 let kd = 1.0 / (1.0 + (a_coeff * (kt - b_coeff)).exp());
185 let dhi = ghi * kd.clamp(0.0, 1.0);
186 let dni = ((ghi - dhi) / cos_z).max(0.0);
187
188 (dni, dhi)
189}
190
191#[inline]
200pub fn dirint(ghi: f64, zenith: f64, _dew_point: f64, _pressure: f64, dni_extra: f64) -> (f64, f64) {
201 if ghi <= 0.0 || zenith >= 90.0 { return (0.0, 0.0); }
204 let cos_z = zenith.to_radians().cos().max(85.0_f64.to_radians().cos());
205
206 let kt = ghi / (dni_extra * cos_z);
207
208 let kd = if kt <= 0.2 {
210 0.99
211 } else if kt <= 0.8 {
212 0.95 - 0.9 * (kt - 0.2)
213 } else {
214 0.15
215 };
216
217 let dhi = ghi * kd.clamp(0.0, 1.0);
218 let dni = ((ghi - dhi) / cos_z).max(0.0);
219 (dni, dhi)
220}
221
222#[inline]
224pub fn poa_direct(aoi_in: f64, dni: f64) -> f64 {
225 let aoi_rad = aoi_in.to_radians();
226 if aoi_rad.abs() > std::f64::consts::PI / 2.0 {
227 0.0
228 } else {
229 (dni * aoi_rad.cos()).max(0.0)
230 }
231}
232
233#[allow(clippy::too_many_arguments)]
240#[inline]
241pub fn reindl(surface_tilt: f64, dhi: f64, ghi: f64, dni: f64, dni_extra: f64, solar_zenith: f64, aoi_in: f64) -> f64 {
242 let mut a = 0.0;
243 if dni_extra > 0.0 { a = dni / dni_extra; }
244 let a = a.clamp(0.0, 1.0);
245
246 let cos_z = solar_zenith.to_radians().cos().max(85.0_f64.to_radians().cos());
247 let cos_aoi = aoi_in.to_radians().cos().max(0.0);
248 let r_b = cos_aoi / cos_z;
249
250 let cos_z_reindl = solar_zenith.to_radians().cos().max(0.0);
251 let f = if ghi > 0.0 { ((dni * cos_z_reindl) / ghi).sqrt() } else { 0.0 };
252
253 let tilt_rad = surface_tilt.to_radians();
254 let term1 = dhi * (1.0 - a) * (1.0 + tilt_rad.cos()) / 2.0 * (1.0 + f * (tilt_rad / 2.0).sin().powi(3));
255 let term2 = dhi * a * r_b;
256
257 term1 + term2
258}
259
260#[inline]
264pub fn clearness_index(ghi: f64, solar_zenith: f64, dni_extra: f64) -> f64 {
265 let cos_z = solar_zenith.to_radians().cos().max(0.01);
266 let ghi_extra = dni_extra * cos_z;
267 if ghi_extra <= 0.0 { 0.0 } else { (ghi / ghi_extra).clamp(0.0, 1.0) }
268}
269
270#[inline]
275pub fn clearness_index_zenith_independent(clearness_idx: f64, _solar_zenith: f64, airmass_absolute: f64) -> f64 {
276 let am = airmass_absolute.max(1.0);
277 let denominator = 1.031 * (-1.4 / (0.9 + 9.4 / am)).exp() + 0.1;
279 (clearness_idx / denominator).max(0.0)
280}
281
282#[inline]
291pub fn aoi_projection(surface_tilt: f64, surface_azimuth: f64, solar_zenith: f64, solar_azimuth: f64) -> f64 {
292 let tilt_rad = surface_tilt.to_radians();
293 let surf_az_rad = surface_azimuth.to_radians();
294 let zen_rad = solar_zenith.to_radians();
295 let sol_az_rad = solar_azimuth.to_radians();
296
297 let projection = zen_rad.cos() * tilt_rad.cos()
298 + zen_rad.sin() * tilt_rad.sin() * (sol_az_rad - surf_az_rad).cos();
299
300 projection.clamp(-1.0, 1.0)
301}
302
303#[inline]
314pub fn beam_component(surface_tilt: f64, surface_azimuth: f64, solar_zenith: f64, solar_azimuth: f64, dni: f64) -> f64 {
315 let proj = aoi_projection(surface_tilt, surface_azimuth, solar_zenith, solar_azimuth);
316 (dni * proj).max(0.0)
317}
318
319#[inline]
333pub fn get_ground_diffuse(surface_tilt: f64, ghi: f64, albedo: f64) -> f64 {
334 ghi * albedo * (1.0 - surface_tilt.to_radians().cos()) * 0.5
335}
336
337#[derive(Debug, Clone, Copy)]
339pub struct PoaComponents {
340 pub poa_global: f64,
342 pub poa_direct: f64,
344 pub poa_diffuse: f64,
346 pub poa_sky_diffuse: f64,
348 pub poa_ground_diffuse: f64,
350}
351
352#[inline]
364pub fn poa_components(aoi_val: f64, dni: f64, poa_sky_diffuse: f64, poa_ground_diffuse: f64) -> PoaComponents {
365 let poa_direct = (dni * aoi_val.to_radians().cos()).max(0.0);
366 let poa_diffuse = poa_sky_diffuse + poa_ground_diffuse;
367 let poa_global = poa_direct + poa_diffuse;
368
369 PoaComponents {
370 poa_global,
371 poa_direct,
372 poa_diffuse,
373 poa_sky_diffuse,
374 poa_ground_diffuse,
375 }
376}
377
378pub type TotalIrradiance = PoaComponents;
380
381#[derive(Debug, Clone, Copy, PartialEq, Eq)]
383pub enum DiffuseModel {
384 Isotropic,
385 Klucher,
386 HayDavies,
387 Reindl,
388 Perez,
389}
390
391#[allow(clippy::too_many_arguments)]
408#[inline]
409pub fn get_sky_diffuse(
410 surface_tilt: f64,
411 surface_azimuth: f64,
412 solar_zenith: f64,
413 solar_azimuth: f64,
414 dni: f64,
415 ghi: f64,
416 dhi: f64,
417 model: DiffuseModel,
418 dni_extra: Option<f64>,
419 airmass: Option<f64>,
420) -> f64 {
421 let aoi_val = aoi(surface_tilt, surface_azimuth, solar_zenith, solar_azimuth);
422
423 match model {
424 DiffuseModel::Isotropic => isotropic(surface_tilt, dhi),
425 DiffuseModel::Klucher => klucher(surface_tilt, surface_azimuth, dhi, ghi, solar_zenith, solar_azimuth, aoi_val),
426 DiffuseModel::HayDavies => {
427 let extra = dni_extra.unwrap_or(0.0);
428 haydavies(surface_tilt, surface_azimuth, dhi, dni, extra, solar_zenith, solar_azimuth, aoi_val)
429 }
430 DiffuseModel::Reindl => {
431 let extra = dni_extra.unwrap_or(0.0);
432 reindl(surface_tilt, dhi, ghi, dni, extra, solar_zenith, aoi_val)
433 }
434 DiffuseModel::Perez => {
435 let extra = dni_extra.unwrap_or(0.0);
436 let am = airmass.unwrap_or_else(|| atmosphere::get_relative_airmass(solar_zenith));
437 perez(surface_tilt, surface_azimuth, dhi, dni, extra, solar_zenith, solar_azimuth, am, aoi_val)
438 }
439 }
440}
441
442#[allow(clippy::too_many_arguments)]
458#[inline]
459pub fn get_total_irradiance(
460 surface_tilt: f64,
461 surface_azimuth: f64,
462 solar_zenith: f64,
463 solar_azimuth: f64,
464 dni: f64,
465 ghi: f64,
466 dhi: f64,
467 albedo: f64,
468 model: DiffuseModel,
469 dni_extra: Option<f64>,
470 airmass: Option<f64>,
471) -> TotalIrradiance {
472 let aoi_val = aoi(surface_tilt, surface_azimuth, solar_zenith, solar_azimuth);
473
474 let sky_diffuse = get_sky_diffuse(
475 surface_tilt, surface_azimuth, solar_zenith, solar_azimuth,
476 dni, ghi, dhi, model, dni_extra, airmass,
477 );
478
479 let ground_diffuse = get_ground_diffuse(surface_tilt, ghi, albedo);
480
481 poa_components(aoi_val, dni, sky_diffuse, ground_diffuse)
482}
483
484#[derive(Debug, Clone, Copy)]
486pub struct DiscOutput {
487 pub dni: f64,
489 pub kt: f64,
491 pub airmass: f64,
493}
494
495fn disc_kn(kt: f64, am: f64) -> (f64, f64) {
497 let am = am.min(12.0);
498
499 let (a, b, c) = if kt <= 0.6 {
500 (
501 0.512 + kt * (-1.56 + kt * (2.286 - 2.222 * kt)),
502 0.37 + 0.962 * kt,
503 -0.28 + kt * (0.932 - 2.048 * kt),
504 )
505 } else {
506 (
507 -5.743 + kt * (21.77 + kt * (-27.49 + 11.56 * kt)),
508 41.4 + kt * (-118.5 + kt * (66.05 + 31.9 * kt)),
509 -47.01 + kt * (184.2 + kt * (-222.0 + 73.81 * kt)),
510 )
511 };
512
513 let delta_kn = a + b * (c * am).exp();
514 let knc = 0.866 + am * (-0.122 + am * (0.0121 + am * (-0.000653 + 1.4e-05 * am)));
515 let kn = knc - delta_kn;
516
517 (kn, am)
518}
519
520#[inline]
537pub fn disc(ghi: f64, solar_zenith: f64, day_of_year: i32, pressure: Option<f64>) -> DiscOutput {
538 let max_zenith = 87.0;
539 let min_cos_zenith = 0.065;
540
541 let b = 2.0 * PI * ((day_of_year - 1) as f64) / 365.0;
543 let rover = 1.00011 + 0.034221 * b.cos() + 0.00128 * b.sin()
544 + 0.000719 * (2.0 * b).cos() + 0.000077 * (2.0 * b).sin();
545 let i0 = 1370.0 * rover;
546
547 let cos_z = solar_zenith.to_radians().cos().max(min_cos_zenith);
549 let ghi_extra = i0 * cos_z;
550 let kt = if ghi_extra > 0.0 { (ghi / ghi_extra).clamp(0.0, 1.0) } else { 0.0 };
551
552 let mut am = {
555 let z = solar_zenith;
556 let cos_z = z.to_radians().cos();
557 let c = 93.885 - z;
558 if c <= 0.0 {
559 f64::NAN
560 } else {
561 1.0 / (cos_z + 0.15 * c.powf(-1.253))
562 }
563 };
564 if let Some(p) = pressure {
565 am = atmosphere::get_absolute_airmass(am, p);
566 }
567
568 let (kn, am) = disc_kn(kt, am);
569 let mut dni = kn * i0;
570
571 if solar_zenith > max_zenith || ghi < 0.0 || dni < 0.0 {
572 dni = 0.0;
573 }
574
575 DiscOutput { dni, kt, airmass: am }
576}
577
578#[derive(Debug, Clone, Copy)]
580pub struct ErbsDriesseOutput {
581 pub dni: f64,
583 pub dhi: f64,
585 pub kt: f64,
587}
588
589#[inline]
605pub fn erbs_driesse(ghi: f64, solar_zenith: f64, day_of_year: i32) -> ErbsDriesseOutput {
606 let max_zenith = 87.0;
607 let min_cos_zenith = 0.065;
608
609 let ghi = ghi.max(0.0);
610
611 let dni_extra = get_extra_radiation(day_of_year);
612
613 let cos_z = solar_zenith.to_radians().cos().max(min_cos_zenith);
615 let ghi_extra = dni_extra * cos_z;
616 let kt = if ghi_extra > 0.0 { (ghi / ghi_extra).clamp(0.0, 1.0) } else { 0.0 };
617
618 let p = [12.26911439571261, -16.4705084246973, 4.24692671521831700,
620 -0.11390583806313881, 0.946296633571001];
621
622 let df = if kt <= 0.216 {
624 1.0 - 0.09 * kt
625 } else if kt <= 0.792 {
626 p[0] * kt.powi(4) + p[1] * kt.powi(3) + p[2] * kt.powi(2) + p[3] * kt + p[4]
628 } else {
629 0.165
630 };
631
632 let dhi = df * ghi;
633 let mut dni = (ghi - dhi) / solar_zenith.to_radians().cos();
634
635 let bad = solar_zenith > max_zenith || ghi < 0.0 || dni < 0.0;
636 let dhi = if bad { ghi } else { dhi };
637 if bad {
638 dni = 0.0;
639 }
640
641 ErbsDriesseOutput { dni, dhi, kt }
642}
643
644#[inline]
655pub fn king(surface_tilt: f64, dhi: f64, ghi: f64, solar_zenith: f64) -> f64 {
656 let cos_tilt = surface_tilt.to_radians().cos();
657 let sky_diffuse = dhi * (1.0 + cos_tilt) / 2.0
658 + ghi * (0.012 * solar_zenith - 0.04) * (1.0 - cos_tilt) / 2.0;
659 sky_diffuse.max(0.0)
660}
661
662#[inline]
681pub fn dirindex(
682 ghi: f64,
683 ghi_clearsky: f64,
684 dni_clearsky: f64,
685 zenith: f64,
686 day_of_year: i32,
687 pressure: Option<f64>,
688) -> f64 {
689 let dni_extra = get_extra_radiation(day_of_year);
690 let p = pressure.unwrap_or(101325.0);
691
692 let (dni_dirint, _) = dirint(ghi, zenith, 0.0, p, dni_extra);
693 let (dni_dirint_clear, _) = dirint(ghi_clearsky, zenith, 0.0, p, dni_extra);
694
695 if dni_dirint_clear <= 0.0 {
696 return 0.0;
697 }
698
699 let dni = dni_clearsky * dni_dirint / dni_dirint_clear;
700 dni.max(0.0)
701}
702
703const PD_KNOTS: [f64; 13] = [
709 0.000, 0.000, 0.000,
710 0.061, 0.187, 0.333, 0.487, 0.643, 0.778, 0.839,
711 1.000, 1.000, 1.000,
712];
713
714const PD_COEFS: [[[f64; 13]; 3]; 2] = [
718 [
720 [-0.053, -0.008, 0.131, 0.328, 0.557, 0.861, 1.212, 1.099, 0.544, 0.544, 0.000, 0.000, 0.000],
722 [ 0.529, 0.588, 0.770, 0.471, 0.241, -0.323, -1.239, -1.847, 0.157, 0.157, 0.000, 0.000, 0.000],
724 [-0.028, -0.062, -0.167, -0.216, -0.300, -0.355, -0.444, -0.365, -0.213, -0.213, 0.000, 0.000, 0.000],
726 ],
727 [
729 [-0.071, -0.060, -0.026, 0.069, 0.086, 0.240, 0.305, 0.275, 0.118, 0.118, 0.000, 0.000, 0.000],
731 [ 0.061, 0.072, 0.106, -0.105, -0.085, -0.467, -0.797, -1.132, -1.455, -1.455, 0.000, 0.000, 0.000],
733 [-0.019, -0.022, -0.032, -0.028, -0.012, -0.008, 0.047, 0.124, 0.292, 0.292, 0.000, 0.000, 0.000],
735 ],
736];
737
738fn pd_splev(x: f64, coefs: &[f64; 13]) -> f64 {
742 let t = &PD_KNOTS;
743 let k = 2_usize; let n = t.len() - k - 1; let x = x.clamp(t[k], t[n]);
748
749 let mut span = k;
752 for i in k..n {
753 if t[i + 1] > x {
754 span = i;
755 break;
756 }
757 span = i;
758 }
759
760 let mut d = [0.0_f64; 3]; for j in 0..=k {
763 let idx = span - k + j;
764 if idx < 13 {
765 d[j] = coefs[idx];
766 }
767 }
768
769 for r in 1..=k {
771 for j in (r..=k).rev() {
772 let left = span + j - k;
773 let right = span + 1 + j - r;
774 let denom = t[right] - t[left];
775 if denom.abs() < 1e-15 {
776 d[j] = 0.0;
777 } else {
778 let alpha = (x - t[left]) / denom;
779 d[j] = (1.0 - alpha) * d[j - 1] + alpha * d[j];
780 }
781 }
782 }
783
784 d[k]
785}
786
787fn pd_calc_delta(dhi: f64, dni_extra: f64, solar_zenith: f64, airmass: Option<f64>) -> f64 {
789 let am = match airmass {
790 Some(a) => {
791 if solar_zenith >= 90.0 {
792 atmosphere::get_relative_airmass(89.999)
794 } else {
795 a
796 }
797 }
798 None => {
799 if solar_zenith >= 90.0 {
800 atmosphere::get_relative_airmass(89.999)
801 } else {
802 atmosphere::get_relative_airmass(solar_zenith)
803 }
804 }
805 };
806
807 let am = if am.is_nan() { atmosphere::get_relative_airmass(89.999) } else { am };
808
809 if dni_extra <= 0.0 || am <= 0.0 {
810 return 0.0;
811 }
812
813 dhi / (dni_extra / am)
814}
815
816fn pd_calc_zeta(dhi: f64, dni: f64, zenith: f64) -> f64 {
818 if dhi <= 0.0 && dni <= 0.0 {
819 return 0.0;
820 }
821
822 let sum = dhi + dni;
823 let mut zeta = if sum > 0.0 { dni / sum } else { 0.0 };
824
825 if dhi == 0.0 {
826 zeta = 0.0;
827 }
828
829 let kappa = 1.041;
831 let kterm = kappa * zenith.to_radians().powi(3);
832 let denom = 1.0 - kterm * (zeta - 1.0);
833 if denom.abs() > 1e-15 {
834 zeta /= denom;
835 }
836
837 zeta
838}
839
840fn pd_f(i: usize, j: usize, zeta: f64) -> f64 {
842 pd_splev(zeta, &PD_COEFS[i - 1][j - 1])
843}
844
845#[allow(clippy::too_many_arguments)]
867#[inline]
868pub fn perez_driesse(
869 surface_tilt: f64,
870 surface_azimuth: f64,
871 dhi: f64,
872 dni: f64,
873 dni_extra: f64,
874 solar_zenith: f64,
875 solar_azimuth: f64,
876 airmass: Option<f64>,
877) -> f64 {
878 let delta = pd_calc_delta(dhi, dni_extra, solar_zenith, airmass);
879 let zeta = pd_calc_zeta(dhi, dni, solar_zenith);
880
881 let z = solar_zenith.to_radians();
882
883 let f1 = pd_f(1, 1, zeta) + pd_f(1, 2, zeta) * delta + pd_f(1, 3, zeta) * z;
884 let f2 = pd_f(2, 1, zeta) + pd_f(2, 2, zeta) * delta + pd_f(2, 3, zeta) * z;
885
886 let f1 = f1.clamp(0.0, 0.9);
888
889 let a = aoi_projection(surface_tilt, surface_azimuth, solar_zenith, solar_azimuth).max(0.0);
891
892 let b = solar_zenith.to_radians().cos().max(85.0_f64.to_radians().cos());
894
895 let term1 = 0.5 * (1.0 - f1) * (1.0 + surface_tilt.to_radians().cos());
896 let term2 = f1 * a / b;
897 let term3 = f2 * surface_tilt.to_radians().sin();
898
899 (dhi * (term1 + term2 + term3)).max(0.0)
900}
901