1pub fn ground_angle(surface_tilt: f64, gcr: f64, slant_height: f64) -> f64 {
12 let x1 = gcr * slant_height * surface_tilt.to_radians().sin();
13 let x2 = gcr * slant_height * surface_tilt.to_radians().cos() + 1.0;
14 x1.atan2(x2).to_degrees()
15}
16
17pub fn masking_angle(surface_tilt: f64, gcr: f64, slant_height: f64) -> f64 {
30 if gcr <= 0.0 {
31 return 0.0;
32 }
33 let numerator = gcr * (1.0 - slant_height) * surface_tilt.to_radians().sin();
34 let denominator = 1.0 - gcr * (1.0 - slant_height) * surface_tilt.to_radians().cos();
35 (numerator / denominator).atan().to_degrees()
36}
37
38pub fn masking_angle_passias(surface_tilt: f64, gcr: f64) -> f64 {
47 let beta = surface_tilt.to_radians();
48 let sin_b = beta.sin();
49 let cos_b = beta.cos();
50
51 if sin_b.abs() < 1e-10 || gcr <= 0.0 {
52 return 0.0;
53 }
54
55 let x = 1.0 / gcr;
56
57 let term1 = -x * sin_b * (2.0 * x * cos_b - (x * x + 1.0)).abs().ln() / 2.0;
58 let term2 = (x * cos_b - 1.0) * ((x * cos_b - 1.0) / (x * sin_b)).atan();
59 let term3 = (1.0 - x * cos_b) * (cos_b / sin_b).atan();
60 let term4 = x * x.ln() * sin_b;
61
62 let psi_avg = term1 + term2 + term3 + term4;
63
64 if psi_avg.is_finite() {
65 psi_avg.to_degrees()
66 } else {
67 0.0
68 }
69}
70
71pub fn projected_solar_zenith_angle(
82 solar_zenith: f64,
83 solar_azimuth: f64,
84 axis_tilt: f64,
85 axis_azimuth: f64,
86) -> f64 {
87 let sin_sz = solar_zenith.to_radians().sin();
88 let cos_aa = axis_azimuth.to_radians().cos();
89 let sin_aa = axis_azimuth.to_radians().sin();
90 let sin_at = axis_tilt.to_radians().sin();
91
92 let sx = sin_sz * solar_azimuth.to_radians().sin();
94 let sy = sin_sz * solar_azimuth.to_radians().cos();
95 let sz = solar_zenith.to_radians().cos();
96
97 let sx_prime = sx * cos_aa - sy * sin_aa;
99 let sz_prime = sx * sin_aa * sin_at + sy * sin_at * cos_aa + sz * axis_tilt.to_radians().cos();
100
101 sx_prime.atan2(sz_prime).to_degrees()
103}
104
105#[allow(clippy::too_many_arguments)]
123pub fn shaded_fraction1d(
124 solar_zenith: f64,
125 solar_azimuth: f64,
126 axis_azimuth: f64,
127 shaded_row_rotation: f64,
128 collector_width: f64,
129 pitch: f64,
130 axis_tilt: f64,
131 surface_to_axis_offset: f64,
132 cross_axis_slope: f64,
133) -> f64 {
134 let shading_row_rotation = shaded_row_rotation;
136
137 let psza = projected_solar_zenith_angle(solar_zenith, solar_azimuth, axis_tilt, axis_azimuth);
138
139 let thetas_1_s_diff = shading_row_rotation - psza;
140 let thetas_2_s_diff = shaded_row_rotation - psza;
141 let theta_s_rotation_diff = psza - cross_axis_slope;
142
143 let cos_theta_2_s_diff_abs = thetas_2_s_diff.to_radians().cos().abs().max(1e-6);
144 let collector_width_safe = collector_width.max(1e-6);
145 let cross_axis_cos = cross_axis_slope.to_radians().cos().max(1e-6);
146
147 let t_asterisk = 0.5
149 + thetas_1_s_diff.to_radians().cos().abs() / cos_theta_2_s_diff_abs / 2.0
150 + (psza.signum()
151 * surface_to_axis_offset
152 / collector_width_safe
153 / cos_theta_2_s_diff_abs
154 * (thetas_2_s_diff.to_radians().sin() - thetas_1_s_diff.to_radians().sin()))
155 - (pitch / collector_width_safe
156 * theta_s_rotation_diff.to_radians().cos()
157 / cos_theta_2_s_diff_abs
158 / cross_axis_cos);
159
160 t_asterisk.clamp(0.0, 1.0)
161}
162
163pub fn sky_diffuse_pass_equation(masking_angle: f64) -> f64 {
165 (1.0 + masking_angle.to_radians().cos()) / 2.0
166}
167
168pub fn sky_diffuse_passias(masking_angle: f64) -> f64 {
178 1.0 - (masking_angle / 2.0).to_radians().cos().powi(2)
179}