1 #![cfg_attr(feature = "benchmark", feature(test))]
2 // Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
3 // http://www.apache.org/licenses/LICENSE-2.0>.
4 // This file may not be copied, modified, or distributed
5 // except according to those terms.
6
7 //! Solar Position Algorithm module for Rust
8 //!
9 //! Collection of algorithms calculating sunrise/sunset and azimuth/zenith-angle.
10
11 extern crate chrono;
12
13 use chrono::prelude::Utc;
14 use chrono::DateTime;
15 use chrono::TimeZone;
16 use chrono::Timelike;
17 use std::error;
18 use std::f64::consts::PI;
19 use std::time::{SystemTime, UNIX_EPOCH};
20
21 const PI2: f64 = PI * 2.0;
22 const RAD: f64 = 0.017453292519943295769236907684886;
23 const EARTH_MEAN_RADIUS: f64 = 6371.01;
24 // In km
25 const ASTRONOMICAL_UNIT: f64 = 149597890.0;
26 // In km
27 const JD2000: f64 = 2451545.0;
28
29 /// The sun-rise and sun-set as UTC, otherwise permanent polar-night or polar-day
30 #[derive(Debug, Clone)]
31 pub enum SunriseAndSet {
32 /// The polar night occurs in the northernmost and southernmost regions of the Earth when
33 /// the night lasts for more than 24 hours. This occurs only inside the polar circles.
34 PolarNight,
35 /// The polar day occurs when the Sun stays above the horizon for more than 24 hours.
36 /// This occurs only inside the polar circles.
37 PolarDay,
38 /// The sunrise and sunset as UTC, the Coordinated Universal Time (offset +00:00), incl.
39 /// leap-seconds.
40 ///
41 /// These UTC time-points can be transformed to local times based on longitude or the
42 /// corresponding timezone. As timezones are corresponding to borders of countries, a map
43 /// would be required.
44 Daylight(DateTime<Utc>, DateTime<Utc>),
45 }
46
47 /// The solar position
48 #[derive(Debug, Clone)]
49 pub struct SolarPos {
50 /// horizontal angle measured clockwise from a north base line or meridian
51 pub azimuth: f64,
52 /// the angle between the zenith and the centre of the sun's disc
53 pub zenith_angle: f64,
54 }
55
56 /// The error conditions
57 #[derive(Debug, Clone)]
58 pub enum SpaError {
59 BadParam,
60 }
61
62 /// Displaying SpaError, enabling error message on console
63 impl std::fmt::Display for SpaError {
fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result64 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
65 match *self {
66 SpaError::BadParam => write!(f, "Latitude or longitude are not within valid ranges."),
67 }
68 }
69 }
70
71 /// Implementing Error trait for SpaError
72 impl error::Error for SpaError {}
73
74 /// Converting DateTime<Utc> to Julian-Days (f64)
75 ///
76 /// Julian-Days is the number of days (incl. fraction) since January 1, 4713 BC, noon, without the
77 /// leap seconds.
78 ///
79 /// # Arguments
80 ///
81 /// * `utc` - UTC time point
82 ///
to_julian(utc: DateTime<Utc>) -> f6483 fn to_julian(utc: DateTime<Utc>) -> f64 {
84 let systime: SystemTime = utc.into();
85
86 let seconds_since_epoch = systime.duration_since(UNIX_EPOCH).unwrap().as_secs();
87
88 ((seconds_since_epoch as f64) / 86400.0) + 2440587.5
89 }
90
91 /// Convert Julian-Days to UTC time-point
92 ///
93 /// Julian-Days is the number of days (incl. fraction) since January 1, 4713 BC, noon, without the
94 /// leap seconds.
95 ///
96 /// # Arguments
97 ///
98 /// * `jd` - Julian days since January 1, 4713 BC noon (12:00)
99 ///
to_utc(jd: f64) -> DateTime<Utc>100 fn to_utc(jd: f64) -> DateTime<Utc> {
101 let secs_since_epoch = (jd - 2440587.5) * 86400.0;
102 let secs = secs_since_epoch.trunc();
103 let nanos = (secs_since_epoch - secs) * (1000.0 * 1000.0 * 1000.0);
104 Utc.timestamp(secs as i64, nanos as u32)
105 }
106
107 /// Projecting value into range [0,..,PI]
108 ///
109 /// # Arguments
110 ///
111 /// * `x` - radiant, not normalized to range [-PI..PI]
in_pi(x: f64) -> f64112 fn in_pi(x: f64) -> f64 {
113 let n = (x / PI2) as i64;
114 let result = x - (n as f64 * PI2);
115 if result < 0.0 {
116 (result + PI2)
117 } else {
118 result
119 }
120 }
121
122 /// Returns the eccentricity of earth ellipse
123 ///
124 /// # Arguments
125 ///
126 /// * `t` - time according to standard equinox J2000.0
127 ///
eps(t: f64) -> f64128 fn eps(t: f64) -> f64 {
129 return RAD * (23.43929111 + ((-46.8150) * t - 0.00059 * t * t + 0.001813 * t * t * t) / 3600.0);
130 }
131
132 /// Calculates equation of time, returning the tuple (delta-ascension, declination)
133 ///
134 /// # Arguments
135 ///
136 /// * `t` - time according to standard equinox J2000.0
berechne_zeitgleichung(t: f64) -> (f64, f64)137 fn berechne_zeitgleichung(t: f64) -> (f64, f64) {
138 let mut ra_mittel = 18.71506921 + 2400.0513369 * t + (2.5862e-5 - 1.72e-9 * t) * t * t;
139
140 let m = in_pi(PI2 * (0.993133 + 99.997361 * t));
141 let l = in_pi(
142 PI2 * (0.7859453
143 + m / PI2
144 + (6893.0 * f64::sin(m) + 72.0 * f64::sin(2.0 * m) + 6191.2 * t) / 1296.0e3),
145 );
146 let e = eps(t);
147 let mut ra = f64::atan(f64::tan(l) * f64::cos(e));
148
149 if ra < 0.0 {
150 ra += PI;
151 }
152 if l > PI {
153 ra += PI;
154 }
155
156 ra = 24.0 * ra / PI2;
157
158 let dk = f64::asin(f64::sin(e) * f64::sin(l));
159
160 // Damit 0<=RA_Mittel<24
161 ra_mittel = 24.0 * in_pi(PI2 * ra_mittel / 24.0) / PI2;
162
163 let mut d_ra = ra_mittel - ra;
164 if d_ra < -12.0 {
165 d_ra += 24.0;
166 }
167 if d_ra > 12.0 {
168 d_ra -= 24.0;
169 }
170
171 d_ra = d_ra * 1.0027379;
172
173 return (d_ra, dk);
174 }
175
176 /// Returning Sunrise and Sunset (or PolarNight/PolarDay) at geo-pos `lat/lon` at time `t` (UTC)
177 ///
178 /// # Arguments
179 ///
180 /// * `utc` - UTC time-point (DateTime<Utc>)
181 /// * `lat` - latitude in WGS84 system, ranging from -90.0 to 90.0.
182 /// * `lon` - longitude in WGS84 system, ranging from -180.0 to 180.0
183 ///
184 /// North of ca 67.4 degree and south of ca -67.4 it may be permanent night or day. In this
185 /// case may be SunriseAndSet::PolarNight or SunriseAndSet::PolarDay.
186 ///
187 /// In case latitude or longitude are not in valid ranges, the function will return Err(BadParam).
188 ///
189 /// Algorithm ported to Rust from http://lexikon.astronomie.info/zeitgleichung/neu.html
190 /// Its accuracy is in the range of a few minutes.
calc_sunrise_and_set( utc: DateTime<Utc>, lat: f64, lon: f64, ) -> Result<SunriseAndSet, SpaError>191 pub fn calc_sunrise_and_set(
192 utc: DateTime<Utc>,
193 lat: f64,
194 lon: f64,
195 ) -> Result<SunriseAndSet, SpaError> {
196 if -90.0 > lat || 90.0 < lat || -180.0 > lon || 180.0 < lon {
197 return Err(SpaError::BadParam);
198 }
199
200 let jd = to_julian(utc);
201 let t = (jd - JD2000) / 36525.0;
202 let h = -50.0 / 60.0 * RAD;
203 let b = lat * RAD; // geographische Breite
204 let geographische_laenge = lon;
205
206 let (ra_d, dk) = berechne_zeitgleichung(t);
207
208 let aux = (f64::sin(h) - f64::sin(b) * f64::sin(dk)) / (f64::cos(b) * f64::cos(dk));
209 if aux >= 1.0 {
210 Result::Ok(SunriseAndSet::PolarNight)
211 } else if aux <= -1.0 {
212 Result::Ok(SunriseAndSet::PolarDay)
213 } else {
214 let zeitdifferenz = 12.0 * f64::acos(aux) / PI;
215
216 let aufgang_lokal = 12.0 - zeitdifferenz - ra_d;
217 let untergang_lokal = 12.0 + zeitdifferenz - ra_d;
218 let aufgang_welt = aufgang_lokal - geographische_laenge / 15.0;
219 let untergang_welt = untergang_lokal - geographische_laenge / 15.0;
220 let jd_start = jd.trunc(); // discard fraction of day
221
222 let aufgang_jd = (jd_start as f64) - 0.5 + (aufgang_welt / 24.0);
223 let untergang_jd = (jd_start as f64) - 0.5 + (untergang_welt / 24.0);
224
225 // let untergang_utc = untergang_lokal - geographische_laenge /15.0;
226 let sunriseset = SunriseAndSet::Daylight(to_utc(aufgang_jd), to_utc(untergang_jd));
227 Result::Ok(sunriseset)
228 }
229 }
230
231 /// Returning solar position (azimuth and zenith-angle) at time `t` and geo-pos `lat` and `lon`
232 ///
233 /// # Arguments
234 ///
235 /// * `utc` - UTC time-point (DateTime<Utc>)
236 /// * `lat` - latitude in WGS84 system, ranging from -90.0 to 90.0.
237 /// * `lon` - longitude in WGS84 system, ranging from -180.0 to 180.0
238 ///
239 /// In case latitude or longitude are not in valid ranges, the function will return Err(BadParam).
240 ///
241 /// Algorithm ported to Rust from http://www.psa.es/sdg/sunpos.htm
242 /// The algorithm is accurate to within 0.5 minutes of arc for the year 1999 and following.
calc_solar_position(utc: DateTime<Utc>, lat: f64, lon: f64) -> Result<SolarPos, SpaError>243 pub fn calc_solar_position(utc: DateTime<Utc>, lat: f64, lon: f64) -> Result<SolarPos, SpaError> {
244 if -90.0 > lat || 90.0 < lat || -180.0 > lon || 180.0 < lon {
245 return Err(SpaError::BadParam);
246 }
247
248 let decimal_hours =
249 (utc.hour() as f64) + ((utc.minute() as f64) + (utc.second() as f64) / 60.0) / 60.0;
250
251 // Calculate difference in days between the current Julian Day
252 // and JD 2451545.0, which is noon 1 January 2000 Universal Time
253 let elapsed_julian_days = to_julian(utc) - JD2000;
254
255 // Calculate ecliptic coordinates (ecliptic longitude and obliquity of the
256 // ecliptic in radians but without limiting the angle to be less than 2*Pi
257 // (i.e., the result may be greater than 2*Pi)
258 let (ecliptic_longitude, ecliptic_obliquity) = {
259 let omega = 2.1429 - (0.0010394594 * elapsed_julian_days);
260 let mean_longitude = 4.8950630 + (0.017202791698 * elapsed_julian_days); // Radians
261 let mean_anomaly = 6.2400600 + (0.0172019699 * elapsed_julian_days);
262 let ecliptic_longitude = mean_longitude
263 + 0.03341607 * f64::sin(mean_anomaly)
264 + 0.00034894 * f64::sin(2.0 * mean_anomaly)
265 - 0.0001134
266 - 0.0000203 * f64::sin(omega);
267 let ecliptic_obliquity =
268 0.4090928 - 6.2140e-9 * elapsed_julian_days + 0.0000396 * f64::cos(omega);
269 (ecliptic_longitude, ecliptic_obliquity)
270 };
271
272 // Calculate celestial coordinates ( right ascension and declination ) in radians
273 // but without limiting the angle to be less than 2*Pi (i.e., the result may be
274 // greater than 2*Pi)
275 let (declination, right_ascension) = {
276 let sin_ecliptic_longitude = f64::sin(ecliptic_longitude);
277 let dy = f64::cos(ecliptic_obliquity) * sin_ecliptic_longitude;
278 let dx = f64::cos(ecliptic_longitude);
279 let mut right_ascension = f64::atan2(dy, dx);
280 if right_ascension < 0.0 {
281 right_ascension = right_ascension + PI2;
282 }
283 let declination = f64::asin(f64::sin(ecliptic_obliquity) * sin_ecliptic_longitude);
284 (declination, right_ascension)
285 };
286
287 // Calculate local coordinates ( azimuth and zenith angle ) in degrees
288 let (azimuth, zenith_angle) = {
289 let greenwich_mean_sidereal_time =
290 6.6974243242 + 0.0657098283 * elapsed_julian_days + decimal_hours;
291 let local_mean_sidereal_time = (greenwich_mean_sidereal_time * 15.0 + lon) * RAD;
292 let hour_angle = local_mean_sidereal_time - right_ascension;
293 let latitude_in_radians = lat * RAD;
294 let cos_latitude = f64::cos(latitude_in_radians);
295 let sin_latitude = f64::sin(latitude_in_radians);
296 let cos_hour_angle = f64::cos(hour_angle);
297 let mut zenith_angle = f64::acos(
298 cos_latitude * cos_hour_angle * f64::cos(declination)
299 + f64::sin(declination) * sin_latitude,
300 );
301 let dy = -f64::sin(hour_angle);
302 let dx = f64::tan(declination) * cos_latitude - sin_latitude * cos_hour_angle;
303 let mut azimuth = f64::atan2(dy, dx);
304 if azimuth < 0.0 {
305 azimuth = azimuth + PI2;
306 }
307 azimuth = azimuth / RAD;
308 // Parallax Correction
309 let parallax = (EARTH_MEAN_RADIUS / ASTRONOMICAL_UNIT) * f64::sin(zenith_angle);
310 zenith_angle = (zenith_angle + parallax) / RAD;
311 (azimuth, zenith_angle)
312 };
313
314 let solpos = SolarPos {
315 azimuth: azimuth,
316 zenith_angle: zenith_angle,
317 };
318
319 Result::Ok(solpos)
320 }
321
322 #[cfg(test)]
323 mod tests {
324 extern crate chrono;
325
326 use self::chrono::prelude::Utc;
327 use self::chrono::TimeZone;
328 use super::berechne_zeitgleichung;
329 use super::calc_solar_position;
330 use super::calc_sunrise_and_set;
331 use super::eps;
332 use super::in_pi;
333 use super::to_julian;
334 use super::to_utc;
335 use super::SunriseAndSet;
336 use super::JD2000;
337 use super::PI2;
338 use std::f64::consts::FRAC_PI_2;
339 use std::f64::consts::PI;
340 use tests::chrono::Datelike;
341 use tests::chrono::Timelike;
342
343 const LAT_DEG: f64 = 48.1;
344 const LON_DEG: f64 = 11.6;
345
346 #[test]
test_pi()347 fn test_pi() {
348 assert!(FRAC_PI_2 < PI);
349 assert!(in_pi(LAT_DEG) < PI2);
350 assert!(in_pi(LON_DEG) < PI2);
351 }
352
353 #[test]
test_datetime()354 fn test_datetime() {
355 let unix_time_secs = 1128057420; // 2005-09-30T5:17:00Z
356 let utc = Utc.timestamp(unix_time_secs, 0);
357
358 assert_eq!(utc.year(), 2005);
359 assert_eq!(utc.month(), 9);
360 assert_eq!(utc.day(), 30);
361 assert_eq!(utc.hour(), 5);
362 assert_eq!(utc.minute(), 17);
363 }
364
365 #[test]
test_julian_day()366 fn test_julian_day() {
367 // test-vector from https://de.wikipedia.org/wiki/Sonnenstand
368
369 // 6. August 2006 um 6 Uhr ut
370 let dt = Utc.ymd(2006, 8, 6).and_hms(6, 0, 0);
371
372 let jd = to_julian(dt);
373 assert_eq!(jd, 2453953.75);
374 assert_eq!(jd - JD2000, 2408.75);
375
376 assert_eq!(dt, to_utc(jd));
377 }
378
379 #[test]
test_zeitgleichung()380 fn test_zeitgleichung() {
381 // test-vector from http://lexikon.astronomie.info/zeitgleichung/neu.html
382
383 let exp_jd = 2453644.0;
384 let exp_t = 0.057467488021902803;
385 let exp_e = 0.40907976105657956;
386 let exp_d_ra = 0.18539782794253773;
387 let exp_dk = -0.05148602985190724;
388
389 let dt = Utc.ymd(2005, 9, 30).and_hms(12, 0, 0);
390
391 let jd = to_julian(dt);
392 assert_eq!(exp_jd, jd);
393
394 let t = (jd - JD2000) / 36525.0;
395 assert_eq!(exp_t, t);
396
397 assert_eq!(exp_e, eps(t));
398
399 let (d_ra, dk) = berechne_zeitgleichung(t);
400
401 assert_eq!(exp_d_ra, d_ra);
402 assert_eq!(exp_dk, dk);
403 }
404
405 #[test]
test_calc_sunrise_and_set()406 fn test_calc_sunrise_and_set() {
407 // test-vector from http://lexikon.astronomie.info/zeitgleichung/neu.html
408 let dt = Utc.ymd(2005, 9, 30).and_hms(12, 0, 0);
409
410 // geo-pos near Frankfurt/Germany
411 let lat = 50.0;
412 let lon = 10.0;
413
414 let sunriseandset = calc_sunrise_and_set(dt, lat, lon).unwrap();
415
416 match sunriseandset {
417 SunriseAndSet::PolarDay => assert!(false),
418 SunriseAndSet::PolarNight => assert!(false),
419 SunriseAndSet::Daylight(sunrise, sunset) => {
420 assert_eq!(sunrise.year(), 2005);
421 assert_eq!(sunrise.month(), 9);
422 assert_eq!(sunrise.day(), 30);
423 assert_eq!(sunrise.hour(), 5);
424 assert_eq!(sunrise.minute(), 17);
425
426 assert_eq!(sunset.year(), 2005);
427 assert_eq!(sunset.month(), 9);
428 assert_eq!(sunset.day(), 30);
429 assert_eq!(sunset.hour(), 16);
430 assert_eq!(sunset.minute(), 59);
431 }
432 }
433 }
434
435 #[test]
test_calc_sunrise_and_set_polarday()436 fn test_calc_sunrise_and_set_polarday() {
437 // test-vector from http://lexikon.astronomie.info/zeitgleichung/neu.html
438 let dt = Utc.ymd(2005, 6, 30).and_hms(12, 0, 0);
439
440 // geo-pos in northern polar region
441 let lat = 67.4;
442 let lon = 10.0;
443
444 let sunriseandset = calc_sunrise_and_set(dt, lat, lon).unwrap();
445
446 match sunriseandset {
447 SunriseAndSet::PolarDay => assert!(true),
448 SunriseAndSet::PolarNight => assert!(false),
449 _ => assert!(false),
450 }
451 }
452
453 #[test]
test_calc_sunrise_and_set_polarnight()454 fn test_calc_sunrise_and_set_polarnight() {
455 // test-vector from http://lexikon.astronomie.info/zeitgleichung/neu.html
456 let dt = Utc.ymd(2005, 6, 30).and_hms(12, 0, 0);
457
458 // geo-pos in southern polar region
459 let lat = -68.0;
460 let lon = 10.0;
461
462 let sunriseandset = calc_sunrise_and_set(dt, lat, lon).unwrap();
463
464 match sunriseandset {
465 SunriseAndSet::PolarDay => assert!(false),
466 SunriseAndSet::PolarNight => assert!(true),
467 _ => assert!(false),
468 }
469 }
470
471 #[test]
test_calc_solar_position()472 fn test_calc_solar_position() {
473 // test-vector from http://lexikon.astronomie.info/zeitgleichung/neu.html
474 let exp_azimuth = 195.51003782406534;
475 let exp_zenith_angle = 54.03653683638118;
476
477 let dt = Utc.ymd(2005, 9, 30).and_hms(12, 0, 0);
478
479 // geo-pos near Frankfurt/Germany
480 let lat = 50.0;
481 let lon = 10.0;
482
483 let solpos = calc_solar_position(dt, lat, lon).unwrap();
484
485 assert_eq!(exp_azimuth, solpos.azimuth);
486 assert_eq!(exp_zenith_angle, solpos.zenith_angle);
487 }
488 }
489
490 /// # Benchmarks
491 /// Execute the benchmarks entering command:
492 /// ```commandline
493 /// cargo bench --features benchmark
494 /// ```
495 #[cfg(all(feature = "benchmark", test))]
496 mod benchmark {
497 extern crate chrono;
498 extern crate test;
499
500 use self::chrono::prelude::Utc;
501 use self::chrono::TimeZone;
502 use super::calc_solar_position;
503 use super::calc_sunrise_and_set;
504 use std::time::SystemTime;
505
506 #[bench]
bench_reference_read_systime(b: &mut test::Bencher)507 fn bench_reference_read_systime(b: &mut test::Bencher) {
508 b.iter(|| {
509 // any random value, tp prevent the code is discarded by code-optimizer
510 let now: u64 = SystemTime::now().elapsed().unwrap().as_secs();
511
512 // return the value to prevent the code is marked as unused and discarded by optimizer
513 now
514 });
515 }
516
517 #[bench]
bench_calc_solar_position(b: &mut test::Bencher)518 fn bench_calc_solar_position(b: &mut test::Bencher) {
519 b.iter(|| {
520 // test-vector from http://lexikon.astronomie.info/zeitgleichung/neu.html
521 let exp_azimuth = 195.51003782406534;
522 let exp_zenith_angle = 54.03653683638118;
523
524 let dt = Utc.ymd(2005, 9, 30).and_hms(12, 0, 0);
525
526 // geo-pos near Frankfurt/Germany
527 let lat = 50.0;
528 let lon = 10.0;
529
530 let solpos = calc_solar_position(dt, lat, lon).unwrap();
531
532 assert_eq!(exp_azimuth, solpos.azimuth);
533 assert_eq!(exp_zenith_angle, solpos.zenith_angle);
534
535 // return the value to prevent the code is marked as unused and discarded by optimizer
536 solpos
537 });
538 }
539
540 #[bench]
bench_calc_sunrise_and_set(b: &mut test::Bencher)541 fn bench_calc_sunrise_and_set(b: &mut test::Bencher) {
542 b.iter(|| {
543 // test-vector from http://lexikon.astronomie.info/zeitgleichung/neu.html
544 let dt = Utc.ymd(2005, 9, 30).and_hms(12, 0, 0);
545
546 // geo-pos near Frankfurt/Germany
547 let lat = 50.0;
548 let lon = 10.0;
549
550 let sunriseandset = calc_sunrise_and_set(dt, lat, lon).unwrap();
551
552 // return the value to prevent the code is marked as unused and discarded by optimizer
553 sunriseandset
554 });
555 }
556 }
557