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#[allow(clippy::too_many_arguments)]
57#[inline]
58pub 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 {
59 let mut a = 0.0;
60 if dni_extra > 0.0 {
61 a = dni / dni_extra;
62 }
63 let a = a.clamp(0.0, 1.0);
64 let mut cos_z = solar_zenith.to_radians().cos();
65 if cos_z < 85.0_f64.to_radians().cos() { cos_z = 85.0_f64.to_radians().cos(); }
66
67 let cos_aoi = aoi_in.to_radians().cos().max(0.0);
68 let r_b = cos_aoi / cos_z;
69
70 dhi * ((1.0 - a) * (1.0 + surface_tilt.to_radians().cos()) / 2.0 + a * r_b)
71}
72
73#[inline]
79pub fn klucher(surface_tilt: f64, _surface_azimuth: f64, dhi: f64, ghi: f64, solar_zenith: f64, _solar_azimuth: f64, aoi_in: f64) -> f64 {
80 let mut f = 0.0;
81 if ghi > 0.0 {
82 let frac = dhi / ghi;
83 f = 1.0 - frac * frac;
84 }
85 let f = f.clamp(0.0, 1.0);
86
87 let _cos_z = solar_zenith.to_radians().cos();
88 let cos_aoi = aoi_in.to_radians().cos().max(0.0);
89 let tilt_rad = surface_tilt.to_radians();
90
91 let term1 = 1.0 + f * (tilt_rad / 2.0).sin().powi(3);
92 let term2 = 1.0 + f * cos_aoi.powi(2) * (solar_zenith.to_radians().sin()).powi(3);
93
94 dhi * ((1.0 + tilt_rad.cos()) / 2.0) * term1 * term2
95}
96
97#[allow(clippy::too_many_arguments)]
104#[inline]
105pub 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 {
106 let mut cos_z = solar_zenith.to_radians().cos();
107 if cos_z < 85.0_f64.to_radians().cos() { cos_z = 85.0_f64.to_radians().cos(); }
108 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;
113
114 let mut epsilon = 1.0;
115 if dhi > 0.0 {
116 epsilon = ((dhi + dni) / dhi + 1.041 * solar_zenith.to_radians().powi(3)) /
117 (1.0 + 1.041 * solar_zenith.to_radians().powi(3));
118 }
119
120 let bin = if epsilon < 1.065 { 0 }
121 else if epsilon < 1.230 { 1 }
122 else if epsilon < 1.500 { 2 }
123 else if epsilon < 1.950 { 3 }
124 else if epsilon < 2.800 { 4 }
125 else if epsilon < 4.500 { 5 }
126 else if epsilon < 6.200 { 6 }
127 else { 7 };
128
129 let coeffs = PEREZ_COEFFICIENTS[bin];
130 let mut f1 = coeffs[0] + coeffs[1] * delta + coeffs[2] * solar_zenith.to_radians();
131 f1 = f1.max(0.0);
132 let f2 = coeffs[3] + coeffs[4] * delta + coeffs[5] * solar_zenith.to_radians();
133
134 let a_perez = cos_aoi;
135 let b_perez = cos_z;
136
137 dhi * ((1.0 - f1) * (1.0 + surface_tilt.to_radians().cos()) / 2.0 + f1 * a_perez / b_perez + f2 * surface_tilt.to_radians().sin())
138}
139
140#[inline]
147pub fn erbs(ghi: f64, zenith: f64, _day_of_year: u32, dni_extra: f64) -> (f64, f64) {
148 if !ghi.is_finite() || !zenith.is_finite() || !dni_extra.is_finite() {
149 return (0.0, 0.0);
150 }
151 if ghi <= 0.0 || zenith >= 87.0 { return (0.0, ghi); }
152 let mut cos_z = zenith.to_radians().cos();
153 if cos_z < 85.0_f64.to_radians().cos() { cos_z = 85.0_f64.to_radians().cos(); }
154
155 let kt = ghi / (dni_extra * cos_z);
156
157 let kd = if kt <= 0.22 {
158 1.0 - 0.09 * kt
159 } else if kt <= 0.80 {
160 0.9511 - 0.1604 * kt + 4.388 * kt.powi(2) - 16.638 * kt.powi(3) + 12.336 * kt.powi(4)
161 } else {
162 0.165
163 };
164
165 let dhi = ghi * kd.clamp(0.0, 1.0);
166 let dni = (ghi - dhi) / cos_z;
167 if dni < 0.0 { return (0.0, ghi); }
168
169 (dni, dhi)
170}
171
172#[inline]
179pub fn boland(ghi: f64, zenith: f64, dni_extra: f64) -> (f64, f64) {
180 if ghi <= 0.0 || zenith >= 90.0 { return (0.0, 0.0); }
181 let cos_z = zenith.to_radians().cos().max(85.0_f64.to_radians().cos());
182
183 let kt = ghi / (dni_extra * cos_z);
184
185 let a_coeff = 8.645;
188 let b_coeff = 0.613;
189 let kd = 1.0 / (1.0 + (a_coeff * (kt - b_coeff)).exp());
190 let dhi = ghi * kd.clamp(0.0, 1.0);
191 let dni = ((ghi - dhi) / cos_z).max(0.0);
192
193 (dni, dhi)
194}
195
196#[inline]
201pub fn clearness_index_zenith_independent(
202 clearness_index: f64,
203 airmass: f64,
204 max_clearness_index: f64,
205) -> f64 {
206 if !clearness_index.is_finite() || !airmass.is_finite() || airmass <= 0.0 {
207 return 0.0;
208 }
209 let factor = 1.031 * (-1.4 / (0.9 + 9.4 / airmass)).exp() + 0.1;
210 (clearness_index / factor).clamp(0.0, max_clearness_index)
211}
212
213#[inline]
215pub fn precipitable_water_from_dew_point(temp_dew_c: f64) -> f64 {
216 (0.07 * temp_dew_c - 0.075).exp()
217}
218
219#[inline]
241pub fn dirint(
242 ghi: f64,
243 solar_zenith: f64,
244 day_of_year: i32,
245 pressure_pa: f64,
246 temp_dew_c: Option<f64>,
247) -> f64 {
248 let disc_out = disc(ghi, solar_zenith, day_of_year, Some(pressure_pa));
249 let kt_prime =
250 clearness_index_zenith_independent(disc_out.kt, disc_out.airmass, 1.0);
251 let w = temp_dew_c.map_or(-1.0, precipitable_water_from_dew_point);
252 let m = crate::dirint_coeffs::coefficient(kt_prime, solar_zenith, -1.0, w).unwrap_or(f64::NAN);
253 if !m.is_finite() {
254 return 0.0;
255 }
256 (disc_out.dni * m).max(0.0)
257}
258
259pub fn dirint_series(
279 ghi: &[f64],
280 solar_zenith: &[f64],
281 day_of_year: &[i32],
282 pressure_pa: f64,
283 temp_dew_c: Option<&[f64]>,
284 use_delta_kt_prime: bool,
285) -> Vec<f64> {
286 let n = ghi.len();
287 assert_eq!(solar_zenith.len(), n, "dirint_series: solar_zenith length mismatch");
288 assert_eq!(day_of_year.len(), n, "dirint_series: day_of_year length mismatch");
289 if let Some(td) = temp_dew_c {
290 assert_eq!(td.len(), n, "dirint_series: temp_dew_c length mismatch");
291 }
292 if n == 0 {
293 return Vec::new();
294 }
295
296 let mut dni_disc = Vec::with_capacity(n);
298 let mut kt_prime = Vec::with_capacity(n);
299 for i in 0..n {
300 let d = disc(ghi[i], solar_zenith[i], day_of_year[i], Some(pressure_pa));
301 dni_disc.push(d.dni);
302 kt_prime.push(clearness_index_zenith_independent(d.kt, d.airmass, 1.0));
303 }
304
305 let delta_kt_prime: Vec<f64> = if use_delta_kt_prime && n > 1 {
307 let mut out = Vec::with_capacity(n);
308 for i in 0..n {
309 let prev = if i == 0 { kt_prime[1] } else { kt_prime[i - 1] };
312 let next = if i + 1 >= n { kt_prime[n - 2] } else { kt_prime[i + 1] };
313 out.push(0.5 * ((kt_prime[i] - next).abs() + (kt_prime[i] - prev).abs()));
314 }
315 out
316 } else {
317 vec![-1.0; n]
318 };
319
320 let mut dni = Vec::with_capacity(n);
322 for i in 0..n {
323 let w = temp_dew_c
324 .map(|td| td[i])
325 .filter(|t| t.is_finite())
326 .map_or(-1.0, precipitable_water_from_dew_point);
327 let m = crate::dirint_coeffs::coefficient(
328 kt_prime[i],
329 solar_zenith[i],
330 delta_kt_prime[i],
331 w,
332 )
333 .unwrap_or(f64::NAN);
334 dni.push(if m.is_finite() { (dni_disc[i] * m).max(0.0) } else { 0.0 });
335 }
336 dni
337}
338
339#[inline]
341pub fn poa_direct(aoi_in: f64, dni: f64) -> f64 {
342 let aoi_rad = aoi_in.to_radians();
343 if aoi_rad.abs() > std::f64::consts::PI / 2.0 {
344 0.0
345 } else {
346 (dni * aoi_rad.cos()).max(0.0)
347 }
348}
349
350#[allow(clippy::too_many_arguments)]
357#[inline]
358pub fn reindl(surface_tilt: f64, dhi: f64, ghi: f64, dni: f64, dni_extra: f64, solar_zenith: f64, aoi_in: f64) -> f64 {
359 let mut a = 0.0;
360 if dni_extra > 0.0 { a = dni / dni_extra; }
361 let a = a.clamp(0.0, 1.0);
362
363 let cos_z = solar_zenith.to_radians().cos().max(85.0_f64.to_radians().cos());
364 let cos_aoi = aoi_in.to_radians().cos().max(0.0);
365 let r_b = cos_aoi / cos_z;
366
367 let cos_z_reindl = solar_zenith.to_radians().cos().max(0.0);
368 let f = if ghi > 0.0 { ((dni * cos_z_reindl) / ghi).sqrt() } else { 0.0 };
369
370 let tilt_rad = surface_tilt.to_radians();
371 let term1 = dhi * (1.0 - a) * (1.0 + tilt_rad.cos()) / 2.0 * (1.0 + f * (tilt_rad / 2.0).sin().powi(3));
372 let term2 = dhi * a * r_b;
373
374 term1 + term2
375}
376
377#[inline]
381pub fn clearness_index(ghi: f64, solar_zenith: f64, dni_extra: f64) -> f64 {
382 let cos_z = solar_zenith.to_radians().cos().max(0.01);
383 let ghi_extra = dni_extra * cos_z;
384 if ghi_extra <= 0.0 { 0.0 } else { (ghi / ghi_extra).clamp(0.0, 1.0) }
385}
386
387#[inline]
396pub fn aoi_projection(surface_tilt: f64, surface_azimuth: f64, solar_zenith: f64, solar_azimuth: f64) -> f64 {
397 let tilt_rad = surface_tilt.to_radians();
398 let surf_az_rad = surface_azimuth.to_radians();
399 let zen_rad = solar_zenith.to_radians();
400 let sol_az_rad = solar_azimuth.to_radians();
401
402 let projection = zen_rad.cos() * tilt_rad.cos()
403 + zen_rad.sin() * tilt_rad.sin() * (sol_az_rad - surf_az_rad).cos();
404
405 projection.clamp(-1.0, 1.0)
406}
407
408#[inline]
419pub fn beam_component(surface_tilt: f64, surface_azimuth: f64, solar_zenith: f64, solar_azimuth: f64, dni: f64) -> f64 {
420 let proj = aoi_projection(surface_tilt, surface_azimuth, solar_zenith, solar_azimuth);
421 (dni * proj).max(0.0)
422}
423
424#[inline]
438pub fn get_ground_diffuse(surface_tilt: f64, ghi: f64, albedo: f64) -> f64 {
439 ghi * albedo * (1.0 - surface_tilt.to_radians().cos()) * 0.5
440}
441
442#[derive(Debug, Clone, Copy)]
444pub struct PoaComponents {
445 pub poa_global: f64,
447 pub poa_direct: f64,
449 pub poa_diffuse: f64,
451 pub poa_sky_diffuse: f64,
453 pub poa_ground_diffuse: f64,
455}
456
457#[inline]
469pub fn poa_components(aoi_val: f64, dni: f64, poa_sky_diffuse: f64, poa_ground_diffuse: f64) -> PoaComponents {
470 let poa_direct = (dni * aoi_val.to_radians().cos()).max(0.0);
471 let poa_diffuse = poa_sky_diffuse + poa_ground_diffuse;
472 let poa_global = poa_direct + poa_diffuse;
473
474 PoaComponents {
475 poa_global,
476 poa_direct,
477 poa_diffuse,
478 poa_sky_diffuse,
479 poa_ground_diffuse,
480 }
481}
482
483pub type TotalIrradiance = PoaComponents;
485
486#[derive(Debug, Clone, Copy, PartialEq, Eq)]
488pub enum DiffuseModel {
489 Isotropic,
490 Klucher,
491 HayDavies,
492 Reindl,
493 Perez,
494}
495
496#[allow(clippy::too_many_arguments)]
513#[inline]
514pub fn get_sky_diffuse(
515 surface_tilt: f64,
516 surface_azimuth: f64,
517 solar_zenith: f64,
518 solar_azimuth: f64,
519 dni: f64,
520 ghi: f64,
521 dhi: f64,
522 model: DiffuseModel,
523 dni_extra: Option<f64>,
524 airmass: Option<f64>,
525) -> f64 {
526 let aoi_val = aoi(surface_tilt, surface_azimuth, solar_zenith, solar_azimuth);
527
528 match model {
529 DiffuseModel::Isotropic => isotropic(surface_tilt, dhi),
530 DiffuseModel::Klucher => klucher(surface_tilt, surface_azimuth, dhi, ghi, solar_zenith, solar_azimuth, aoi_val),
531 DiffuseModel::HayDavies => {
532 let extra = dni_extra.unwrap_or(0.0);
533 haydavies(surface_tilt, surface_azimuth, dhi, dni, extra, solar_zenith, solar_azimuth, aoi_val)
534 }
535 DiffuseModel::Reindl => {
536 let extra = dni_extra.unwrap_or(0.0);
537 reindl(surface_tilt, dhi, ghi, dni, extra, solar_zenith, aoi_val)
538 }
539 DiffuseModel::Perez => {
540 let extra = dni_extra.unwrap_or(0.0);
541 let am = airmass.unwrap_or_else(|| atmosphere::get_relative_airmass(solar_zenith));
542 perez(surface_tilt, surface_azimuth, dhi, dni, extra, solar_zenith, solar_azimuth, am, aoi_val)
543 }
544 }
545}
546
547#[allow(clippy::too_many_arguments)]
563#[inline]
564pub fn get_total_irradiance(
565 surface_tilt: f64,
566 surface_azimuth: f64,
567 solar_zenith: f64,
568 solar_azimuth: f64,
569 dni: f64,
570 ghi: f64,
571 dhi: f64,
572 albedo: f64,
573 model: DiffuseModel,
574 dni_extra: Option<f64>,
575 airmass: Option<f64>,
576) -> TotalIrradiance {
577 let aoi_val = aoi(surface_tilt, surface_azimuth, solar_zenith, solar_azimuth);
578
579 let sky_diffuse = get_sky_diffuse(
580 surface_tilt, surface_azimuth, solar_zenith, solar_azimuth,
581 dni, ghi, dhi, model, dni_extra, airmass,
582 );
583
584 let ground_diffuse = get_ground_diffuse(surface_tilt, ghi, albedo);
585
586 poa_components(aoi_val, dni, sky_diffuse, ground_diffuse)
587}
588
589#[derive(Debug, Clone, Copy)]
591pub struct DiscOutput {
592 pub dni: f64,
594 pub kt: f64,
596 pub airmass: f64,
598}
599
600fn disc_kn(kt: f64, am: f64) -> (f64, f64) {
602 let am = am.min(12.0);
603
604 let (a, b, c) = if kt <= 0.6 {
605 (
606 0.512 + kt * (-1.56 + kt * (2.286 - 2.222 * kt)),
607 0.37 + 0.962 * kt,
608 -0.28 + kt * (0.932 - 2.048 * kt),
609 )
610 } else {
611 (
612 -5.743 + kt * (21.77 + kt * (-27.49 + 11.56 * kt)),
613 41.4 + kt * (-118.5 + kt * (66.05 + 31.9 * kt)),
614 -47.01 + kt * (184.2 + kt * (-222.0 + 73.81 * kt)),
615 )
616 };
617
618 let delta_kn = a + b * (c * am).exp();
619 let knc = 0.866 + am * (-0.122 + am * (0.0121 + am * (-0.000653 + 1.4e-05 * am)));
620 let kn = knc - delta_kn;
621
622 (kn, am)
623}
624
625#[inline]
642pub fn disc(ghi: f64, solar_zenith: f64, day_of_year: i32, pressure: Option<f64>) -> DiscOutput {
643 let max_zenith = 87.0;
644 let min_cos_zenith = 0.065;
645
646 let b = 2.0 * PI * ((day_of_year - 1) as f64) / 365.0;
648 let rover = 1.00011 + 0.034221 * b.cos() + 0.00128 * b.sin()
649 + 0.000719 * (2.0 * b).cos() + 0.000077 * (2.0 * b).sin();
650 let i0 = 1370.0 * rover;
651
652 let cos_z = solar_zenith.to_radians().cos().max(min_cos_zenith);
654 let ghi_extra = i0 * cos_z;
655 let kt = if ghi_extra > 0.0 { (ghi / ghi_extra).clamp(0.0, 1.0) } else { 0.0 };
656
657 let mut am = {
660 let z = solar_zenith;
661 let cos_z = z.to_radians().cos();
662 let c = 93.885 - z;
663 if c <= 0.0 {
664 f64::NAN
665 } else {
666 1.0 / (cos_z + 0.15 * c.powf(-1.253))
667 }
668 };
669 if let Some(p) = pressure {
670 am = atmosphere::get_absolute_airmass(am, p);
671 }
672
673 if !am.is_finite() {
676 return DiscOutput { dni: 0.0, kt, airmass: f64::NAN };
677 }
678
679 let (kn, am) = disc_kn(kt, am);
680 let mut dni = kn * i0;
681
682 if solar_zenith > max_zenith || ghi < 0.0 || dni < 0.0 || !dni.is_finite() {
683 dni = 0.0;
684 }
685
686 DiscOutput { dni, kt, airmass: am }
687}
688
689#[derive(Debug, Clone, Copy)]
691pub struct ErbsDriesseOutput {
692 pub dni: f64,
694 pub dhi: f64,
696 pub kt: f64,
698}
699
700#[inline]
716pub fn erbs_driesse(ghi: f64, solar_zenith: f64, day_of_year: i32) -> ErbsDriesseOutput {
717 let max_zenith = 87.0;
718 let min_cos_zenith = 0.065;
719
720 let ghi = ghi.max(0.0);
721
722 let dni_extra = get_extra_radiation(day_of_year);
723
724 let cos_z = solar_zenith.to_radians().cos().max(min_cos_zenith);
726 let ghi_extra = dni_extra * cos_z;
727 let kt = if ghi_extra > 0.0 { (ghi / ghi_extra).clamp(0.0, 1.0) } else { 0.0 };
728
729 let p = [12.26911439571261, -16.4705084246973, 4.246926715218317,
731 -0.11390583806313881, 0.946296633571001];
732
733 let df = if kt <= 0.216 {
735 1.0 - 0.09 * kt
736 } else if kt <= 0.792 {
737 p[0] * kt.powi(4) + p[1] * kt.powi(3) + p[2] * kt.powi(2) + p[3] * kt + p[4]
739 } else {
740 0.165
741 };
742
743 let dhi = df * ghi;
744 let mut dni = (ghi - dhi) / solar_zenith.to_radians().cos();
745
746 let bad = solar_zenith > max_zenith || ghi < 0.0 || dni < 0.0;
747 let dhi = if bad { ghi } else { dhi };
748 if bad {
749 dni = 0.0;
750 }
751
752 ErbsDriesseOutput { dni, dhi, kt }
753}
754
755#[inline]
766pub fn king(surface_tilt: f64, dhi: f64, ghi: f64, solar_zenith: f64) -> f64 {
767 let cos_tilt = surface_tilt.to_radians().cos();
768 let sky_diffuse = dhi * (1.0 + cos_tilt) / 2.0
769 + ghi * (0.012 * solar_zenith - 0.04) * (1.0 - cos_tilt) / 2.0;
770 sky_diffuse.max(0.0)
771}
772
773#[inline]
792pub fn dirindex(
793 ghi: f64,
794 ghi_clearsky: f64,
795 dni_clearsky: f64,
796 zenith: f64,
797 day_of_year: i32,
798 pressure: Option<f64>,
799) -> f64 {
800 let p = pressure.unwrap_or(101325.0);
801
802 let dni_dirint = dirint(ghi, zenith, day_of_year, p, None);
803 let dni_dirint_clear = dirint(ghi_clearsky, zenith, day_of_year, p, None);
804
805 if dni_dirint_clear <= 0.0 {
806 return 0.0;
807 }
808
809 let dni = dni_clearsky * dni_dirint / dni_dirint_clear;
810 dni.max(0.0)
811}
812
813const PD_KNOTS: [f64; 13] = [
819 0.000, 0.000, 0.000,
820 0.061, 0.187, 0.333, 0.487, 0.643, 0.778, 0.839,
821 1.000, 1.000, 1.000,
822];
823
824const PD_COEFS: [[[f64; 13]; 3]; 2] = [
828 [
830 [-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],
832 [ 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],
834 [-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],
836 ],
837 [
839 [-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],
841 [ 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],
843 [-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],
845 ],
846];
847
848fn pd_splev(x: f64, coefs: &[f64; 13]) -> f64 {
852 let t = &PD_KNOTS;
853 let k = 2_usize; let n = t.len() - k - 1; let x = x.clamp(t[k], t[n]);
858
859 let mut span = k;
862 for i in k..n {
863 if t[i + 1] > x {
864 span = i;
865 break;
866 }
867 span = i;
868 }
869
870 let mut d = [0.0_f64; 3]; for (j, d_j) in d.iter_mut().enumerate().take(k + 1) {
873 let idx = span - k + j;
874 if idx < 13 {
875 *d_j = coefs[idx];
876 }
877 }
878
879 for r in 1..=k {
881 for j in (r..=k).rev() {
882 let left = span + j - k;
883 let right = span + 1 + j - r;
884 let denom = t[right] - t[left];
885 if denom.abs() < 1e-15 {
886 d[j] = 0.0;
887 } else {
888 let alpha = (x - t[left]) / denom;
889 d[j] = (1.0 - alpha) * d[j - 1] + alpha * d[j];
890 }
891 }
892 }
893
894 d[k]
895}
896
897fn pd_calc_delta(dhi: f64, dni_extra: f64, solar_zenith: f64, airmass: Option<f64>) -> f64 {
899 let am = match airmass {
900 Some(a) => {
901 if solar_zenith >= 90.0 {
902 atmosphere::get_relative_airmass(89.999)
904 } else {
905 a
906 }
907 }
908 None => {
909 if solar_zenith >= 90.0 {
910 atmosphere::get_relative_airmass(89.999)
911 } else {
912 atmosphere::get_relative_airmass(solar_zenith)
913 }
914 }
915 };
916
917 let am = if am.is_nan() { atmosphere::get_relative_airmass(89.999) } else { am };
918
919 if dni_extra <= 0.0 || am <= 0.0 {
920 return 0.0;
921 }
922
923 dhi / (dni_extra / am)
924}
925
926fn pd_calc_zeta(dhi: f64, dni: f64, zenith: f64) -> f64 {
928 if dhi <= 0.0 && dni <= 0.0 {
929 return 0.0;
930 }
931
932 let sum = dhi + dni;
933 let mut zeta = if sum > 0.0 { dni / sum } else { 0.0 };
934
935 if dhi == 0.0 {
936 zeta = 0.0;
937 }
938
939 let kappa = 1.041;
941 let kterm = kappa * zenith.to_radians().powi(3);
942 let denom = 1.0 - kterm * (zeta - 1.0);
943 if denom.abs() > 1e-15 {
944 zeta /= denom;
945 }
946
947 zeta
948}
949
950fn pd_f(i: usize, j: usize, zeta: f64) -> f64 {
952 pd_splev(zeta, &PD_COEFS[i - 1][j - 1])
953}
954
955#[allow(clippy::too_many_arguments)]
977#[inline]
978pub fn perez_driesse(
979 surface_tilt: f64,
980 surface_azimuth: f64,
981 dhi: f64,
982 dni: f64,
983 dni_extra: f64,
984 solar_zenith: f64,
985 solar_azimuth: f64,
986 airmass: Option<f64>,
987) -> f64 {
988 let delta = pd_calc_delta(dhi, dni_extra, solar_zenith, airmass);
989 let zeta = pd_calc_zeta(dhi, dni, solar_zenith);
990
991 let z = solar_zenith.to_radians();
992
993 let f1 = pd_f(1, 1, zeta) + pd_f(1, 2, zeta) * delta + pd_f(1, 3, zeta) * z;
994 let f2 = pd_f(2, 1, zeta) + pd_f(2, 2, zeta) * delta + pd_f(2, 3, zeta) * z;
995
996 let f1 = f1.clamp(0.0, 0.9);
998
999 let a = aoi_projection(surface_tilt, surface_azimuth, solar_zenith, solar_azimuth).max(0.0);
1001
1002 let b = solar_zenith.to_radians().cos().max(85.0_f64.to_radians().cos());
1004
1005 let term1 = 0.5 * (1.0 - f1) * (1.0 + surface_tilt.to_radians().cos());
1006 let term2 = f1 * a / b;
1007 let term3 = f2 * surface_tilt.to_radians().sin();
1008
1009 (dhi * (term1 + term2 + term3)).max(0.0)
1010}
1011