1#[derive(Debug, Clone, PartialEq)]
3pub struct ClearSkyIrradiance {
4 pub ghi: f64,
5 pub dni: f64,
6 pub dhi: f64,
7}
8
9#[derive(Debug, Clone, PartialEq)]
11pub struct BirdResult {
12 pub ghi: f64,
13 pub dni: f64,
14 pub dhi: f64,
15 pub direct_horizontal: f64,
16}
17
18#[inline]
28pub fn haurwitz(zenith: f64) -> f64 {
29 if zenith >= 90.0 {
30 return 0.0;
31 }
32 let cos_z = zenith.to_radians().cos();
33 if cos_z <= 0.0 {
34 return 0.0;
35 }
36
37 1098.0 * cos_z * (-0.059 / cos_z).exp()
38}
39
40#[inline]
55pub fn ineichen(zenith: f64, airmass_absolute: f64, linke_turbidity: f64, altitude: f64, dni_extra: f64) -> ClearSkyIrradiance {
56 if zenith >= 90.0 || airmass_absolute <= 0.0 {
57 return ClearSkyIrradiance { ghi: 0.0, dni: 0.0, dhi: 0.0 };
58 }
59
60 let am = airmass_absolute;
61 let tl = linke_turbidity;
62 let h = altitude;
63
64 let fh1 = (-h / 8000.0).exp();
65 let fh2 = (-h / 1250.0).exp();
66
67 let cos_z = zenith.to_radians().cos().max(0.01);
68
69 let i0 = dni_extra;
70
71 let cg1 = 5.09e-05 * h + 0.868;
72 let cg2 = 3.92e-05 * h + 0.0387;
73
74 let ghi = cg1 * i0 * cos_z * (-cg2 * am * (fh1 + fh2 * (tl - 1.0))).exp().max(0.0);
76
77 let b = 0.664 + 0.163 / fh1;
79 let bnci = i0 * (b * (-0.09 * am * (tl - 1.0)).exp()).max(0.0);
80 let denom = cos_z;
81 let ratio = if denom > 0.0 {
82 ((1.0 - (0.1 - 0.2 * (-tl).exp()) / (0.1 + 0.882 / fh1)) / denom).clamp(0.0, 1e20)
83 } else {
84 0.0
85 };
86 let bnci_2 = ghi * ratio;
87 let dni = bnci.min(bnci_2);
88
89 let dhi = ghi - dni * cos_z;
91
92 ClearSkyIrradiance { ghi: ghi.max(0.0), dni: dni.max(0.0), dhi: dhi.max(0.0) }
93}
94
95#[inline]
118pub fn simplified_solis(apparent_elevation: f64, aod700: f64, precipitable_water: f64, pressure: f64) -> ClearSkyIrradiance {
119 if apparent_elevation <= 0.0 {
120 return ClearSkyIrradiance { ghi: 0.0, dni: 0.0, dhi: 0.0 };
121 }
122
123 let dni_extra = 1364.0;
124 let p = pressure;
125 let p0 = 101325.0_f64;
126 let w = precipitable_water.max(0.2);
127 let aod = aod700;
128 let ln_w = w.ln();
129 let ln_p = (p / p0).ln();
130
131 let io0 = 1.08 * w.powf(0.0051);
133 let i01 = 0.97 * w.powf(0.032);
134 let i02 = 0.12 * w.powf(0.56);
135 let i0p = dni_extra * (i02 * aod * aod + i01 * aod + io0 + 0.071 * ln_p);
136
137 let tb1 = 1.82 + 0.056 * ln_w + 0.0071 * ln_w * ln_w;
139 let tb0 = 0.33 + 0.045 * ln_w + 0.0096 * ln_w * ln_w;
140 let tbp = 0.0089 * w + 0.13;
141 let taub = tb1 * aod + tb0 + tbp * ln_p;
142
143 let b1 = 0.00925 * aod * aod + 0.0148 * aod - 0.0172;
144 let b0 = -0.7565 * aod * aod + 0.5057 * aod + 0.4557;
145 let b = b1 * ln_w + b0;
146
147 let tg1 = 1.24 + 0.047 * ln_w + 0.0061 * ln_w * ln_w;
149 let tg0 = 0.27 + 0.043 * ln_w + 0.0090 * ln_w * ln_w;
150 let tgp = 0.0079 * w + 0.1;
151 let taug = tg1 * aod + tg0 + tgp * ln_p;
152
153 let g = -0.0147 * ln_w - 0.3079 * aod * aod + 0.2846 * aod + 0.3798;
154
155 let (td4, td3, td2, td1, td0, tdp) = if aod < 0.05 {
157 (
158 86.0 * w - 13800.0,
159 -3.11 * w + 79.4,
160 -0.23 * w + 74.8,
161 0.092 * w - 8.86,
162 0.0042 * w + 3.12,
163 -0.83 * (1.0 + aod).powf(-17.2),
164 )
165 } else {
166 (
167 -0.21 * w + 11.6,
168 0.27 * w - 20.7,
169 -0.134 * w + 15.5,
170 0.0554 * w - 5.71,
171 0.0057 * w + 2.94,
172 -0.71 * (1.0 + aod).powf(-15.0),
173 )
174 };
175 let taud = td4 * aod.powi(4) + td3 * aod.powi(3) + td2 * aod.powi(2) + td1 * aod + td0 + tdp * ln_p;
176
177 let dp = 1.0 / (18.0 + 152.0 * aod);
178 let d = -0.337 * aod * aod + 0.63 * aod + 0.116 + dp * ln_p;
179
180 let sin_elev = apparent_elevation.to_radians().sin().max(1e-30);
182
183 let dni = i0p * (-taub / sin_elev.powf(b)).exp();
184 let ghi = i0p * (-taug / sin_elev.powf(g)).exp() * sin_elev;
185 let dhi = i0p * (-taud / sin_elev.powf(d)).exp();
186
187 ClearSkyIrradiance {
188 ghi: ghi.max(0.0),
189 dni: dni.max(0.0),
190 dhi: dhi.max(0.0),
191 }
192}
193
194#[derive(Debug, Clone, Copy, PartialEq)]
205pub struct ClearSkyThresholds {
206 pub window_length_minutes: f64,
208 pub mean_diff: f64,
210 pub max_diff: f64,
212 pub lower_line_length: f64,
214 pub upper_line_length: f64,
216 pub var_diff: f64,
218 pub slope_dev: f64,
220 pub max_iterations: usize,
222}
223
224impl Default for ClearSkyThresholds {
225 fn default() -> Self {
226 Self {
228 window_length_minutes: 10.0,
229 mean_diff: 75.0,
230 max_diff: 75.0,
231 lower_line_length: -5.0,
232 upper_line_length: 10.0,
233 var_diff: 0.005,
234 slope_dev: 8.0,
235 max_iterations: 20,
236 }
237 }
238}
239
240impl ClearSkyThresholds {
241 pub fn from_sample_interval(sample_interval_minutes: f64) -> Self {
244 let si = sample_interval_minutes.clamp(1.0, 30.0);
245 let interp = |xs: &[f64], ys: &[f64]| -> f64 {
246 debug_assert_eq!(xs.len(), ys.len());
248 if si <= xs[0] {
249 return ys[0];
250 }
251 for i in 0..xs.len() - 1 {
252 if si <= xs[i + 1] {
253 let t = (si - xs[i]) / (xs[i + 1] - xs[i]);
254 return ys[i] + t * (ys[i + 1] - ys[i]);
255 }
256 }
257 *ys.last().unwrap()
258 };
259 let breakpoints = [1.0, 5.0, 15.0, 30.0];
260 Self {
261 window_length_minutes: interp(&breakpoints, &[50.0, 60.0, 90.0, 120.0]),
262 mean_diff: 75.0,
263 max_diff: interp(&breakpoints, &[60.0, 65.0, 75.0, 90.0]),
264 lower_line_length: -45.0,
265 upper_line_length: 80.0,
266 var_diff: interp(&breakpoints, &[0.005, 0.01, 0.032, 0.07]),
267 slope_dev: interp(&breakpoints, &[50.0, 60.0, 75.0, 96.0]),
268 max_iterations: 20,
269 }
270 }
271}
272
273#[derive(Debug, Clone)]
276pub struct ClearSkyDetectionResult {
277 pub clear_samples: Vec<bool>,
278 pub alpha: f64,
279 pub iterations: usize,
280}
281
282pub fn detect_clearsky(
315 measured: &[f64],
316 clearsky: &[f64],
317 sample_interval_minutes: f64,
318 thresholds: ClearSkyThresholds,
319) -> Vec<bool> {
320 detect_clearsky_detail(measured, clearsky, sample_interval_minutes, thresholds).clear_samples
321}
322
323pub fn detect_clearsky_detail(
327 measured: &[f64],
328 clearsky: &[f64],
329 sample_interval_minutes: f64,
330 thresholds: ClearSkyThresholds,
331) -> ClearSkyDetectionResult {
332 let n = measured.len();
333 assert_eq!(clearsky.len(), n, "detect_clearsky: length mismatch");
334
335 let samples_per_window =
336 (thresholds.window_length_minutes / sample_interval_minutes).round() as usize;
337 assert!(
338 samples_per_window >= 3,
339 "detect_clearsky: samples_per_window ({samples_per_window}) must be ≥ 3; \
340 increase window_length_minutes or reduce sample_interval_minutes"
341 );
342
343 if n < samples_per_window {
344 return ClearSkyDetectionResult {
345 clear_samples: vec![false; n],
346 alpha: 1.0,
347 iterations: 0,
348 };
349 }
350
351 let num_windows = n + 1 - samples_per_window;
352
353 let meas_mean = window_mean(measured, samples_per_window, num_windows);
356 let meas_max = window_max(measured, samples_per_window, num_windows);
357 let meas_slope_nstd = window_slope_nstd(
358 measured,
359 sample_interval_minutes,
360 samples_per_window,
361 num_windows,
362 &meas_mean,
363 );
364 let meas_line_length = window_line_length(
365 measured,
366 sample_interval_minutes,
367 samples_per_window,
368 num_windows,
369 );
370
371 let clear_mean = window_mean(clearsky, samples_per_window, num_windows);
373 let clear_max = window_max(clearsky, samples_per_window, num_windows);
374
375 let mut alpha: f64 = 1.0;
376 let mut clear_windows_flags = vec![false; num_windows];
377 let mut iterations = 0;
378
379 for it in 0..thresholds.max_iterations {
380 iterations = it + 1;
381
382 let scaled_clear: Vec<f64> = clearsky.iter().map(|v| alpha * v).collect();
385 let clear_line_length = window_line_length(
386 &scaled_clear,
387 sample_interval_minutes,
388 samples_per_window,
389 num_windows,
390 );
391 let residual: Vec<f64> = measured
392 .iter()
393 .zip(&scaled_clear)
394 .map(|(m, c)| m - c)
395 .collect();
396 let slope_max_diff = window_max_abs_diff(
397 &residual,
398 samples_per_window,
399 num_windows,
400 );
401
402 for w in 0..num_windows {
403 let line_diff = meas_line_length[w] - clear_line_length[w];
404 let c1 = (meas_mean[w] - alpha * clear_mean[w]).abs() < thresholds.mean_diff;
405 let c2 = (meas_max[w] - alpha * clear_max[w]).abs() < thresholds.max_diff;
406 let c3 = line_diff > thresholds.lower_line_length
407 && line_diff < thresholds.upper_line_length;
408 let c4 = meas_slope_nstd[w] < thresholds.var_diff;
409 let c5 = slope_max_diff[w] < thresholds.slope_dev;
410 let c6 = clear_mean[w] != 0.0 && !clear_mean[w].is_nan();
411 clear_windows_flags[w] = c1 && c2 && c3 && c4 && c5 && c6;
412 }
413
414 let mut sample_clear = vec![false; n];
417 for w in 0..num_windows {
418 if clear_windows_flags[w] {
419 for j in 0..samples_per_window {
420 sample_clear[w + j] = true;
421 }
422 }
423 }
424
425 let mut num = 0.0;
427 let mut den = 0.0;
428 for i in 0..n {
429 if sample_clear[i] && clearsky[i].is_finite() && measured[i].is_finite() {
430 num += measured[i] * clearsky[i];
431 den += clearsky[i] * clearsky[i];
432 }
433 }
434 let previous_alpha = alpha;
435 if den.is_finite() && den > 0.0 {
436 alpha = num / den;
437 }
438 if (alpha * 10_000.0).round() == (previous_alpha * 10_000.0).round() {
439 break;
440 }
441 }
442
443 let mut clear_samples = vec![false; n];
445 for w in 0..num_windows {
446 if clear_windows_flags[w] {
447 for j in 0..samples_per_window {
448 clear_samples[w + j] = true;
449 }
450 }
451 }
452
453 ClearSkyDetectionResult {
454 clear_samples,
455 alpha,
456 iterations,
457 }
458}
459
460fn window_mean(data: &[f64], w: usize, num_windows: usize) -> Vec<f64> {
463 let mut out = Vec::with_capacity(num_windows);
465 for start in 0..num_windows {
466 let mut s = 0.0_f64;
467 for j in 0..w {
468 s += data[start + j];
469 }
470 out.push(s / w as f64);
471 }
472 out
473}
474
475fn window_max(data: &[f64], w: usize, num_windows: usize) -> Vec<f64> {
476 let mut out = Vec::with_capacity(num_windows);
477 for start in 0..num_windows {
478 let mut m = f64::NEG_INFINITY;
479 for j in 0..w {
480 let v = data[start + j];
481 if v > m {
482 m = v;
483 }
484 }
485 out.push(m);
486 }
487 out
488}
489
490fn window_line_length(
493 data: &[f64],
494 sample_interval: f64,
495 w: usize,
496 num_windows: usize,
497) -> Vec<f64> {
498 let mut out = Vec::with_capacity(num_windows);
499 let dt2 = sample_interval * sample_interval;
500 for start in 0..num_windows {
501 let mut s = 0.0_f64;
502 for j in 0..w - 1 {
503 let d = data[start + j + 1] - data[start + j];
504 s += (d * d + dt2).sqrt();
505 }
506 out.push(s);
507 }
508 out
509}
510
511fn window_slope_nstd(
515 data: &[f64],
516 sample_interval: f64,
517 w: usize,
518 num_windows: usize,
519 window_means: &[f64],
520) -> Vec<f64> {
521 let mut out = Vec::with_capacity(num_windows);
522 let n_slopes = w - 1; let denom_ddof = (n_slopes - 1) as f64; for start in 0..num_windows {
525 let mut sum_s = 0.0_f64;
526 let mut sum_sq = 0.0_f64;
527 for j in 0..n_slopes {
528 let slope = (data[start + j + 1] - data[start + j]) / sample_interval;
529 sum_s += slope;
530 sum_sq += slope * slope;
531 }
532 let mean_s = sum_s / n_slopes as f64;
533 let var = (sum_sq - n_slopes as f64 * mean_s * mean_s) / denom_ddof;
534 let std = if var > 0.0 { var.sqrt() } else { 0.0 };
535 let wm = window_means[start];
536 out.push(if wm.abs() > 0.0 { std / wm } else { f64::NAN });
537 }
538 out
539}
540
541fn window_max_abs_diff(data: &[f64], w: usize, num_windows: usize) -> Vec<f64> {
543 let mut out = Vec::with_capacity(num_windows);
544 for start in 0..num_windows {
545 let mut m = 0.0_f64;
546 for j in 0..w - 1 {
547 let d = (data[start + j + 1] - data[start + j]).abs();
548 if d > m {
549 m = d;
550 }
551 }
552 out.push(m);
553 }
554 out
555}
556
557#[allow(clippy::too_many_arguments)]
580#[inline]
581pub fn bird(
582 zenith: f64,
583 airmass_relative: f64,
584 aod380: f64,
585 aod500: f64,
586 precipitable_water: f64,
587 ozone: f64,
588 pressure: f64,
589 dni_extra: f64,
590 asymmetry: f64,
591 albedo: f64,
592) -> BirdResult {
593 if zenith >= 90.0 || airmass_relative <= 0.0 {
594 return BirdResult { ghi: 0.0, dni: 0.0, dhi: 0.0, direct_horizontal: 0.0 };
595 }
596
597 let etr = dni_extra;
598 let ze_rad = zenith.to_radians();
599 let airmass = airmass_relative;
600
601 let am_press = airmass * (pressure / 101325.0);
603
604 let t_rayleigh = (-0.0903 * am_press.powf(0.84)
606 * (1.0 + am_press - am_press.powf(1.01)))
607 .exp();
608
609 let am_o3 = ozone * airmass;
611 let t_ozone = 1.0
612 - 0.1611 * am_o3 * (1.0 + 139.48 * am_o3).powf(-0.3034)
613 - 0.002715 * am_o3 / (1.0 + 0.044 * am_o3 + 0.0003 * am_o3 * am_o3);
614
615 let t_gases = (-0.0127 * am_press.powf(0.26)).exp();
617
618 let am_h2o = airmass * precipitable_water;
620 let t_water = 1.0
621 - 2.4959 * am_h2o
622 / ((1.0 + 79.034 * am_h2o).powf(0.6828) + 6.385 * am_h2o);
623
624 let bird_hulstrom = 0.27583 * aod380 + 0.35 * aod500;
626
627 let t_aerosol = (-(bird_hulstrom.powf(0.873))
629 * (1.0 + bird_hulstrom - bird_hulstrom.powf(0.7088))
630 * airmass.powf(0.9108))
631 .exp();
632
633 let taa = 1.0 - 0.1 * (1.0 - airmass + airmass.powf(1.06)) * (1.0 - t_aerosol);
635
636 let rs = 0.0685 + (1.0 - asymmetry) * (1.0 - t_aerosol / taa);
638
639 let id = 0.9662 * etr * t_aerosol * t_water * t_gases * t_ozone * t_rayleigh;
641
642 let cos_z = ze_rad.cos().max(0.0);
644 let id_nh = id * cos_z;
645
646 let ias = etr
648 * cos_z
649 * 0.79
650 * t_ozone
651 * t_gases
652 * t_water
653 * taa
654 * (0.5 * (1.0 - t_rayleigh) + asymmetry * (1.0 - t_aerosol / taa))
655 / (1.0 - airmass + airmass.powf(1.02));
656
657 let ghi = (id_nh + ias) / (1.0 - albedo * rs);
659 let dhi = ghi - id_nh;
660
661 BirdResult {
662 ghi: ghi.max(0.0),
663 dni: id.max(0.0),
664 dhi: dhi.max(0.0),
665 direct_horizontal: id_nh.max(0.0),
666 }
667}
668
669#[inline]
673pub fn bird_default(
674 zenith: f64,
675 airmass_relative: f64,
676 aod380: f64,
677 aod500: f64,
678 precipitable_water: f64,
679) -> BirdResult {
680 bird(
681 zenith,
682 airmass_relative,
683 aod380,
684 aod500,
685 precipitable_water,
686 0.3,
687 101325.0,
688 1364.0,
689 0.85,
690 0.2,
691 )
692}
693
694#[inline]
707pub fn lookup_linke_turbidity(latitude: f64, longitude: f64, month: u32) -> f64 {
708 let abs_lat = latitude.abs();
709
710 let base = if abs_lat > 60.0 {
712 2.0
714 } else if abs_lat > 45.0 {
715 3.0
717 } else if abs_lat > 23.5 {
718 let is_desert_belt = (abs_lat > 23.5 && abs_lat < 35.0)
722 && ((longitude > -20.0 && longitude < 60.0) || (longitude > 70.0 && longitude < 110.0) || (longitude > -120.0 && longitude < -100.0)); if is_desert_belt {
726 3.5
727 } else {
728 3.2
729 }
730 } else {
731 4.0
733 };
734
735 let is_northern = latitude >= 0.0;
737 let is_summer = if is_northern {
738 (5..=9).contains(&month)
739 } else {
740 month <= 3 || month >= 11
741 };
742
743 if is_summer {
744 base + 0.5
745 } else {
746 base
747 }
748}