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
18pub fn aoi(surface_tilt: f64, surface_azimuth: f64, solar_zenith: f64, solar_azimuth: f64) -> f64 {
20 let tilt_rad = surface_tilt.to_radians();
21 let surf_az_rad = surface_azimuth.to_radians();
22 let zen_rad = solar_zenith.to_radians();
23 let sol_az_rad = solar_azimuth.to_radians();
24
25 let cos_aoi = zen_rad.cos() * tilt_rad.cos()
26 + zen_rad.sin() * tilt_rad.sin() * (sol_az_rad - surf_az_rad).cos();
27
28 let cos_aoi = cos_aoi.clamp(-1.0, 1.0);
29 cos_aoi.acos().to_degrees()
30}
31
32pub fn get_extra_radiation(dayofyear: i32) -> f64 {
34 let b = 2.0 * PI * ((dayofyear - 1) as f64) / 365.0;
35 let rover_r0_sqrd = 1.00011
36 + 0.034221 * b.cos()
37 + 0.00128 * b.sin()
38 + 0.000719 * (2.0 * b).cos()
39 + 0.000077 * (2.0 * b).sin();
40 1366.1 * rover_r0_sqrd
41}
42
43pub fn isotropic(surface_tilt: f64, dhi: f64) -> f64 {
45 dhi * (1.0 + surface_tilt.to_radians().cos()) / 2.0
46}
47
48pub 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 {
54 let mut a = 0.0;
55 if dni_extra > 0.0 {
56 a = dni / dni_extra;
57 }
58 let a = a.clamp(0.0, 1.0);
59 let mut cos_z = solar_zenith.to_radians().cos();
60 if cos_z < 0.0436 { cos_z = 0.0436; }
61
62 let cos_aoi = aoi_in.to_radians().cos().max(0.0);
63 let r_b = cos_aoi / cos_z;
64
65 dhi * ((1.0 - a) * (1.0 + surface_tilt.to_radians().cos()) / 2.0 + a * r_b)
66}
67
68pub fn klucher(surface_tilt: f64, _surface_azimuth: f64, dhi: f64, ghi: f64, solar_zenith: f64, _solar_azimuth: f64, aoi_in: f64) -> f64 {
74 let mut f = 0.0;
75 if ghi > 0.0 {
76 let frac = dhi / ghi;
77 f = 1.0 - frac * frac;
78 }
79 let f = f.clamp(0.0, 1.0);
80
81 let _cos_z = solar_zenith.to_radians().cos();
82 let cos_aoi = aoi_in.to_radians().cos().max(0.0);
83 let tilt_rad = surface_tilt.to_radians();
84
85 let term1 = 1.0 + f * (tilt_rad / 2.0).sin().powi(3);
86 let term2 = 1.0 + f * cos_aoi.powi(2) * (solar_zenith.to_radians().sin()).powi(3);
87
88 dhi * ((1.0 + tilt_rad.cos()) / 2.0) * term1 * term2
89}
90
91pub 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 {
98 let mut cos_z = solar_zenith.to_radians().cos();
99 if cos_z < 0.0436 { cos_z = 0.0436; } 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;
105
106 let mut epsilon = 1.0;
107 if dhi > 0.0 {
108 epsilon = ((dhi + dni) / dhi + 1.041 * solar_zenith.to_radians().powi(3)) /
109 (1.0 + 1.041 * solar_zenith.to_radians().powi(3));
110 }
111
112 let bin = if epsilon < 1.065 { 0 }
113 else if epsilon < 1.230 { 1 }
114 else if epsilon < 1.500 { 2 }
115 else if epsilon < 1.950 { 3 }
116 else if epsilon < 2.800 { 4 }
117 else if epsilon < 4.500 { 5 }
118 else if epsilon < 6.200 { 6 }
119 else { 7 };
120
121 let coeffs = PEREZ_COEFFICIENTS[bin];
122 let mut f1 = coeffs[0] + coeffs[1] * delta + coeffs[2] * solar_zenith.to_radians();
123 f1 = f1.max(0.0);
124 let f2 = coeffs[3] + coeffs[4] * delta + coeffs[5] * solar_zenith.to_radians();
125
126 let a_perez = cos_aoi;
127 let b_perez = cos_z;
128
129 dhi * ((1.0 - f1) * (1.0 + surface_tilt.to_radians().cos()) / 2.0 + f1 * a_perez / b_perez + f2 * surface_tilt.to_radians().sin())
130}
131
132pub fn erbs(ghi: f64, zenith: f64, _day_of_year: u32, dni_extra: f64) -> (f64, f64) {
139 if ghi <= 0.0 || zenith >= 90.0 { return (0.0, 0.0); }
140 let mut cos_z = zenith.to_radians().cos();
141 if cos_z < 0.0436 { cos_z = 0.0436; }
142
143 let kt = ghi / (dni_extra * cos_z);
144
145 let kd = if kt <= 0.22 {
146 1.0 - 0.09 * kt
147 } else if kt <= 0.80 {
148 0.9511 - 0.1604 * kt + 4.388 * kt.powi(2) - 16.638 * kt.powi(3) + 12.336 * kt.powi(4)
149 } else {
150 0.165
151 };
152
153 let dhi = ghi * kd.clamp(0.0, 1.0);
154 let dni = ((ghi - dhi) / cos_z).max(0.0);
155
156 (dni, dhi)
157}
158
159pub fn boland(ghi: f64, zenith: f64, dni_extra: f64) -> (f64, f64) {
166 if ghi <= 0.0 || zenith >= 90.0 { return (0.0, 0.0); }
167 let cos_z = zenith.to_radians().cos().max(0.0436);
168
169 let kt = ghi / (dni_extra * cos_z);
170
171 let kd = 1.0 / (1.0 + (-5.0033 + 8.6025 * kt).exp());
173 let dhi = ghi * kd.clamp(0.0, 1.0);
174 let dni = ((ghi - dhi) / cos_z).max(0.0);
175
176 (dni, dhi)
177}
178
179pub fn dirint(ghi: f64, zenith: f64, _dew_point: f64, _pressure: f64, dni_extra: f64) -> (f64, f64) {
188 if ghi <= 0.0 || zenith >= 90.0 { return (0.0, 0.0); }
191 let cos_z = zenith.to_radians().cos().max(0.0436);
192
193 let kt = ghi / (dni_extra * cos_z);
194
195 let kd = if kt <= 0.2 {
197 0.99
198 } else if kt <= 0.8 {
199 0.95 - 0.9 * (kt - 0.2)
200 } else {
201 0.15
202 };
203
204 let dhi = ghi * kd.clamp(0.0, 1.0);
205 let dni = ((ghi - dhi) / cos_z).max(0.0);
206 (dni, dhi)
207}
208
209pub fn poa_direct(aoi_in: f64, dni: f64) -> f64 {
211 let aoi_rad = aoi_in.to_radians();
212 if aoi_rad > std::f64::consts::PI / 2.0 {
213 0.0
214 } else {
215 dni * aoi_rad.cos()
216 }
217}
218
219#[allow(clippy::too_many_arguments)]
226pub fn reindl(surface_tilt: f64, dhi: f64, ghi: f64, dni: f64, dni_extra: f64, solar_zenith: f64, aoi_in: f64) -> f64 {
227 let mut a = 0.0;
228 if dni_extra > 0.0 { a = dni / dni_extra; }
229 let a = a.clamp(0.0, 1.0);
230
231 let cos_z = solar_zenith.to_radians().cos().max(0.0436);
232 let cos_aoi = aoi_in.to_radians().cos().max(0.0);
233 let r_b = cos_aoi / cos_z;
234
235 let f = if ghi > 0.0 { (dni / ghi).powi(2) } else { 0.0 };
236
237 let tilt_rad = surface_tilt.to_radians();
238 let term1 = dhi * (1.0 - a) * (1.0 + tilt_rad.cos()) / 2.0 * (1.0 + f * (tilt_rad / 2.0).sin().powi(3));
239 let term2 = dhi * a * r_b;
240
241 term1 + term2
242}
243
244pub fn clearness_index(ghi: f64, solar_zenith: f64, dni_extra: f64) -> f64 {
248 let cos_z = solar_zenith.to_radians().cos().max(0.01);
249 let ghi_extra = dni_extra * cos_z;
250 if ghi_extra <= 0.0 { 0.0 } else { (ghi / ghi_extra).clamp(0.0, 1.0) }
251}
252
253pub fn clearness_index_zenith_independent(clearness_idx: f64, _solar_zenith: f64, airmass_absolute: f64) -> f64 {
258 let am = airmass_absolute.max(1.0);
259 let denominator = 1.031 * (-1.4 / (0.9 + 9.4 / am)).exp() + 0.1;
261 (clearness_idx / denominator).max(0.0)
262}
263
264pub fn aoi_projection(surface_tilt: f64, surface_azimuth: f64, solar_zenith: f64, solar_azimuth: f64) -> f64 {
273 let tilt_rad = surface_tilt.to_radians();
274 let surf_az_rad = surface_azimuth.to_radians();
275 let zen_rad = solar_zenith.to_radians();
276 let sol_az_rad = solar_azimuth.to_radians();
277
278 let projection = zen_rad.cos() * tilt_rad.cos()
279 + zen_rad.sin() * tilt_rad.sin() * (sol_az_rad - surf_az_rad).cos();
280
281 projection.clamp(-1.0, 1.0)
282}
283
284pub fn beam_component(surface_tilt: f64, surface_azimuth: f64, solar_zenith: f64, solar_azimuth: f64, dni: f64) -> f64 {
295 let proj = aoi_projection(surface_tilt, surface_azimuth, solar_zenith, solar_azimuth);
296 (dni * proj).max(0.0)
297}
298
299pub fn get_ground_diffuse(surface_tilt: f64, ghi: f64, albedo: f64) -> f64 {
313 ghi * albedo * (1.0 - surface_tilt.to_radians().cos()) * 0.5
314}
315
316#[derive(Debug, Clone, Copy)]
318pub struct PoaComponents {
319 pub poa_global: f64,
321 pub poa_direct: f64,
323 pub poa_diffuse: f64,
325 pub poa_sky_diffuse: f64,
327 pub poa_ground_diffuse: f64,
329}
330
331pub fn poa_components(aoi_val: f64, dni: f64, poa_sky_diffuse: f64, poa_ground_diffuse: f64) -> PoaComponents {
343 let poa_direct = (dni * aoi_val.to_radians().cos()).max(0.0);
344 let poa_diffuse = poa_sky_diffuse + poa_ground_diffuse;
345 let poa_global = poa_direct + poa_diffuse;
346
347 PoaComponents {
348 poa_global,
349 poa_direct,
350 poa_diffuse,
351 poa_sky_diffuse,
352 poa_ground_diffuse,
353 }
354}
355
356pub type TotalIrradiance = PoaComponents;
358
359#[derive(Debug, Clone, Copy, PartialEq, Eq)]
361pub enum DiffuseModel {
362 Isotropic,
363 Klucher,
364 HayDavies,
365 Reindl,
366 Perez,
367}
368
369#[allow(clippy::too_many_arguments)]
386pub fn get_sky_diffuse(
387 surface_tilt: f64,
388 surface_azimuth: f64,
389 solar_zenith: f64,
390 solar_azimuth: f64,
391 dni: f64,
392 ghi: f64,
393 dhi: f64,
394 model: DiffuseModel,
395 dni_extra: Option<f64>,
396 airmass: Option<f64>,
397) -> f64 {
398 let aoi_val = aoi(surface_tilt, surface_azimuth, solar_zenith, solar_azimuth);
399
400 match model {
401 DiffuseModel::Isotropic => isotropic(surface_tilt, dhi),
402 DiffuseModel::Klucher => klucher(surface_tilt, surface_azimuth, dhi, ghi, solar_zenith, solar_azimuth, aoi_val),
403 DiffuseModel::HayDavies => {
404 let extra = dni_extra.unwrap_or(0.0);
405 haydavies(surface_tilt, surface_azimuth, dhi, dni, extra, solar_zenith, solar_azimuth, aoi_val)
406 }
407 DiffuseModel::Reindl => {
408 let extra = dni_extra.unwrap_or(0.0);
409 reindl(surface_tilt, dhi, ghi, dni, extra, solar_zenith, aoi_val)
410 }
411 DiffuseModel::Perez => {
412 let extra = dni_extra.unwrap_or(0.0);
413 let am = airmass.unwrap_or_else(|| atmosphere::get_relative_airmass(solar_zenith));
414 perez(surface_tilt, surface_azimuth, dhi, dni, extra, solar_zenith, solar_azimuth, am, aoi_val)
415 }
416 }
417}
418
419#[allow(clippy::too_many_arguments)]
435pub fn get_total_irradiance(
436 surface_tilt: f64,
437 surface_azimuth: f64,
438 solar_zenith: f64,
439 solar_azimuth: f64,
440 dni: f64,
441 ghi: f64,
442 dhi: f64,
443 albedo: f64,
444 model: DiffuseModel,
445 dni_extra: Option<f64>,
446 airmass: Option<f64>,
447) -> TotalIrradiance {
448 let aoi_val = aoi(surface_tilt, surface_azimuth, solar_zenith, solar_azimuth);
449
450 let sky_diffuse = get_sky_diffuse(
451 surface_tilt, surface_azimuth, solar_zenith, solar_azimuth,
452 dni, ghi, dhi, model, dni_extra, airmass,
453 );
454
455 let ground_diffuse = get_ground_diffuse(surface_tilt, ghi, albedo);
456
457 poa_components(aoi_val, dni, sky_diffuse, ground_diffuse)
458}
459
460#[derive(Debug, Clone, Copy)]
462pub struct DiscOutput {
463 pub dni: f64,
465 pub kt: f64,
467 pub airmass: f64,
469}
470
471fn disc_kn(kt: f64, am: f64) -> (f64, f64) {
473 let am = am.min(12.0);
474
475 let (a, b, c) = if kt <= 0.6 {
476 (
477 0.512 + kt * (-1.56 + kt * (2.286 - 2.222 * kt)),
478 0.37 + 0.962 * kt,
479 -0.28 + kt * (0.932 - 2.048 * kt),
480 )
481 } else {
482 (
483 -5.743 + kt * (21.77 + kt * (-27.49 + 11.56 * kt)),
484 41.4 + kt * (-118.5 + kt * (66.05 + 31.9 * kt)),
485 -47.01 + kt * (184.2 + kt * (-222.0 + 73.81 * kt)),
486 )
487 };
488
489 let delta_kn = a + b * (c * am).exp();
490 let knc = 0.866 + am * (-0.122 + am * (0.0121 + am * (-0.000653 + 1.4e-05 * am)));
491 let kn = knc - delta_kn;
492
493 (kn, am)
494}
495
496pub fn disc(ghi: f64, solar_zenith: f64, day_of_year: i32, pressure: Option<f64>) -> DiscOutput {
513 let max_zenith = 87.0;
514 let min_cos_zenith = 0.065;
515
516 let b = 2.0 * PI * ((day_of_year - 3) as f64) / 365.0;
518 let i0 = 1370.0 * (1.0 + 0.033412 * b.cos());
519
520 let cos_z = solar_zenith.to_radians().cos().max(min_cos_zenith);
522 let ghi_extra = i0 * cos_z;
523 let kt = if ghi_extra > 0.0 { (ghi / ghi_extra).clamp(0.0, 1.0) } else { 0.0 };
524
525 let mut am = {
528 let z = solar_zenith;
529 let cos_z = z.to_radians().cos();
530 let c = 93.885 - z;
531 if c <= 0.0 {
532 f64::NAN
533 } else {
534 1.0 / (cos_z + 0.15 * c.powf(-1.253))
535 }
536 };
537 if let Some(p) = pressure {
538 am = atmosphere::get_absolute_airmass(am, p);
539 }
540
541 let (kn, am) = disc_kn(kt, am);
542 let mut dni = kn * i0;
543
544 if solar_zenith > max_zenith || ghi < 0.0 || dni < 0.0 {
545 dni = 0.0;
546 }
547
548 DiscOutput { dni, kt, airmass: am }
549}
550
551#[derive(Debug, Clone, Copy)]
553pub struct ErbsDriesseOutput {
554 pub dni: f64,
556 pub dhi: f64,
558 pub kt: f64,
560}
561
562pub fn erbs_driesse(ghi: f64, solar_zenith: f64, day_of_year: i32) -> ErbsDriesseOutput {
578 let max_zenith = 87.0;
579 let min_cos_zenith = 0.065;
580
581 let ghi = ghi.max(0.0);
582
583 let dni_extra = get_extra_radiation(day_of_year);
584
585 let cos_z = solar_zenith.to_radians().cos().max(min_cos_zenith);
587 let ghi_extra = dni_extra * cos_z;
588 let kt = if ghi_extra > 0.0 { (ghi / ghi_extra).clamp(0.0, 1.0) } else { 0.0 };
589
590 let p = [12.26911439571261, -16.4705084246973, 4.24692671521831700,
592 -0.11390583806313881, 0.946296633571001];
593
594 let df = if kt <= 0.216 {
596 1.0 - 0.09 * kt
597 } else if kt <= 0.792 {
598 p[0] * kt.powi(4) + p[1] * kt.powi(3) + p[2] * kt.powi(2) + p[3] * kt + p[4]
600 } else {
601 0.165
602 };
603
604 let dhi = df * ghi;
605 let mut dni = (ghi - dhi) / solar_zenith.to_radians().cos();
606
607 let bad = solar_zenith > max_zenith || ghi < 0.0 || dni < 0.0;
608 let dhi = if bad { ghi } else { dhi };
609 if bad {
610 dni = 0.0;
611 }
612
613 ErbsDriesseOutput { dni, dhi, kt }
614}
615
616pub fn king(surface_tilt: f64, dhi: f64, ghi: f64, solar_zenith: f64) -> f64 {
627 let cos_tilt = surface_tilt.to_radians().cos();
628 let sky_diffuse = dhi * (1.0 + cos_tilt) / 2.0
629 + ghi * (0.012 * solar_zenith - 0.04) * (1.0 - cos_tilt) / 2.0;
630 sky_diffuse.max(0.0)
631}
632
633pub fn dirindex(
652 ghi: f64,
653 ghi_clearsky: f64,
654 dni_clearsky: f64,
655 zenith: f64,
656 day_of_year: i32,
657 pressure: Option<f64>,
658) -> f64 {
659 let dni_extra = get_extra_radiation(day_of_year);
660 let p = pressure.unwrap_or(101325.0);
661
662 let (dni_dirint, _) = dirint(ghi, zenith, 0.0, p, dni_extra);
663 let (dni_dirint_clear, _) = dirint(ghi_clearsky, zenith, 0.0, p, dni_extra);
664
665 if dni_dirint_clear <= 0.0 {
666 return 0.0;
667 }
668
669 let dni = dni_clearsky * dni_dirint / dni_dirint_clear;
670 dni.max(0.0)
671}
672
673const PD_KNOTS: [f64; 13] = [
679 0.000, 0.000, 0.000,
680 0.061, 0.187, 0.333, 0.487, 0.643, 0.778, 0.839,
681 1.000, 1.000, 1.000,
682];
683
684const PD_COEFS: [[[f64; 13]; 3]; 2] = [
688 [
690 [-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],
692 [ 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],
694 [-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],
696 ],
697 [
699 [-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],
701 [ 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],
703 [-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],
705 ],
706];
707
708fn pd_splev(x: f64, coefs: &[f64; 13]) -> f64 {
712 let t = &PD_KNOTS;
713 let k = 2_usize; let n = t.len() - k - 1; let x = x.clamp(t[k], t[n]);
718
719 let mut span = k;
722 for i in k..n {
723 if t[i + 1] > x {
724 span = i;
725 break;
726 }
727 span = i;
728 }
729
730 let mut d = [0.0_f64; 3]; for j in 0..=k {
733 let idx = span - k + j;
734 if idx < 13 {
735 d[j] = coefs[idx];
736 }
737 }
738
739 for r in 1..=k {
741 for j in (r..=k).rev() {
742 let left = span + j - k;
743 let right = span + 1 + j - r;
744 let denom = t[right] - t[left];
745 if denom.abs() < 1e-15 {
746 d[j] = 0.0;
747 } else {
748 let alpha = (x - t[left]) / denom;
749 d[j] = (1.0 - alpha) * d[j - 1] + alpha * d[j];
750 }
751 }
752 }
753
754 d[k]
755}
756
757fn pd_calc_delta(dhi: f64, dni_extra: f64, solar_zenith: f64, airmass: Option<f64>) -> f64 {
759 let am = match airmass {
760 Some(a) => {
761 if solar_zenith >= 90.0 {
762 atmosphere::get_relative_airmass(89.999)
764 } else {
765 a
766 }
767 }
768 None => {
769 if solar_zenith >= 90.0 {
770 atmosphere::get_relative_airmass(89.999)
771 } else {
772 atmosphere::get_relative_airmass(solar_zenith)
773 }
774 }
775 };
776
777 let am = if am.is_nan() { atmosphere::get_relative_airmass(89.999) } else { am };
778
779 if dni_extra <= 0.0 || am <= 0.0 {
780 return 0.0;
781 }
782
783 dhi / (dni_extra / am)
784}
785
786fn pd_calc_zeta(dhi: f64, dni: f64, zenith: f64) -> f64 {
788 if dhi <= 0.0 && dni <= 0.0 {
789 return 0.0;
790 }
791
792 let sum = dhi + dni;
793 let mut zeta = if sum > 0.0 { dni / sum } else { 0.0 };
794
795 if dhi == 0.0 {
796 zeta = 0.0;
797 }
798
799 let kappa = 1.041;
801 let kterm = kappa * zenith.to_radians().powi(3);
802 let denom = 1.0 - kterm * (zeta - 1.0);
803 if denom.abs() > 1e-15 {
804 zeta /= denom;
805 }
806
807 zeta
808}
809
810fn pd_f(i: usize, j: usize, zeta: f64) -> f64 {
812 pd_splev(zeta, &PD_COEFS[i - 1][j - 1])
813}
814
815#[allow(clippy::too_many_arguments)]
837pub fn perez_driesse(
838 surface_tilt: f64,
839 surface_azimuth: f64,
840 dhi: f64,
841 dni: f64,
842 dni_extra: f64,
843 solar_zenith: f64,
844 solar_azimuth: f64,
845 airmass: Option<f64>,
846) -> f64 {
847 let delta = pd_calc_delta(dhi, dni_extra, solar_zenith, airmass);
848 let zeta = pd_calc_zeta(dhi, dni, solar_zenith);
849
850 let z = solar_zenith.to_radians();
851
852 let f1 = pd_f(1, 1, zeta) + pd_f(1, 2, zeta) * delta + pd_f(1, 3, zeta) * z;
853 let f2 = pd_f(2, 1, zeta) + pd_f(2, 2, zeta) * delta + pd_f(2, 3, zeta) * z;
854
855 let f1 = f1.clamp(0.0, 0.9);
857
858 let a = aoi_projection(surface_tilt, surface_azimuth, solar_zenith, solar_azimuth).max(0.0);
860
861 let b = solar_zenith.to_radians().cos().max(85.0_f64.to_radians().cos());
863
864 let term1 = 0.5 * (1.0 - f1) * (1.0 + surface_tilt.to_radians().cos());
865 let term2 = f1 * a / b;
866 let term3 = f2 * surface_tilt.to_radians().sin();
867
868 (dhi * (term1 + term2 + term3)).max(0.0)
869}
870