solar_positioning/
grena3.rs1#![allow(clippy::unreadable_literal)]
10#![allow(clippy::many_single_char_names)]
11#![allow(clippy::suboptimal_flops)]
12
13use crate::error::{
14 check_coordinates, check_pressure, check_refraction_params_usable, check_temperature,
15};
16use crate::math::{
17 PI, asin, atan2, cos, degrees_to_radians, normalize_degrees_0_to_360, radians_to_degrees, sin,
18 sqrt, tan,
19};
20use crate::{Result, SolarPosition};
21use chrono::{DateTime, Datelike, TimeZone, Timelike};
22
23pub fn solar_position<Tz: TimeZone>(
57 datetime: DateTime<Tz>,
58 latitude: f64,
59 longitude: f64,
60 delta_t: f64,
61) -> Result<SolarPosition> {
62 solar_position_with_refraction(datetime, latitude, longitude, delta_t, None, None)
63}
64
65pub fn solar_position_with_refraction<Tz: TimeZone>(
81 datetime: DateTime<Tz>,
82 latitude: f64,
83 longitude: f64,
84 delta_t: f64,
85 pressure: Option<f64>,
86 temperature: Option<f64>,
87) -> Result<SolarPosition> {
88 check_coordinates(latitude, longitude)?;
89
90 if let Some(p) = pressure {
92 check_pressure(p)?;
93 }
94 if let Some(t) = temperature {
95 check_temperature(t)?;
96 }
97
98 let t = calc_t(&datetime);
100 let t_e = t + 1.1574e-5 * delta_t;
101 let omega_at_e = 0.0172019715 * t_e;
102
103 let lambda = -1.388803
105 + 1.720279216e-2 * t_e
106 + 3.3366e-2 * sin(omega_at_e - 0.06172)
107 + 3.53e-4 * sin(2.0 * omega_at_e - 0.1163);
108
109 let epsilon = 4.089567e-1 - 6.19e-9 * t_e;
111
112 let s_lambda = sin(lambda);
113 let c_lambda = cos(lambda);
114 let s_epsilon = sin(epsilon);
115 let c_epsilon = sqrt(1.0 - s_epsilon * s_epsilon);
116
117 let mut alpha = atan2(s_lambda * c_epsilon, c_lambda);
119 if alpha < 0.0 {
120 alpha += 2.0 * PI;
121 }
122
123 let delta = asin(s_lambda * s_epsilon);
125
126 let mut h = 1.7528311 + 6.300388099 * t + degrees_to_radians(longitude) - alpha;
128 h = ((h + PI) % (2.0 * PI)) - PI;
129 if h < -PI {
130 h += 2.0 * PI;
131 }
132
133 let s_phi = sin(degrees_to_radians(latitude));
135 let c_phi = sqrt(1.0 - s_phi * s_phi);
136 let s_delta = sin(delta);
137 let c_delta = sqrt(1.0 - s_delta * s_delta);
138 let s_h = sin(h);
139 let c_h = cos(h);
140
141 let s_epsilon0 = s_phi * s_delta + c_phi * c_delta * c_h;
142 let e_p = asin(s_epsilon0) - 4.26e-5 * sqrt(1.0 - s_epsilon0 * s_epsilon0);
143 let gamma = atan2(s_h, c_h * s_phi - s_delta * c_phi / c_delta);
144
145 let delta_re = if let (Some(p), Some(t)) = (pressure, temperature) {
147 if check_refraction_params_usable(p, t) && e_p > 0.0 {
148 (0.08422 * (p / 1000.0)) / ((273.0 + t) * tan(e_p + 0.003138 / (e_p + 0.08919)))
149 } else {
150 0.0
151 }
152 } else {
153 0.0
154 };
155
156 let z = PI / 2.0 - e_p - delta_re;
157
158 let azimuth = normalize_degrees_0_to_360(radians_to_degrees(gamma + PI));
159 let zenith = radians_to_degrees(z);
160
161 SolarPosition::new(azimuth, zenith)
162}
163
164fn calc_t<Tz: TimeZone>(datetime: &DateTime<Tz>) -> f64 {
166 let utc_datetime = datetime.with_timezone(&chrono::Utc);
168 let mut m = i32::try_from(utc_datetime.month()).expect("month should fit in i32");
169 let mut y = utc_datetime.year();
170 let d = i32::try_from(utc_datetime.day()).expect("day should fit in i32");
171 let h = f64::from(utc_datetime.hour())
172 + f64::from(utc_datetime.minute()) / 60.0
173 + f64::from(utc_datetime.second()) / 3600.0;
174
175 if m <= 2 {
176 m += 12;
177 y -= 1;
178 }
179
180 f64::from((365.25 * f64::from(y - 2000)) as i32)
181 + f64::from((30.6001 * f64::from(m + 1)) as i32)
182 - f64::from((0.01 * f64::from(y)) as i32)
183 + f64::from(d)
184 + 0.0416667 * h
185 - 21958.0
186}
187
188#[cfg(test)]
189mod tests {
190 use super::*;
191 use chrono::{DateTime, FixedOffset};
192
193 #[test]
194 fn test_grena3_basic_functionality() {
195 let datetime = "2023-06-21T12:00:00-07:00"
196 .parse::<DateTime<FixedOffset>>()
197 .unwrap();
198
199 let result = solar_position(datetime, 37.7749, -122.4194, 69.0);
200
201 assert!(result.is_ok());
202 let position = result.unwrap();
203 assert!(position.azimuth() >= 0.0 && position.azimuth() <= 360.0);
204 assert!(position.zenith_angle() >= 0.0 && position.zenith_angle() <= 180.0);
205 }
206
207 #[test]
208 fn test_grena3_with_refraction() {
209 let datetime = "2023-06-21T12:00:00-07:00"
210 .parse::<DateTime<FixedOffset>>()
211 .unwrap();
212
213 let result = solar_position_with_refraction(
214 datetime,
215 37.7749,
216 -122.4194,
217 69.0,
218 Some(1013.25),
219 Some(15.0),
220 );
221
222 assert!(result.is_ok());
223 let position = result.unwrap();
224 assert!(position.azimuth() >= 0.0 && position.azimuth() <= 360.0);
225 assert!(position.zenith_angle() >= 0.0 && position.zenith_angle() <= 180.0);
226 }
227
228 #[test]
229 fn test_grena3_coordinate_validation() {
230 let datetime = "2023-06-21T12:00:00-07:00"
231 .parse::<DateTime<FixedOffset>>()
232 .unwrap();
233
234 assert!(solar_position(datetime, 95.0, 0.0, 0.0).is_err());
236
237 assert!(solar_position(datetime, 0.0, 185.0, 0.0).is_err());
239 }
240
241 #[test]
242 fn test_calc_t() {
243 let datetime = "2023-06-21T12:00:00-07:00"
245 .parse::<DateTime<FixedOffset>>()
246 .unwrap();
247 let t = calc_t(&datetime);
248
249 assert!(t.is_finite(), "t should be finite");
252
253 let t2 = calc_t(&datetime);
255 assert!(
256 (t - t2).abs() < f64::EPSILON,
257 "calc_t should be deterministic"
258 );
259
260 let datetime2 = "2023-06-22T12:00:00Z"
262 .parse::<DateTime<FixedOffset>>()
263 .unwrap();
264 let t3 = calc_t(&datetime2);
265 assert!(
266 (t - t3).abs() > 0.5,
267 "Different dates should give different t values"
268 );
269 }
270}