1#![allow(clippy::similar_names)]
10#![allow(clippy::many_single_char_names)]
11#![allow(clippy::unreadable_literal)]
12
13use crate::error::check_coordinates;
14use crate::math::{
15 acos, asin, atan, atan2, cos, degrees_to_radians, floor, mul_add, normalize_degrees_0_to_360,
16 polynomial, powi, radians_to_degrees, sin, tan,
17};
18use crate::time::JulianDate;
19#[cfg(feature = "chrono")]
20use crate::Horizon;
21use crate::{RefractionCorrection, Result, SolarPosition};
22
23pub mod coefficients;
24use coefficients::{
25 NUTATION_COEFFS, OBLIQUITY_COEFFS, TERMS_B, TERMS_L, TERMS_PE, TERMS_R, TERMS_Y,
26};
27
28#[cfg(feature = "chrono")]
29use chrono::{offset::Offset, DateTime, Datelike, NaiveDate, TimeZone};
30
31const SUNRISE_SUNSET_ANGLE: f64 = -0.83337;
33
34const ABERRATION_CONSTANT: f64 = -20.4898;
36
37const EARTH_FLATTENING_FACTOR: f64 = 0.99664719;
39
40const EARTH_RADIUS_METERS: f64 = 6378140.0;
42
43const SECONDS_PER_HOUR: f64 = 3600.0;
45
46#[cfg(feature = "chrono")]
93#[cfg_attr(docsrs, doc(cfg(feature = "chrono")))]
94#[allow(clippy::needless_pass_by_value)]
95pub fn solar_position<Tz: TimeZone>(
96 datetime: DateTime<Tz>,
97 latitude: f64,
98 longitude: f64,
99 elevation: f64,
100 delta_t: f64,
101 refraction: Option<RefractionCorrection>,
102) -> Result<SolarPosition> {
103 let jd = JulianDate::from_datetime(&datetime, delta_t)?;
104 solar_position_from_julian(jd, latitude, longitude, elevation, refraction)
105}
106
107pub fn solar_position_from_julian(
143 jd: JulianDate,
144 latitude: f64,
145 longitude: f64,
146 elevation: f64,
147 refraction: Option<RefractionCorrection>,
148) -> Result<SolarPosition> {
149 let time_dependent = spa_time_dependent_from_julian(jd)?;
150 spa_with_time_dependent_parts(latitude, longitude, elevation, refraction, &time_dependent)
151}
152
153#[derive(Debug, Clone)]
158pub struct SpaTimeDependent {
159 pub(crate) r: f64,
161 pub(crate) nu_degrees: f64,
163 pub(crate) alpha_degrees: f64,
165 pub(crate) delta_degrees: f64,
167}
168
169#[derive(Debug, Clone, Copy)]
170struct DeltaPsiEpsilon {
171 delta_psi: f64,
172 delta_epsilon: f64,
173}
174
175fn calculate_lbr_terms(jme: f64, term_coeffs: &[&[&[f64; 3]]]) -> [f64; 6] {
177 let mut lbr_terms = [0.0; 6];
180
181 for (i, term_set) in term_coeffs.iter().enumerate().take(6) {
182 let mut lbr_sum = 0.0;
183 for term in *term_set {
184 lbr_sum += term[0] * cos(mul_add(term[2], jme, term[1]));
185 }
186 lbr_terms[i] = lbr_sum;
187 }
188
189 lbr_terms
190}
191
192fn calculate_lbr_polynomial(jme: f64, terms: &[f64], num_terms: usize) -> f64 {
194 polynomial(&terms[..num_terms], jme) / 1e8
195}
196
197fn lbr_to_normalized_degrees(jme: f64, terms: &[f64], num_terms: usize) -> f64 {
199 normalize_degrees_0_to_360(radians_to_degrees(calculate_lbr_polynomial(
200 jme, terms, num_terms,
201 )))
202}
203
204fn calculate_nutation_terms(jce: f64) -> [f64; 5] {
206 [
209 polynomial(NUTATION_COEFFS[0], jce),
210 polynomial(NUTATION_COEFFS[1], jce),
211 polynomial(NUTATION_COEFFS[2], jce),
212 polynomial(NUTATION_COEFFS[3], jce),
213 polynomial(NUTATION_COEFFS[4], jce),
214 ]
215}
216
217fn calculate_delta_psi_epsilon(jce: f64, x: &[f64]) -> DeltaPsiEpsilon {
219 let mut delta_psi = 0.0;
220 let mut delta_epsilon = 0.0;
221
222 for (i, pe_term) in TERMS_PE.iter().enumerate() {
223 let xj_yterm_sum = degrees_to_radians(calculate_xj_yterm_sum(i, x));
224
225 let delta_psi_contrib = mul_add(pe_term[1], jce, pe_term[0]) * sin(xj_yterm_sum);
227 let delta_epsilon_contrib = mul_add(pe_term[3], jce, pe_term[2]) * cos(xj_yterm_sum);
228
229 delta_psi += delta_psi_contrib;
230 delta_epsilon += delta_epsilon_contrib;
231 }
232
233 DeltaPsiEpsilon {
234 delta_psi: delta_psi / 36_000_000.0,
235 delta_epsilon: delta_epsilon / 36_000_000.0,
236 }
237}
238
239fn calculate_xj_yterm_sum(i: usize, x: &[f64]) -> f64 {
241 let mut sum = 0.0;
242 for (j, &x_val) in x.iter().enumerate() {
243 sum += x_val * f64::from(TERMS_Y[i][j]);
244 }
245 sum
246}
247
248fn calculate_true_obliquity_of_ecliptic(jd: &JulianDate, delta_epsilon: f64) -> f64 {
250 let epsilon0 = polynomial(OBLIQUITY_COEFFS, jd.julian_ephemeris_millennium() / 10.0);
251 epsilon0 / 3600.0 + delta_epsilon
252}
253
254fn calculate_apparent_sidereal_time_at_greenwich(
256 jd: &JulianDate,
257 delta_psi: f64,
258 epsilon_degrees: f64,
259) -> f64 {
260 let nu0_degrees = normalize_degrees_0_to_360(mul_add(
261 powi(jd.julian_century(), 2),
262 0.000387933 - jd.julian_century() / 38710000.0,
263 mul_add(
264 360.98564736629f64,
265 jd.julian_date() - 2451545.0,
266 280.46061837,
267 ),
268 ));
269
270 mul_add(
271 delta_psi,
272 cos(degrees_to_radians(epsilon_degrees)),
273 nu0_degrees,
274 )
275}
276
277fn calculate_geocentric_sun_right_ascension(
279 beta_rad: f64,
280 epsilon_rad: f64,
281 lambda_rad: f64,
282) -> f64 {
283 let alpha = atan2(
284 mul_add(
285 sin(lambda_rad),
286 cos(epsilon_rad),
287 -(tan(beta_rad) * sin(epsilon_rad)),
288 ),
289 cos(lambda_rad),
290 );
291 normalize_degrees_0_to_360(radians_to_degrees(alpha))
292}
293
294fn calculate_geocentric_sun_declination(beta_rad: f64, epsilon_rad: f64, lambda_rad: f64) -> f64 {
296 asin(mul_add(
297 sin(beta_rad),
298 cos(epsilon_rad),
299 cos(beta_rad) * sin(epsilon_rad) * sin(lambda_rad),
300 ))
301}
302
303pub fn sunrise_sunset_utc(
344 year: i32,
345 month: u32,
346 day: u32,
347 latitude: f64,
348 longitude: f64,
349 delta_t: f64,
350 elevation_angle: f64,
351) -> Result<crate::SunriseResult<crate::HoursUtc>> {
352 check_coordinates(latitude, longitude)?;
353
354 let jd_midnight = JulianDate::from_utc(year, month, day, 0, 0, 0.0, delta_t)?;
356
357 calculate_sunrise_sunset_core(jd_midnight, latitude, longitude, delta_t, elevation_angle)
359}
360
361pub fn sunrise_sunset_utc_for_horizon(
402 year: i32,
403 month: u32,
404 day: u32,
405 latitude: f64,
406 longitude: f64,
407 delta_t: f64,
408 horizon: crate::Horizon,
409) -> Result<crate::SunriseResult<crate::HoursUtc>> {
410 sunrise_sunset_utc(
411 year,
412 month,
413 day,
414 latitude,
415 longitude,
416 delta_t,
417 horizon.elevation_angle(),
418 )
419}
420
421#[cfg(feature = "chrono")]
464#[cfg_attr(docsrs, doc(cfg(feature = "chrono")))]
465#[allow(clippy::needless_pass_by_value)]
466pub fn sunrise_sunset<Tz: TimeZone>(
467 date: DateTime<Tz>,
468 latitude: f64,
469 longitude: f64,
470 delta_t: f64,
471 elevation_angle: f64,
472) -> Result<crate::SunriseResult<DateTime<Tz>>> {
473 check_coordinates(latitude, longitude)?;
474
475 let tz = date.timezone();
476 let local_date = date.date_naive();
477 let base_utc_date_guess = utc_date_for_local_calendar_day(&date);
478 let (_base_utc_date, result) =
479 select_utc_date_by_transit(local_date, base_utc_date_guess, |d| {
480 let hours_result = sunrise_sunset_utc(
481 d.year(),
482 d.month(),
483 d.day(),
484 latitude,
485 longitude,
486 delta_t,
487 elevation_angle,
488 )?;
489
490 let converted = match hours_result {
491 crate::SunriseResult::RegularDay {
492 sunrise,
493 transit,
494 sunset,
495 } => crate::SunriseResult::RegularDay {
496 sunrise: hours_utc_to_datetime(&tz, d, sunrise),
497 transit: hours_utc_to_datetime(&tz, d, transit),
498 sunset: hours_utc_to_datetime(&tz, d, sunset),
499 },
500 crate::SunriseResult::AllDay { transit } => crate::SunriseResult::AllDay {
501 transit: hours_utc_to_datetime(&tz, d, transit),
502 },
503 crate::SunriseResult::AllNight { transit } => crate::SunriseResult::AllNight {
504 transit: hours_utc_to_datetime(&tz, d, transit),
505 },
506 };
507
508 let transit_local_date = match &converted {
509 crate::SunriseResult::RegularDay { transit, .. }
510 | crate::SunriseResult::AllDay { transit }
511 | crate::SunriseResult::AllNight { transit } => transit.date_naive(),
512 };
513
514 Ok((transit_local_date, converted))
515 })?;
516
517 Ok(ensure_events_bracket_transit(result))
518}
519
520fn precompute_sunrise_sunset_for_jd_midnight(jd_midnight: JulianDate) -> (f64, [AlphaDelta; 3]) {
522 let jce_day = jd_midnight.julian_ephemeris_century();
524 let x_terms = calculate_nutation_terms(jce_day);
525 let delta_psi_epsilon = calculate_delta_psi_epsilon(jce_day, &x_terms);
526 let epsilon_degrees =
527 calculate_true_obliquity_of_ecliptic(&jd_midnight, delta_psi_epsilon.delta_epsilon);
528 let nu_degrees = calculate_apparent_sidereal_time_at_greenwich(
529 &jd_midnight,
530 delta_psi_epsilon.delta_psi,
531 epsilon_degrees,
532 );
533
534 let mut alpha_deltas = [AlphaDelta {
536 alpha: 0.0,
537 delta: 0.0,
538 }; 3];
539 for (i, alpha_delta) in alpha_deltas.iter_mut().enumerate() {
540 let current_jd = jd_midnight.add_days((i as f64) - 1.0);
541 let current_jme = current_jd.julian_ephemeris_millennium();
542 *alpha_delta =
543 calculate_alpha_delta(current_jme, delta_psi_epsilon.delta_psi, epsilon_degrees);
544 }
545
546 (nu_degrees, alpha_deltas)
547}
548
549fn calculate_sunrise_sunset_hours_with_precomputed(
550 latitude: f64,
551 longitude: f64,
552 delta_t: f64,
553 elevation_angle: f64,
554 nu_degrees: f64,
555 alpha_deltas: [AlphaDelta; 3],
556) -> crate::SunriseResult<crate::HoursUtc> {
557 let m0 = (alpha_deltas[1].alpha - longitude - nu_degrees) / 360.0;
559 let polar_type = check_polar_conditions_type(latitude, elevation_angle, alpha_deltas[1].delta);
560
561 let (m_values, _h0_degrees) =
563 calculate_approximate_times(m0, latitude, elevation_angle, alpha_deltas[1].delta);
564
565 let (t_frac, r_frac, s_frac) = calculate_final_time_fractions(
567 m_values,
568 nu_degrees,
569 delta_t,
570 latitude,
571 longitude,
572 elevation_angle,
573 alpha_deltas,
574 );
575
576 let transit_hours = crate::HoursUtc::from_hours(t_frac * 24.0);
578 let sunrise_hours = crate::HoursUtc::from_hours(r_frac * 24.0);
579 let sunset_hours = crate::HoursUtc::from_hours(s_frac * 24.0);
580
581 match polar_type {
583 Some(PolarType::AllDay) => crate::SunriseResult::AllDay {
584 transit: transit_hours,
585 },
586 Some(PolarType::AllNight) => crate::SunriseResult::AllNight {
587 transit: transit_hours,
588 },
589 None => crate::SunriseResult::RegularDay {
590 sunrise: sunrise_hours,
591 transit: transit_hours,
592 sunset: sunset_hours,
593 },
594 }
595}
596
597#[allow(clippy::unnecessary_wraps)]
601fn calculate_sunrise_sunset_core(
602 jd_midnight: JulianDate,
603 latitude: f64,
604 longitude: f64,
605 delta_t: f64,
606 elevation_angle: f64,
607) -> Result<crate::SunriseResult<crate::HoursUtc>> {
608 let (nu_degrees, alpha_deltas) = precompute_sunrise_sunset_for_jd_midnight(jd_midnight);
609 Ok(calculate_sunrise_sunset_hours_with_precomputed(
610 latitude,
611 longitude,
612 delta_t,
613 elevation_angle,
614 nu_degrees,
615 alpha_deltas,
616 ))
617}
618
619#[derive(Debug, Clone, Copy)]
626enum PolarType {
627 AllDay,
628 AllNight,
629}
630
631fn check_polar_conditions_type(
633 latitude: f64,
634 elevation_angle: f64,
635 delta1: f64,
636) -> Option<PolarType> {
637 let phi = degrees_to_radians(latitude);
638 let elevation_rad = degrees_to_radians(elevation_angle);
639 let delta1_rad = degrees_to_radians(delta1);
640
641 let acos_arg =
642 mul_add(sin(phi), -sin(delta1_rad), sin(elevation_rad)) / (cos(phi) * cos(delta1_rad));
643
644 if acos_arg < -1.0 {
645 Some(PolarType::AllDay)
646 } else if acos_arg > 1.0 {
647 Some(PolarType::AllNight)
648 } else {
649 None
650 }
651}
652
653fn calculate_approximate_times(
655 m0: f64,
656 latitude: f64,
657 elevation_angle: f64,
658 delta1: f64,
659) -> ([f64; 3], f64) {
660 let phi = degrees_to_radians(latitude);
661 let delta1_rad = degrees_to_radians(delta1);
662 let elevation_rad = degrees_to_radians(elevation_angle);
663
664 let acos_arg =
665 mul_add(sin(phi), -sin(delta1_rad), sin(elevation_rad)) / (cos(phi) * cos(delta1_rad));
666 let h0 = acos(acos_arg);
667 let h0_degrees = radians_to_degrees(h0).min(180.0);
668
669 let mut m = [0.0; 3];
670 m[0] = normalize_to_unit_range(m0);
671 m[1] = normalize_to_unit_range(m0 - h0_degrees / 360.0);
672 m[2] = normalize_to_unit_range(m0 + h0_degrees / 360.0);
673
674 (m, h0_degrees)
675}
676
677fn calculate_final_time_fractions(
680 m_values: [f64; 3],
681 nu_degrees: f64,
682 delta_t: f64,
683 latitude: f64,
684 longitude: f64,
685 elevation_angle: f64,
686 alpha_deltas: [AlphaDelta; 3],
687) -> (f64, f64, f64) {
688 let mut nu = [0.0; 3];
690 for (i, nu_item) in nu.iter_mut().enumerate() {
691 *nu_item = mul_add(360.985647f64, m_values[i], nu_degrees);
692 }
693
694 let mut n = [0.0; 3];
696 for (i, n_item) in n.iter_mut().enumerate() {
697 *n_item = m_values[i] + delta_t / 86400.0;
698 }
699
700 let alpha_delta_primes = calculate_interpolated_alpha_deltas(&alpha_deltas, &n);
702
703 let mut h_prime = [0.0; 3];
705 for i in 0..3 {
706 let h_prime_i = nu[i] + longitude - alpha_delta_primes[i].alpha;
707 h_prime[i] = limit_h_prime(h_prime_i);
708 }
709
710 let phi = degrees_to_radians(latitude);
712 let mut h = [0.0; 3];
713 for i in 0..3 {
714 let delta_prime_rad = degrees_to_radians(alpha_delta_primes[i].delta);
715 h[i] = radians_to_degrees(asin(mul_add(
716 sin(phi),
717 sin(delta_prime_rad),
718 cos(phi) * cos(delta_prime_rad) * cos(degrees_to_radians(h_prime[i])),
719 )));
720 }
721
722 let t = m_values[0] - h_prime[0] / 360.0;
724 let r = m_values[1]
725 + (h[1] - elevation_angle)
726 / (360.0
727 * cos(degrees_to_radians(alpha_delta_primes[1].delta))
728 * cos(phi)
729 * sin(degrees_to_radians(h_prime[1])));
730 let s = m_values[2]
731 + (h[2] - elevation_angle)
732 / (360.0
733 * cos(degrees_to_radians(alpha_delta_primes[2].delta))
734 * cos(phi)
735 * sin(degrees_to_radians(h_prime[2])));
736
737 (t, r, s)
738}
739
740fn calculate_interpolated_alpha_deltas(
742 alpha_deltas: &[AlphaDelta; 3],
743 n: &[f64; 3],
744) -> [AlphaDelta; 3] {
745 let a = limit_if_necessary(alpha_deltas[1].alpha - alpha_deltas[0].alpha);
746 let a_prime = limit_if_necessary(alpha_deltas[1].delta - alpha_deltas[0].delta);
747
748 let b = limit_if_necessary(alpha_deltas[2].alpha - alpha_deltas[1].alpha);
749 let b_prime = limit_if_necessary(alpha_deltas[2].delta - alpha_deltas[1].delta);
750
751 let c = b - a;
752 let c_prime = b_prime - a_prime;
753
754 let mut alpha_delta_primes = [AlphaDelta {
755 alpha: 0.0,
756 delta: 0.0,
757 }; 3];
758 for i in 0..3 {
759 alpha_delta_primes[i].alpha =
760 alpha_deltas[1].alpha + (n[i] * (mul_add(c, n[i], a + b))) / 2.0;
761 alpha_delta_primes[i].delta =
762 alpha_deltas[1].delta + (n[i] * (mul_add(c_prime, n[i], a_prime + b_prime))) / 2.0;
763 }
764 alpha_delta_primes
765}
766
767#[derive(Debug, Clone, Copy)]
768struct AlphaDelta {
769 alpha: f64,
770 delta: f64,
771}
772
773#[cfg(feature = "chrono")]
817#[cfg_attr(docsrs, doc(cfg(feature = "chrono")))]
818pub fn sunrise_sunset_for_horizon<Tz: TimeZone>(
819 date: DateTime<Tz>,
820 latitude: f64,
821 longitude: f64,
822 delta_t: f64,
823 horizon: Horizon,
824) -> Result<crate::SunriseResult<DateTime<Tz>>> {
825 sunrise_sunset(
826 date,
827 latitude,
828 longitude,
829 delta_t,
830 horizon.elevation_angle(),
831 )
832}
833
834fn calculate_alpha_delta(jme: f64, delta_psi: f64, epsilon_degrees: f64) -> AlphaDelta {
837 let b_terms = calculate_lbr_terms(jme, TERMS_B);
841 let b_degrees = lbr_to_normalized_degrees(jme, &b_terms, TERMS_B.len());
842
843 let r_terms = calculate_lbr_terms(jme, TERMS_R);
845 let r = calculate_lbr_polynomial(jme, &r_terms, TERMS_R.len());
846 assert!(
847 r != 0.0,
848 "Earth radius vector is zero - astronomical impossibility"
849 );
850
851 let l_terms = calculate_lbr_terms(jme, TERMS_L);
853 let l_degrees = lbr_to_normalized_degrees(jme, &l_terms, TERMS_L.len());
854
855 let theta_degrees = normalize_degrees_0_to_360(l_degrees + 180.0);
857
858 let beta_degrees = -b_degrees;
860 let beta = degrees_to_radians(beta_degrees);
861 let epsilon = degrees_to_radians(epsilon_degrees);
862
863 let delta_tau = ABERRATION_CONSTANT / (SECONDS_PER_HOUR * r);
865
866 let lambda_degrees = theta_degrees + delta_psi + delta_tau;
868 let lambda = degrees_to_radians(lambda_degrees);
869
870 let alpha_degrees = calculate_geocentric_sun_right_ascension(beta, epsilon, lambda);
872 let delta_degrees =
873 radians_to_degrees(calculate_geocentric_sun_declination(beta, epsilon, lambda));
874
875 AlphaDelta {
876 alpha: alpha_degrees,
877 delta: delta_degrees,
878 }
879}
880
881fn normalize_to_unit_range(val: f64) -> f64 {
883 let divided = val;
884 let limited = divided - floor(divided);
885 if limited < 0.0 {
886 limited + 1.0
887 } else {
888 limited
889 }
890}
891
892#[cfg(feature = "chrono")]
893fn utc_date_for_local_calendar_day<Tz: TimeZone>(datetime: &DateTime<Tz>) -> NaiveDate {
894 datetime.date_naive()
899}
900
901#[cfg(feature = "chrono")]
902fn select_utc_date_by_transit<V, F>(
903 local_date: NaiveDate,
904 mut utc_date: NaiveDate,
905 mut compute: F,
906) -> Result<(NaiveDate, V)>
907where
908 F: FnMut(NaiveDate) -> Result<(NaiveDate, V)>,
909{
910 let mut last = None;
911 let mut last_transit = None;
912 for _ in 0..2 {
913 let (transit_local_date, value) = compute(utc_date)?;
914 last = Some(value);
915 last_transit = Some(transit_local_date);
916 if transit_local_date == local_date {
917 break;
918 }
919
920 utc_date = if transit_local_date > local_date {
921 utc_date.pred_opt().unwrap_or(utc_date)
922 } else {
923 utc_date.succ_opt().unwrap_or(utc_date)
924 };
925 }
926
927 debug_assert_eq!(last_transit, Some(local_date));
928 Ok((utc_date, last.expect("loop runs at least once")))
929}
930
931#[cfg(feature = "chrono")]
932fn hours_utc_to_datetime<Tz: TimeZone>(
933 tz: &Tz,
934 base_utc_date: NaiveDate,
935 hours: crate::HoursUtc,
936) -> DateTime<Tz> {
937 let base_utc_midnight = base_utc_date
938 .and_hms_opt(0, 0, 0)
939 .expect("midnight is always valid")
940 .and_utc();
941
942 let millis_plus = (hours.hours() * 3_600_000.0) as i64;
945 let utc_dt = base_utc_midnight + chrono::Duration::milliseconds(millis_plus);
946
947 tz.from_utc_datetime(&utc_dt.naive_utc())
948}
949
950#[cfg(feature = "chrono")]
951fn ensure_events_bracket_transit<Tz: TimeZone>(
952 result: crate::SunriseResult<DateTime<Tz>>,
953) -> crate::SunriseResult<DateTime<Tz>> {
954 let crate::SunriseResult::RegularDay {
955 mut sunrise,
956 transit,
957 mut sunset,
958 } = result
959 else {
960 return result;
961 };
962
963 if transit.offset().fix().local_minus_utc() == 0 {
967 return crate::SunriseResult::RegularDay {
968 sunrise,
969 transit,
970 sunset,
971 };
972 }
973
974 if sunrise > transit {
976 sunrise -= chrono::Duration::days(1);
977 }
978
979 if sunset < transit {
980 sunset += chrono::Duration::days(1);
981 }
982
983 crate::SunriseResult::RegularDay {
984 sunrise,
985 transit,
986 sunset,
987 }
988}
989
990fn limit_if_necessary(val: f64) -> f64 {
992 if val.abs() > 2.0 {
993 normalize_to_unit_range(val)
994 } else {
995 val
996 }
997}
998
999fn limit_h_prime(h_prime: f64) -> f64 {
1001 let normalized = h_prime / 360.0;
1002 let limited = 360.0 * (normalized - floor(normalized));
1003
1004 if limited < -180.0 {
1005 limited + 360.0
1006 } else if limited > 180.0 {
1007 limited - 360.0
1008 } else {
1009 limited
1010 }
1011}
1012
1013#[cfg(feature = "chrono")]
1067#[cfg_attr(docsrs, doc(cfg(feature = "chrono")))]
1068#[allow(clippy::needless_pass_by_value)]
1069pub fn sunrise_sunset_multiple<Tz, H>(
1070 date: DateTime<Tz>,
1071 latitude: f64,
1072 longitude: f64,
1073 delta_t: f64,
1074 horizons: H,
1075) -> impl Iterator<Item = Result<(Horizon, crate::SunriseResult<DateTime<Tz>>)>>
1076where
1077 Tz: TimeZone,
1078 H: IntoIterator<Item = Horizon>,
1079{
1080 let tz = date.timezone();
1081 let local_date = date.date_naive();
1082 let base_utc_date_guess = utc_date_for_local_calendar_day(&date);
1083
1084 let precomputed = (|| -> Result<_> {
1086 check_coordinates(latitude, longitude)?;
1087 let (base_utc_date, (nu_degrees, alpha_deltas)) =
1088 select_utc_date_by_transit(local_date, base_utc_date_guess, |d| {
1089 let jd_midnight =
1090 JulianDate::from_utc(d.year(), d.month(), d.day(), 0, 0, 0.0, delta_t)?;
1091
1092 let (nu_degrees, alpha_deltas) =
1093 precompute_sunrise_sunset_for_jd_midnight(jd_midnight);
1094 let transit_hours = match calculate_sunrise_sunset_hours_with_precomputed(
1095 latitude,
1096 longitude,
1097 delta_t,
1098 Horizon::SunriseSunset.elevation_angle(),
1099 nu_degrees,
1100 alpha_deltas,
1101 ) {
1102 crate::SunriseResult::RegularDay { transit, .. }
1103 | crate::SunriseResult::AllDay { transit }
1104 | crate::SunriseResult::AllNight { transit } => transit,
1105 };
1106
1107 let transit_local_date = hours_utc_to_datetime(&tz, d, transit_hours).date_naive();
1108 Ok((transit_local_date, (nu_degrees, alpha_deltas)))
1109 })?;
1110
1111 Ok((base_utc_date, nu_degrees, alpha_deltas))
1112 })();
1113
1114 horizons.into_iter().map(move |horizon| {
1115 let (base_utc_date, nu_degrees, alpha_deltas) = precomputed.clone()?;
1116 let hours_result = calculate_sunrise_sunset_hours_with_precomputed(
1117 latitude,
1118 longitude,
1119 delta_t,
1120 horizon.elevation_angle(),
1121 nu_degrees,
1122 alpha_deltas,
1123 );
1124
1125 let result = match hours_result {
1126 crate::SunriseResult::RegularDay {
1127 sunrise,
1128 transit,
1129 sunset,
1130 } => crate::SunriseResult::RegularDay {
1131 sunrise: hours_utc_to_datetime(&tz, base_utc_date, sunrise),
1132 transit: hours_utc_to_datetime(&tz, base_utc_date, transit),
1133 sunset: hours_utc_to_datetime(&tz, base_utc_date, sunset),
1134 },
1135 crate::SunriseResult::AllDay { transit } => crate::SunriseResult::AllDay {
1136 transit: hours_utc_to_datetime(&tz, base_utc_date, transit),
1137 },
1138 crate::SunriseResult::AllNight { transit } => crate::SunriseResult::AllNight {
1139 transit: hours_utc_to_datetime(&tz, base_utc_date, transit),
1140 },
1141 };
1142
1143 let result = ensure_events_bracket_transit(result);
1144
1145 Ok((horizon, result))
1146 })
1147}
1148
1149#[cfg(feature = "chrono")]
1187#[cfg_attr(docsrs, doc(cfg(feature = "chrono")))]
1188#[allow(clippy::needless_pass_by_value)]
1189pub fn spa_time_dependent_parts<Tz: TimeZone>(
1190 datetime: DateTime<Tz>,
1191 delta_t: f64,
1192) -> Result<SpaTimeDependent> {
1193 let jd = JulianDate::from_datetime(&datetime, delta_t)?;
1194 spa_time_dependent_from_julian(jd)
1195}
1196
1197pub fn spa_time_dependent_from_julian(jd: JulianDate) -> Result<SpaTimeDependent> {
1207 let jme = jd.julian_ephemeris_millennium();
1208 let jce = jd.julian_ephemeris_century();
1209
1210 let l_terms = calculate_lbr_terms(jme, TERMS_L);
1212 let l_degrees = lbr_to_normalized_degrees(jme, &l_terms, TERMS_L.len());
1213
1214 let b_terms = calculate_lbr_terms(jme, TERMS_B);
1216 let b_degrees = lbr_to_normalized_degrees(jme, &b_terms, TERMS_B.len());
1217
1218 let r_terms = calculate_lbr_terms(jme, TERMS_R);
1220 let r = calculate_lbr_polynomial(jme, &r_terms, TERMS_R.len());
1221
1222 assert!(
1224 r != 0.0,
1225 "Earth radius vector is zero - astronomical impossibility"
1226 );
1227
1228 let theta_degrees = normalize_degrees_0_to_360(l_degrees + 180.0);
1230 let beta_degrees = -b_degrees;
1232
1233 let x_terms = calculate_nutation_terms(jce);
1235 let delta_psi_epsilon = calculate_delta_psi_epsilon(jce, &x_terms);
1236
1237 let epsilon_degrees =
1239 calculate_true_obliquity_of_ecliptic(&jd, delta_psi_epsilon.delta_epsilon);
1240
1241 let delta_tau = ABERRATION_CONSTANT / (SECONDS_PER_HOUR * r);
1243
1244 let lambda_degrees = theta_degrees + delta_psi_epsilon.delta_psi + delta_tau;
1246
1247 let nu_degrees = calculate_apparent_sidereal_time_at_greenwich(
1249 &jd,
1250 delta_psi_epsilon.delta_psi,
1251 epsilon_degrees,
1252 );
1253
1254 let beta = degrees_to_radians(beta_degrees);
1256 let epsilon = degrees_to_radians(epsilon_degrees);
1257 let lambda = degrees_to_radians(lambda_degrees);
1258 let alpha_degrees = calculate_geocentric_sun_right_ascension(beta, epsilon, lambda);
1259
1260 let delta_degrees =
1262 radians_to_degrees(calculate_geocentric_sun_declination(beta, epsilon, lambda));
1263
1264 Ok(SpaTimeDependent {
1265 r,
1266 nu_degrees,
1267 alpha_degrees,
1268 delta_degrees,
1269 })
1270}
1271
1272pub fn spa_with_time_dependent_parts(
1310 latitude: f64,
1311 longitude: f64,
1312 elevation: f64,
1313 refraction: Option<RefractionCorrection>,
1314 time_dependent: &SpaTimeDependent,
1315) -> Result<SolarPosition> {
1316 check_coordinates(latitude, longitude)?;
1317
1318 let nu_degrees = time_dependent.nu_degrees;
1321
1322 let h_degrees =
1324 normalize_degrees_0_to_360(nu_degrees + longitude - time_dependent.alpha_degrees);
1325 let h = degrees_to_radians(h_degrees);
1326
1327 let xi_degrees = 8.794 / (3600.0 * time_dependent.r);
1329 let xi = degrees_to_radians(xi_degrees);
1330 let phi = degrees_to_radians(latitude);
1331 let delta = degrees_to_radians(time_dependent.delta_degrees);
1332
1333 let u = atan(EARTH_FLATTENING_FACTOR * tan(phi));
1334 let y = mul_add(
1335 EARTH_FLATTENING_FACTOR,
1336 sin(u),
1337 (elevation / EARTH_RADIUS_METERS) * sin(phi),
1338 );
1339 let x = mul_add(elevation / EARTH_RADIUS_METERS, cos(phi), cos(u));
1340
1341 let delta_alpha_prime_degrees = radians_to_degrees(atan2(
1342 -x * sin(xi) * sin(h),
1343 mul_add(x * sin(xi), -cos(h), cos(delta)),
1344 ));
1345
1346 let delta_prime = radians_to_degrees(atan2(
1347 mul_add(y, -sin(xi), sin(delta)) * cos(degrees_to_radians(delta_alpha_prime_degrees)),
1348 mul_add(x * sin(xi), -cos(h), cos(delta)),
1349 ));
1350
1351 let h_prime_degrees = h_degrees - delta_alpha_prime_degrees;
1353
1354 let zenith_angle = radians_to_degrees(acos(mul_add(
1356 sin(degrees_to_radians(latitude)),
1357 sin(degrees_to_radians(delta_prime)),
1358 cos(degrees_to_radians(latitude))
1359 * cos(degrees_to_radians(delta_prime))
1360 * cos(degrees_to_radians(h_prime_degrees)),
1361 )));
1362
1363 let azimuth = normalize_degrees_0_to_360(
1365 180.0
1366 + radians_to_degrees(atan2(
1367 sin(degrees_to_radians(h_prime_degrees)),
1368 cos(degrees_to_radians(h_prime_degrees)) * sin(degrees_to_radians(latitude))
1369 - tan(degrees_to_radians(delta_prime)) * cos(degrees_to_radians(latitude)),
1370 )),
1371 );
1372
1373 let elevation_angle = 90.0 - zenith_angle;
1375 let final_zenith = refraction.map_or(zenith_angle, |correction| {
1376 if elevation_angle > SUNRISE_SUNSET_ANGLE {
1377 let pressure = correction.pressure();
1378 let temperature = correction.temperature();
1379 zenith_angle
1381 - (pressure / 1010.0) * (283.0 / (273.0 + temperature)) * 1.02
1382 / (60.0
1383 * tan(degrees_to_radians(
1384 elevation_angle + 10.3 / (elevation_angle + 5.11),
1385 )))
1386 } else {
1387 zenith_angle
1388 }
1389 });
1390
1391 SolarPosition::new(azimuth, final_zenith)
1392}
1393
1394#[cfg(all(test, feature = "chrono", feature = "std"))]
1395mod tests {
1396 use super::*;
1397 use chrono::{DateTime, FixedOffset};
1398 use std::collections::HashSet;
1399
1400 #[test]
1401 fn test_spa_basic_functionality() {
1402 let datetime = "2023-06-21T12:00:00Z"
1403 .parse::<DateTime<FixedOffset>>()
1404 .unwrap();
1405
1406 let result = solar_position(
1407 datetime,
1408 37.7749, -122.4194,
1410 0.0,
1411 69.0,
1412 Some(RefractionCorrection::new(1013.25, 15.0).unwrap()),
1413 );
1414
1415 assert!(result.is_ok());
1416 let position = result.unwrap();
1417 assert!(position.azimuth() >= 0.0 && position.azimuth() <= 360.0);
1418 assert!(position.zenith_angle() >= 0.0 && position.zenith_angle() <= 180.0);
1419 }
1420
1421 #[test]
1422 fn test_sunrise_sunset_multiple() {
1423 let datetime = "2023-06-21T12:00:00Z"
1424 .parse::<DateTime<FixedOffset>>()
1425 .unwrap();
1426 let horizons = [
1427 Horizon::SunriseSunset,
1428 Horizon::CivilTwilight,
1429 Horizon::NauticalTwilight,
1430 ];
1431
1432 let results: Result<Vec<_>> = sunrise_sunset_multiple(
1433 datetime,
1434 37.7749, -122.4194, 69.0, horizons.iter().copied(),
1438 )
1439 .collect();
1440
1441 let results = results.unwrap();
1442
1443 assert_eq!(results.len(), 3);
1445
1446 let returned_horizons: HashSet<_> = results.iter().map(|(h, _)| *h).collect();
1448 for expected_horizon in horizons {
1449 assert!(returned_horizons.contains(&expected_horizon));
1450 }
1451
1452 for (horizon, bulk_result) in &results {
1454 let individual_result =
1455 sunrise_sunset_for_horizon(datetime, 37.7749, -122.4194, 69.0, *horizon).unwrap();
1456
1457 match (&individual_result, bulk_result) {
1459 (
1460 crate::SunriseResult::RegularDay {
1461 sunrise: s1,
1462 transit: t1,
1463 sunset: ss1,
1464 },
1465 crate::SunriseResult::RegularDay {
1466 sunrise: s2,
1467 transit: t2,
1468 sunset: ss2,
1469 },
1470 ) => {
1471 assert_eq!(s1, s2);
1472 assert_eq!(t1, t2);
1473 assert_eq!(ss1, ss2);
1474 }
1475 (
1476 crate::SunriseResult::AllDay { transit: t1 },
1477 crate::SunriseResult::AllDay { transit: t2 },
1478 )
1479 | (
1480 crate::SunriseResult::AllNight { transit: t1 },
1481 crate::SunriseResult::AllNight { transit: t2 },
1482 ) => {
1483 assert_eq!(t1, t2);
1484 }
1485 _ => panic!("Bulk and individual results differ in type for {horizon:?}"),
1486 }
1487 }
1488 }
1489
1490 #[test]
1491 fn test_sunrise_sunset_multiple_polar_consistency() {
1492 let datetime = "2023-06-21T12:00:00Z"
1493 .parse::<DateTime<FixedOffset>>()
1494 .unwrap();
1495
1496 let individual = sunrise_sunset_for_horizon(
1497 datetime,
1498 80.0, 0.0,
1500 69.0,
1501 Horizon::SunriseSunset,
1502 )
1503 .unwrap();
1504
1505 let bulk_results: Result<Vec<_>> =
1506 sunrise_sunset_multiple(datetime, 80.0, 0.0, 69.0, [Horizon::SunriseSunset]).collect();
1507
1508 let (_, bulk) = bulk_results.unwrap().into_iter().next().unwrap();
1509
1510 match (bulk, individual) {
1511 (
1512 crate::SunriseResult::AllDay { transit: t1 },
1513 crate::SunriseResult::AllDay { transit: t2 },
1514 )
1515 | (
1516 crate::SunriseResult::AllNight { transit: t1 },
1517 crate::SunriseResult::AllNight { transit: t2 },
1518 ) => assert_eq!(t1, t2),
1519 _ => panic!("expected matching polar-day/night results between bulk and individual"),
1520 }
1521 }
1522
1523 #[test]
1524 fn test_spa_no_refraction() {
1525 let datetime = "2023-06-21T12:00:00Z"
1526 .parse::<DateTime<FixedOffset>>()
1527 .unwrap();
1528
1529 let result = solar_position(datetime, 37.7749, -122.4194, 0.0, 69.0, None);
1530
1531 assert!(result.is_ok());
1532 let position = result.unwrap();
1533 assert!(position.azimuth() >= 0.0 && position.azimuth() <= 360.0);
1534 assert!(position.zenith_angle() >= 0.0 && position.zenith_angle() <= 180.0);
1535 }
1536
1537 #[test]
1538 fn test_spa_coordinate_validation() {
1539 let datetime = "2023-06-21T12:00:00Z"
1540 .parse::<DateTime<FixedOffset>>()
1541 .unwrap();
1542
1543 assert!(solar_position(
1545 datetime,
1546 95.0,
1547 0.0,
1548 0.0,
1549 0.0,
1550 Some(RefractionCorrection::new(1013.25, 15.0).unwrap())
1551 )
1552 .is_err());
1553
1554 assert!(solar_position(
1556 datetime,
1557 0.0,
1558 185.0,
1559 0.0,
1560 0.0,
1561 Some(RefractionCorrection::new(1013.25, 15.0).unwrap())
1562 )
1563 .is_err());
1564 }
1565
1566 #[test]
1567 fn test_sunrise_sunset_basic() {
1568 let date = "2023-06-21T00:00:00Z"
1569 .parse::<DateTime<FixedOffset>>()
1570 .unwrap();
1571
1572 let result = sunrise_sunset(date, 37.7749, -122.4194, 69.0, -0.833);
1573 assert!(result.is_ok());
1574
1575 let result =
1576 sunrise_sunset_for_horizon(date, 37.7749, -122.4194, 69.0, Horizon::SunriseSunset);
1577 assert!(result.is_ok());
1578 }
1579
1580 #[test]
1581 fn test_horizon_enum() {
1582 assert_eq!(Horizon::SunriseSunset.elevation_angle(), -0.83337);
1583 assert_eq!(Horizon::CivilTwilight.elevation_angle(), -6.0);
1584 assert_eq!(Horizon::Custom(-10.5).elevation_angle(), -10.5);
1585 }
1586}