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