solar_positioning/
grena3.rs1#![allow(clippy::unreadable_literal)]
11#![allow(clippy::many_single_char_names)]
12#![allow(clippy::suboptimal_flops)]
13
14use crate::error::check_coordinates;
15use crate::math::{
16 asin, atan2, cos, degrees_to_radians, floor, normalize_degrees_0_to_360, radians_to_degrees,
17 sin, sqrt, tan, PI,
18};
19use crate::{RefractionCorrection, Result, SolarPosition};
20#[cfg(feature = "chrono")]
21use chrono::{DateTime, Datelike, TimeZone, Timelike};
22
23#[cfg(feature = "chrono")]
73#[cfg_attr(docsrs, doc(cfg(feature = "chrono")))]
74#[allow(clippy::needless_pass_by_value)]
75pub fn solar_position<Tz: TimeZone>(
76 datetime: DateTime<Tz>,
77 latitude: f64,
78 longitude: f64,
79 delta_t: f64,
80 refraction: Option<RefractionCorrection>,
81) -> Result<SolarPosition> {
82 let t = calc_t(&datetime);
84 solar_position_from_t(t, latitude, longitude, delta_t, refraction)
85}
86
87pub fn solar_position_from_t(
104 t: f64,
105 latitude: f64,
106 longitude: f64,
107 delta_t: f64,
108 refraction: Option<RefractionCorrection>,
109) -> Result<SolarPosition> {
110 check_coordinates(latitude, longitude)?;
111
112 let t_e = t + 1.1574e-5 * delta_t;
113 let omega_at_e = 0.0172019715 * t_e;
114
115 let lambda = -1.388803
117 + 1.720279216e-2 * t_e
118 + 3.3366e-2 * sin(omega_at_e - 0.06172)
119 + 3.53e-4 * sin(2.0 * omega_at_e - 0.1163);
120
121 let epsilon = 4.089567e-1 - 6.19e-9 * t_e;
123
124 let s_lambda = sin(lambda);
125 let c_lambda = cos(lambda);
126 let s_epsilon = sin(epsilon);
127 let c_epsilon = sqrt(1.0 - s_epsilon * s_epsilon);
128
129 let mut alpha = atan2(s_lambda * c_epsilon, c_lambda);
131 if alpha < 0.0 {
132 alpha += 2.0 * PI;
133 }
134
135 let delta = asin(s_lambda * s_epsilon);
137
138 let mut h = 1.7528311 + 6.300388099 * t + degrees_to_radians(longitude) - alpha;
140 h = ((h + PI) % (2.0 * PI)) - PI;
141 if h < -PI {
142 h += 2.0 * PI;
143 }
144
145 let s_phi = sin(degrees_to_radians(latitude));
147 let c_phi = sqrt(1.0 - s_phi * s_phi);
148 let s_delta = sin(delta);
149 let c_delta = sqrt(1.0 - s_delta * s_delta);
150 let s_h = sin(h);
151 let c_h = cos(h);
152
153 let s_epsilon0 = s_phi * s_delta + c_phi * c_delta * c_h;
154 let e_p = asin(s_epsilon0) - 4.26e-5 * sqrt(1.0 - s_epsilon0 * s_epsilon0);
155 let gamma = atan2(s_h, c_h * s_phi - s_delta * c_phi / c_delta);
156
157 let delta_re = refraction.map_or(0.0, |correction| {
159 if e_p > 0.0 {
160 let pressure = correction.pressure();
161 let temperature = correction.temperature();
162 (0.08422 * (pressure / 1000.0))
163 / ((273.0 + temperature) * tan(e_p + 0.003138 / (e_p + 0.08919)))
164 } else {
165 0.0
166 }
167 });
168
169 let z = PI / 2.0 - e_p - delta_re;
170
171 let azimuth = normalize_degrees_0_to_360(radians_to_degrees(gamma + PI));
172 let zenith = radians_to_degrees(z);
173
174 SolarPosition::new(azimuth, zenith)
175}
176
177#[must_use]
184pub fn calc_t_from_components(
185 year: i32,
186 month: u32,
187 day: u32,
188 hour: u32,
189 minute: u32,
190 second: f64,
191) -> f64 {
192 let mut m = i32::try_from(month).expect("month should fit in i32");
193 let mut y = year;
194 let d = i32::try_from(day).expect("day should fit in i32");
195 let h = f64::from(hour) + f64::from(minute) / 60.0 + second / 3600.0;
196
197 if m <= 2 {
198 m += 12;
199 y -= 1;
200 }
201
202 floor(365.25 * f64::from(y - 2000)) + floor(30.6001 * f64::from(m + 1))
203 - floor(0.01 * f64::from(y))
204 + f64::from(d)
205 + 0.0416667 * h
206 - 21958.0
207}
208
209#[cfg(feature = "chrono")]
211fn calc_t<Tz: TimeZone>(datetime: &DateTime<Tz>) -> f64 {
212 let utc_datetime = datetime.with_timezone(&chrono::Utc);
214 calc_t_from_components(
215 utc_datetime.year(),
216 utc_datetime.month(),
217 utc_datetime.day(),
218 utc_datetime.hour(),
219 utc_datetime.minute(),
220 f64::from(utc_datetime.second()) + f64::from(utc_datetime.nanosecond()) / 1e9,
221 )
222}
223
224#[cfg(all(test, feature = "chrono"))]
225mod tests {
226 use super::*;
227 use chrono::{DateTime, FixedOffset};
228
229 #[test]
230 fn test_grena3_basic_functionality() {
231 let datetime = "2023-06-21T12:00:00-07:00"
232 .parse::<DateTime<FixedOffset>>()
233 .unwrap();
234
235 let result = solar_position(datetime, 37.7749, -122.4194, 69.0, None);
236
237 assert!(result.is_ok());
238 let position = result.unwrap();
239 assert!(position.azimuth() >= 0.0 && position.azimuth() <= 360.0);
240 assert!(position.zenith_angle() >= 0.0 && position.zenith_angle() <= 180.0);
241 }
242
243 #[test]
244 fn test_grena3_with_refraction() {
245 let datetime = "2023-06-21T12:00:00-07:00"
246 .parse::<DateTime<FixedOffset>>()
247 .unwrap();
248
249 let result = solar_position(
250 datetime,
251 37.7749,
252 -122.4194,
253 69.0,
254 Some(RefractionCorrection::new(1013.25, 15.0).unwrap()),
255 );
256
257 assert!(result.is_ok());
258 let position = result.unwrap();
259 assert!(position.azimuth() >= 0.0 && position.azimuth() <= 360.0);
260 assert!(position.zenith_angle() >= 0.0 && position.zenith_angle() <= 180.0);
261 }
262
263 #[test]
264 fn test_grena3_coordinate_validation() {
265 let datetime = "2023-06-21T12:00:00-07:00"
266 .parse::<DateTime<FixedOffset>>()
267 .unwrap();
268
269 assert!(solar_position(datetime, 95.0, 0.0, 0.0, None).is_err());
271
272 assert!(solar_position(datetime, 0.0, 185.0, 0.0, None).is_err());
274 }
275
276 #[test]
277 fn test_calc_t() {
278 let datetime = "2023-06-21T12:00:00-07:00"
280 .parse::<DateTime<FixedOffset>>()
281 .unwrap();
282 let t = calc_t(&datetime);
283
284 assert!(t.is_finite(), "t should be finite");
287
288 let t2 = calc_t(&datetime);
290 assert!(
291 (t - t2).abs() < f64::EPSILON,
292 "calc_t should be deterministic"
293 );
294
295 let datetime2 = "2023-06-22T12:00:00Z"
297 .parse::<DateTime<FixedOffset>>()
298 .unwrap();
299 let t3 = calc_t(&datetime2);
300 assert!(
301 (t - t3).abs() > 0.5,
302 "Different dates should give different t values"
303 );
304 }
305
306 #[test]
307 fn test_calc_t_pre_2000_flooring() {
308 let t = calc_t_from_components(1999, 12, 31, 0, 0, 0.0);
309 assert_eq!(t, -21915.0);
310 }
311}