#[inline]
pub fn solar_projection_tangent(
solar_zenith: f64,
solar_azimuth: f64,
surface_azimuth: f64,
) -> f64 {
let rotation = (solar_azimuth - surface_azimuth).to_radians();
rotation.cos() * solar_zenith.to_radians().tan()
}
#[inline]
pub fn unshaded_ground_fraction(
surface_tilt: f64,
surface_azimuth: f64,
solar_zenith: f64,
solar_azimuth: f64,
gcr: f64,
) -> f64 {
const MAX_ZENITH_DEG: f64 = 87.0;
if solar_zenith > MAX_ZENITH_DEG {
return 0.0;
}
let tan_phi = solar_projection_tangent(solar_zenith, solar_azimuth, surface_azimuth);
let b = surface_tilt.to_radians();
let width = gcr * (b.cos() + b.sin() * tan_phi).abs();
(1.0 - width.min(1.0)).max(0.0)
}
#[inline]
fn vf_poly(surface_tilt: f64, gcr: f64, x: f64, delta: f64) -> f64 {
let a = 1.0 / gcr;
let c = surface_tilt.to_radians().cos();
(a * a + 2.0 * delta * a * c * x + x * x).sqrt()
}
#[inline]
pub fn vf_row_sky_2d(surface_tilt: f64, gcr: f64, x: f64) -> f64 {
let p = vf_poly(surface_tilt, gcr, 1.0 - x, -1.0);
let c = surface_tilt.to_radians().cos();
0.5 * (1.0 + (c / gcr - (1.0 - x)) / p)
}
#[inline]
pub fn vf_row_sky_2d_integ(surface_tilt: f64, gcr: f64, x0: f64, x1: f64) -> f64 {
let u = (x1 - x0).abs();
if u < 1e-6 {
return vf_row_sky_2d(surface_tilt, gcr, x0);
}
let p0 = vf_poly(surface_tilt, gcr, 1.0 - x0, -1.0);
let p1 = vf_poly(surface_tilt, gcr, 1.0 - x1, -1.0);
0.5 * (1.0 + (p1 - p0) / u)
}
#[inline]
pub fn vf_row_ground_2d(surface_tilt: f64, gcr: f64, x: f64) -> f64 {
let p = vf_poly(surface_tilt, gcr, x, 1.0);
let c = surface_tilt.to_radians().cos();
0.5 * (1.0 - (c / gcr + x) / p)
}
#[inline]
pub fn vf_row_ground_2d_integ(surface_tilt: f64, gcr: f64, x0: f64, x1: f64) -> f64 {
let u = (x1 - x0).abs();
if u < 1e-6 {
return vf_row_ground_2d(surface_tilt, gcr, x0);
}
let p0 = vf_poly(surface_tilt, gcr, x0, 1.0);
let p1 = vf_poly(surface_tilt, gcr, x1, 1.0);
0.5 * (1.0 - (p1 - p0) / u)
}
#[allow(clippy::too_many_arguments)]
#[inline]
pub fn get_irradiance_infinite_sheds(
surface_tilt: f64,
surface_azimuth: f64,
gcr: f64,
_height: f64,
_pitch: f64,
ghi: f64,
dhi: f64,
_dni: f64,
albedo: f64,
) -> f64 {
if gcr <= 0.0 || !ghi.is_finite() || !dhi.is_finite() {
return 0.0;
}
rear_irradiance_sheds(
surface_tilt,
surface_azimuth,
None,
gcr,
ghi,
dhi,
albedo,
)
}
#[inline]
pub fn rear_irradiance_sheds(
surface_tilt: f64,
surface_azimuth: f64,
solar_position: Option<(f64, f64)>,
gcr: f64,
ghi: f64,
dhi: f64,
albedo: f64,
) -> f64 {
if gcr <= 0.0 {
return 0.0;
}
let rear_tilt = 180.0 - surface_tilt;
let vf_rear_sky = vf_row_sky_2d_integ(rear_tilt, gcr, 0.0, 1.0);
let rear_sky = dhi * vf_rear_sky;
let f_gnd_beam = match solar_position {
Some((sz, sa)) => unshaded_ground_fraction(surface_tilt, surface_azimuth, sz, sa, gcr),
None => {
let b = surface_tilt.to_radians();
(1.0 - (gcr * b.cos()).min(1.0)).max(0.0)
}
};
let vf_gnd_sky = 1.0;
let poa_ground = albedo * (f_gnd_beam * (ghi - dhi).max(0.0) + vf_gnd_sky * dhi.max(0.0));
let vf_rear_ground = vf_row_ground_2d_integ(rear_tilt, gcr, 0.0, 1.0);
let rear_ground = poa_ground * vf_rear_ground;
(rear_sky + rear_ground).max(0.0)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn horizontal_row_sees_full_sky() {
let vf = vf_row_sky_2d_integ(0.0, 0.01, 0.0, 1.0);
assert!(vf > 0.9, "flat row with tiny GCR should see most of sky, got {vf}");
}
#[test]
fn vertical_row_sees_half_sky_at_most() {
let vf = vf_row_sky_2d_integ(90.0, 0.5, 0.0, 1.0);
assert!(vf <= 0.5 + 1e-9, "vertical row vf to sky {vf} should be ≤ 0.5");
}
#[test]
fn row_sky_plus_row_ground_sum_leaves_neighbor_view() {
for &tilt in &[10.0_f64, 30.0, 60.0] {
for &gcr in &[0.3_f64, 0.5] {
let s = vf_row_sky_2d(tilt, gcr, 0.5);
let g = vf_row_ground_2d(tilt, gcr, 0.5);
assert!((0.0..=1.0).contains(&s), "vf_sky out of range: {s}");
assert!((0.0..=1.0).contains(&g), "vf_ground out of range: {g}");
assert!(
s + g <= 1.0 + 1e-9,
"vf_sky+vf_ground at tilt={tilt} gcr={gcr} mid-row = {}",
s + g
);
}
}
}
#[test]
fn unshaded_fraction_bounds() {
let f = unshaded_ground_fraction(30.0, 180.0, 30.0, 180.0, 0.5);
assert!((0.0..=1.0).contains(&f));
assert_eq!(unshaded_ground_fraction(30.0, 180.0, 90.0, 180.0, 0.5), 0.0);
}
#[test]
fn rear_irradiance_positive() {
let rear = rear_irradiance_sheds(
30.0,
180.0,
Some((30.0, 180.0)),
0.4,
1000.0,
150.0,
0.25,
);
assert!(rear > 0.0 && rear < 500.0, "implausible rear irradiance {rear}");
}
}