1#![allow(clippy::similar_names)]
10#![allow(clippy::many_single_char_names)]
11#![allow(clippy::unreadable_literal)]
12
13use crate::error::{check_coordinates, check_elevation_angle};
14use crate::math::{
15 acos, asin, atan, atan2, cos, degrees_to_radians, 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 check_elevation_angle(elevation_angle)?;
354
355 let jd_midnight = JulianDate::from_utc(year, month, day, 0, 0, 0.0, delta_t)?;
357
358 Ok(calculate_sunrise_sunset_core(
360 jd_midnight,
361 latitude,
362 longitude,
363 delta_t,
364 elevation_angle,
365 ))
366}
367
368pub fn sunrise_sunset_utc_for_horizon(
410 year: i32,
411 month: u32,
412 day: u32,
413 latitude: f64,
414 longitude: f64,
415 delta_t: f64,
416 horizon: crate::Horizon,
417) -> Result<crate::SunriseResult<crate::HoursUtc>> {
418 sunrise_sunset_utc(
419 year,
420 month,
421 day,
422 latitude,
423 longitude,
424 delta_t,
425 horizon.elevation_angle(),
426 )
427}
428
429#[cfg(feature = "chrono")]
474#[cfg_attr(docsrs, doc(cfg(feature = "chrono")))]
475#[allow(clippy::needless_pass_by_value)]
476pub fn sunrise_sunset<Tz: TimeZone>(
477 date: DateTime<Tz>,
478 latitude: f64,
479 longitude: f64,
480 delta_t: f64,
481 elevation_angle: f64,
482) -> Result<crate::SunriseResult<DateTime<Tz>>> {
483 check_coordinates(latitude, longitude)?;
484
485 let tz = date.timezone();
486 let local_date = date.date_naive();
487 let base_utc_date_guess = date.date_naive();
491 let (_base_utc_date, result) =
492 select_utc_date_by_transit(local_date, base_utc_date_guess, |d| {
493 let hours_result = sunrise_sunset_utc(
494 d.year(),
495 d.month(),
496 d.day(),
497 latitude,
498 longitude,
499 delta_t,
500 elevation_angle,
501 )?;
502
503 let converted = match hours_result {
504 crate::SunriseResult::RegularDay {
505 sunrise,
506 transit,
507 sunset,
508 } => crate::SunriseResult::RegularDay {
509 sunrise: hours_utc_to_datetime(&tz, d, sunrise),
510 transit: hours_utc_to_datetime(&tz, d, transit),
511 sunset: hours_utc_to_datetime(&tz, d, sunset),
512 },
513 crate::SunriseResult::AllDay { transit } => crate::SunriseResult::AllDay {
514 transit: hours_utc_to_datetime(&tz, d, transit),
515 },
516 crate::SunriseResult::AllNight { transit } => crate::SunriseResult::AllNight {
517 transit: hours_utc_to_datetime(&tz, d, transit),
518 },
519 };
520
521 let transit_local_date = match &converted {
522 crate::SunriseResult::RegularDay { transit, .. }
523 | crate::SunriseResult::AllDay { transit }
524 | crate::SunriseResult::AllNight { transit } => transit.date_naive(),
525 };
526
527 Ok((transit_local_date, converted))
528 })?;
529
530 Ok(ensure_events_bracket_transit(result))
531}
532
533fn precompute_sunrise_sunset_for_jd_midnight(jd_midnight: JulianDate) -> (f64, [AlphaDelta; 3]) {
535 let jce_day = jd_midnight.julian_ephemeris_century();
537 let x_terms = calculate_nutation_terms(jce_day);
538 let delta_psi_epsilon = calculate_delta_psi_epsilon(jce_day, &x_terms);
539 let epsilon_degrees =
540 calculate_true_obliquity_of_ecliptic(&jd_midnight, delta_psi_epsilon.delta_epsilon);
541 let nu_degrees = calculate_apparent_sidereal_time_at_greenwich(
542 &jd_midnight,
543 delta_psi_epsilon.delta_psi,
544 epsilon_degrees,
545 );
546
547 let mut alpha_deltas = [AlphaDelta {
549 alpha: 0.0,
550 delta: 0.0,
551 }; 3];
552 for (i, alpha_delta) in alpha_deltas.iter_mut().enumerate() {
553 let current_jd = jd_midnight.add_days((i as f64) - 1.0);
554 let current_jme = current_jd.julian_ephemeris_millennium();
555 let current_jce = current_jd.julian_ephemeris_century();
556 let current_x_terms = calculate_nutation_terms(current_jce);
557 let current_delta_psi_epsilon = calculate_delta_psi_epsilon(current_jce, ¤t_x_terms);
558 let current_epsilon_degrees = calculate_true_obliquity_of_ecliptic(
559 ¤t_jd,
560 current_delta_psi_epsilon.delta_epsilon,
561 );
562 *alpha_delta = calculate_alpha_delta(
563 current_jme,
564 current_delta_psi_epsilon.delta_psi,
565 current_epsilon_degrees,
566 );
567 }
568
569 (nu_degrees, alpha_deltas)
570}
571
572fn calculate_sunrise_sunset_hours_with_precomputed(
573 latitude: f64,
574 longitude: f64,
575 delta_t: f64,
576 elevation_angle: f64,
577 nu_degrees: f64,
578 alpha_deltas: [AlphaDelta; 3],
579) -> crate::SunriseResult<crate::HoursUtc> {
580 let m0 = (alpha_deltas[1].alpha - longitude - nu_degrees) / 360.0;
582 let polar_type = check_polar_conditions_type(latitude, elevation_angle, alpha_deltas[1].delta);
583
584 let m_values =
586 calculate_approximate_times(m0, latitude, elevation_angle, alpha_deltas[1].delta);
587
588 let (t_frac, r_frac, s_frac) = calculate_final_time_fractions(
590 m_values,
591 nu_degrees,
592 delta_t,
593 latitude,
594 longitude,
595 elevation_angle,
596 alpha_deltas,
597 );
598
599 let transit_hours = crate::HoursUtc::from_hours(t_frac * 24.0);
601 let sunrise_hours = crate::HoursUtc::from_hours(r_frac * 24.0);
602 let sunset_hours = crate::HoursUtc::from_hours(s_frac * 24.0);
603
604 match polar_type {
606 Some(PolarType::AllDay) => crate::SunriseResult::AllDay {
607 transit: transit_hours,
608 },
609 Some(PolarType::AllNight) => crate::SunriseResult::AllNight {
610 transit: transit_hours,
611 },
612 None => crate::SunriseResult::RegularDay {
613 sunrise: sunrise_hours,
614 transit: transit_hours,
615 sunset: sunset_hours,
616 },
617 }
618}
619
620fn calculate_sunrise_sunset_core(
624 jd_midnight: JulianDate,
625 latitude: f64,
626 longitude: f64,
627 delta_t: f64,
628 elevation_angle: f64,
629) -> crate::SunriseResult<crate::HoursUtc> {
630 let (nu_degrees, alpha_deltas) = precompute_sunrise_sunset_for_jd_midnight(jd_midnight);
631 calculate_sunrise_sunset_hours_with_precomputed(
632 latitude,
633 longitude,
634 delta_t,
635 elevation_angle,
636 nu_degrees,
637 alpha_deltas,
638 )
639}
640
641#[derive(Debug, Clone, Copy)]
648enum PolarType {
649 AllDay,
650 AllNight,
651}
652
653fn check_polar_conditions_type(
655 latitude: f64,
656 elevation_angle: f64,
657 delta1: f64,
658) -> Option<PolarType> {
659 let phi = degrees_to_radians(latitude);
660 let elevation_rad = degrees_to_radians(elevation_angle);
661 let delta1_rad = degrees_to_radians(delta1);
662
663 let acos_arg =
664 mul_add(sin(phi), -sin(delta1_rad), sin(elevation_rad)) / (cos(phi) * cos(delta1_rad));
665
666 if acos_arg < -1.0 {
667 Some(PolarType::AllDay)
668 } else if acos_arg > 1.0 {
669 Some(PolarType::AllNight)
670 } else {
671 None
672 }
673}
674
675fn calculate_approximate_times(
677 m0: f64,
678 latitude: f64,
679 elevation_angle: f64,
680 delta1: f64,
681) -> [f64; 3] {
682 let phi = degrees_to_radians(latitude);
683 let delta1_rad = degrees_to_radians(delta1);
684 let elevation_rad = degrees_to_radians(elevation_angle);
685
686 let acos_arg =
687 mul_add(sin(phi), -sin(delta1_rad), sin(elevation_rad)) / (cos(phi) * cos(delta1_rad));
688 let h0 = acos(acos_arg);
689 let h0_degrees = radians_to_degrees(h0);
690
691 let mut m = [0.0; 3];
692 m[0] = normalize_to_unit_range(m0);
693 m[1] = normalize_to_unit_range(m0 - h0_degrees / 360.0);
694 m[2] = normalize_to_unit_range(m0 + h0_degrees / 360.0);
695
696 m
697}
698
699fn calculate_final_time_fractions(
702 m_values: [f64; 3],
703 nu_degrees: f64,
704 delta_t: f64,
705 latitude: f64,
706 longitude: f64,
707 elevation_angle: f64,
708 alpha_deltas: [AlphaDelta; 3],
709) -> (f64, f64, f64) {
710 let mut nu = [0.0; 3];
712 for (i, nu_item) in nu.iter_mut().enumerate() {
713 *nu_item = mul_add(360.985647f64, m_values[i], nu_degrees);
714 }
715
716 let mut n = [0.0; 3];
718 for (i, n_item) in n.iter_mut().enumerate() {
719 *n_item = m_values[i] + delta_t / 86400.0;
720 }
721
722 let alpha_delta_primes = calculate_interpolated_alpha_deltas(&alpha_deltas, &n);
724
725 let mut h_prime = [0.0; 3];
727 for i in 0..3 {
728 let h_prime_i = nu[i] + longitude - alpha_delta_primes[i].alpha;
729 h_prime[i] = limit_h_prime(h_prime_i);
730 }
731
732 let phi = degrees_to_radians(latitude);
734 let mut h = [0.0; 3];
735 for i in 0..3 {
736 let delta_prime_rad = degrees_to_radians(alpha_delta_primes[i].delta);
737 h[i] = radians_to_degrees(asin(mul_add(
738 sin(phi),
739 sin(delta_prime_rad),
740 cos(phi) * cos(delta_prime_rad) * cos(degrees_to_radians(h_prime[i])),
741 )));
742 }
743
744 let t = m_values[0] - h_prime[0] / 360.0;
746 let r = m_values[1]
747 + (h[1] - elevation_angle)
748 / (360.0
749 * cos(degrees_to_radians(alpha_delta_primes[1].delta))
750 * cos(phi)
751 * sin(degrees_to_radians(h_prime[1])));
752 let s = m_values[2]
753 + (h[2] - elevation_angle)
754 / (360.0
755 * cos(degrees_to_radians(alpha_delta_primes[2].delta))
756 * cos(phi)
757 * sin(degrees_to_radians(h_prime[2])));
758
759 (t, r, s)
760}
761
762fn calculate_interpolated_alpha_deltas(
764 alpha_deltas: &[AlphaDelta; 3],
765 n: &[f64; 3],
766) -> [AlphaDelta; 3] {
767 let a = limit_if_necessary(alpha_deltas[1].alpha - alpha_deltas[0].alpha);
768 let a_prime = limit_if_necessary(alpha_deltas[1].delta - alpha_deltas[0].delta);
769
770 let b = limit_if_necessary(alpha_deltas[2].alpha - alpha_deltas[1].alpha);
771 let b_prime = limit_if_necessary(alpha_deltas[2].delta - alpha_deltas[1].delta);
772
773 let c = b - a;
774 let c_prime = b_prime - a_prime;
775
776 let mut alpha_delta_primes = [AlphaDelta {
777 alpha: 0.0,
778 delta: 0.0,
779 }; 3];
780 for i in 0..3 {
781 alpha_delta_primes[i].alpha =
782 alpha_deltas[1].alpha + (n[i] * (mul_add(c, n[i], a + b))) / 2.0;
783 alpha_delta_primes[i].delta =
784 alpha_deltas[1].delta + (n[i] * (mul_add(c_prime, n[i], a_prime + b_prime))) / 2.0;
785 }
786 alpha_delta_primes
787}
788
789#[derive(Debug, Clone, Copy)]
790struct AlphaDelta {
791 alpha: f64,
792 delta: f64,
793}
794
795#[cfg(feature = "chrono")]
841#[cfg_attr(docsrs, doc(cfg(feature = "chrono")))]
842pub fn sunrise_sunset_for_horizon<Tz: TimeZone>(
843 date: DateTime<Tz>,
844 latitude: f64,
845 longitude: f64,
846 delta_t: f64,
847 horizon: Horizon,
848) -> Result<crate::SunriseResult<DateTime<Tz>>> {
849 sunrise_sunset(
850 date,
851 latitude,
852 longitude,
853 delta_t,
854 horizon.elevation_angle(),
855 )
856}
857
858fn calculate_alpha_delta(jme: f64, delta_psi: f64, epsilon_degrees: f64) -> AlphaDelta {
861 let b_terms = calculate_lbr_terms(jme, TERMS_B);
865 let b_degrees = lbr_to_normalized_degrees(jme, &b_terms, TERMS_B.len());
866
867 let r_terms = calculate_lbr_terms(jme, TERMS_R);
869 let r = calculate_lbr_polynomial(jme, &r_terms, TERMS_R.len());
870 assert!(
871 r != 0.0,
872 "Earth radius vector is zero - astronomical impossibility"
873 );
874
875 let l_terms = calculate_lbr_terms(jme, TERMS_L);
877 let l_degrees = lbr_to_normalized_degrees(jme, &l_terms, TERMS_L.len());
878
879 let theta_degrees = normalize_degrees_0_to_360(l_degrees + 180.0);
881
882 let beta_degrees = -b_degrees;
884 let beta = degrees_to_radians(beta_degrees);
885 let epsilon = degrees_to_radians(epsilon_degrees);
886
887 let delta_tau = ABERRATION_CONSTANT / (SECONDS_PER_HOUR * r);
889
890 let lambda_degrees = theta_degrees + delta_psi + delta_tau;
892 let lambda = degrees_to_radians(lambda_degrees);
893
894 let alpha_degrees = calculate_geocentric_sun_right_ascension(beta, epsilon, lambda);
896 let delta_degrees =
897 radians_to_degrees(calculate_geocentric_sun_declination(beta, epsilon, lambda));
898
899 AlphaDelta {
900 alpha: alpha_degrees,
901 delta: delta_degrees,
902 }
903}
904
905fn normalize_to_unit_range(val: f64) -> f64 {
907 let limited = val % 1.0;
908 if limited < 0.0 {
909 limited + 1.0
910 } else {
911 limited
912 }
913}
914
915#[cfg(feature = "chrono")]
916fn select_utc_date_by_transit<V, F>(
917 local_date: NaiveDate,
918 mut utc_date: NaiveDate,
919 mut compute: F,
920) -> Result<(NaiveDate, V)>
921where
922 F: FnMut(NaiveDate) -> Result<(NaiveDate, V)>,
923{
924 let (transit_local_date, value) = compute(utc_date)?;
925 if transit_local_date == local_date {
926 return Ok((utc_date, value));
927 }
928
929 utc_date = if transit_local_date > local_date {
930 utc_date.pred_opt().unwrap_or(utc_date)
931 } else {
932 utc_date.succ_opt().unwrap_or(utc_date)
933 };
934
935 let (transit_local_date, value) = compute(utc_date)?;
936 debug_assert_eq!(transit_local_date, local_date);
937 Ok((utc_date, value))
938}
939
940#[cfg(feature = "chrono")]
941fn hours_utc_to_datetime<Tz: TimeZone>(
942 tz: &Tz,
943 base_utc_date: NaiveDate,
944 hours: crate::HoursUtc,
945) -> DateTime<Tz> {
946 let base_utc_midnight = base_utc_date
947 .and_hms_opt(0, 0, 0)
948 .expect("midnight is always valid")
949 .and_utc();
950
951 let millis_plus = (hours.hours() * 3_600_000.0) as i64;
954 let utc_dt = base_utc_midnight + chrono::Duration::milliseconds(millis_plus);
955
956 tz.from_utc_datetime(&utc_dt.naive_utc())
957}
958
959#[cfg(feature = "chrono")]
960fn ensure_events_bracket_transit<Tz: TimeZone>(
961 result: crate::SunriseResult<DateTime<Tz>>,
962) -> crate::SunriseResult<DateTime<Tz>> {
963 let crate::SunriseResult::RegularDay {
964 mut sunrise,
965 transit,
966 mut sunset,
967 } = result
968 else {
969 return result;
970 };
971
972 if transit.offset().fix().local_minus_utc() == 0 {
976 return crate::SunriseResult::RegularDay {
977 sunrise,
978 transit,
979 sunset,
980 };
981 }
982
983 if sunrise > transit {
985 sunrise -= chrono::Duration::days(1);
986 }
987
988 if sunset < transit {
989 sunset += chrono::Duration::days(1);
990 }
991
992 crate::SunriseResult::RegularDay {
993 sunrise,
994 transit,
995 sunset,
996 }
997}
998
999fn limit_if_necessary(val: f64) -> f64 {
1001 if val.abs() > 2.0 {
1002 normalize_to_unit_range(val)
1003 } else {
1004 val
1005 }
1006}
1007
1008fn limit_h_prime(h_prime: f64) -> f64 {
1010 let limited = {
1011 let v = h_prime % 360.0;
1012 if v < 0.0 {
1013 v + 360.0
1014 } else {
1015 v
1016 }
1017 };
1018 if limited > 180.0 {
1019 limited - 360.0
1020 } else {
1021 limited
1022 }
1023}
1024
1025#[cfg(feature = "chrono")]
1080#[cfg_attr(docsrs, doc(cfg(feature = "chrono")))]
1081#[allow(clippy::needless_pass_by_value)]
1082pub fn sunrise_sunset_multiple<Tz, H>(
1083 date: DateTime<Tz>,
1084 latitude: f64,
1085 longitude: f64,
1086 delta_t: f64,
1087 horizons: H,
1088) -> impl Iterator<Item = Result<(Horizon, crate::SunriseResult<DateTime<Tz>>)>>
1089where
1090 Tz: TimeZone,
1091 H: IntoIterator<Item = Horizon>,
1092{
1093 let tz = date.timezone();
1094 let local_date = date.date_naive();
1095 let base_utc_date_guess = date.date_naive();
1099
1100 let precomputed = (|| -> Result<_> {
1102 check_coordinates(latitude, longitude)?;
1103 let (base_utc_date, (nu_degrees, alpha_deltas)) =
1104 select_utc_date_by_transit(local_date, base_utc_date_guess, |d| {
1105 let jd_midnight =
1106 JulianDate::from_utc(d.year(), d.month(), d.day(), 0, 0, 0.0, delta_t)?;
1107
1108 let (nu_degrees, alpha_deltas) =
1109 precompute_sunrise_sunset_for_jd_midnight(jd_midnight);
1110 let transit_hours = match calculate_sunrise_sunset_hours_with_precomputed(
1111 latitude,
1112 longitude,
1113 delta_t,
1114 Horizon::SunriseSunset.elevation_angle(),
1115 nu_degrees,
1116 alpha_deltas,
1117 ) {
1118 crate::SunriseResult::RegularDay { transit, .. }
1119 | crate::SunriseResult::AllDay { transit }
1120 | crate::SunriseResult::AllNight { transit } => transit,
1121 };
1122
1123 let transit_local_date = hours_utc_to_datetime(&tz, d, transit_hours).date_naive();
1124 Ok((transit_local_date, (nu_degrees, alpha_deltas)))
1125 })?;
1126
1127 Ok((base_utc_date, nu_degrees, alpha_deltas))
1128 })();
1129
1130 horizons.into_iter().map(move |horizon| {
1131 let (base_utc_date, nu_degrees, alpha_deltas) = precomputed.clone()?;
1132 let hours_result = calculate_sunrise_sunset_hours_with_precomputed(
1133 latitude,
1134 longitude,
1135 delta_t,
1136 horizon.elevation_angle(),
1137 nu_degrees,
1138 alpha_deltas,
1139 );
1140
1141 let result = match hours_result {
1142 crate::SunriseResult::RegularDay {
1143 sunrise,
1144 transit,
1145 sunset,
1146 } => crate::SunriseResult::RegularDay {
1147 sunrise: hours_utc_to_datetime(&tz, base_utc_date, sunrise),
1148 transit: hours_utc_to_datetime(&tz, base_utc_date, transit),
1149 sunset: hours_utc_to_datetime(&tz, base_utc_date, sunset),
1150 },
1151 crate::SunriseResult::AllDay { transit } => crate::SunriseResult::AllDay {
1152 transit: hours_utc_to_datetime(&tz, base_utc_date, transit),
1153 },
1154 crate::SunriseResult::AllNight { transit } => crate::SunriseResult::AllNight {
1155 transit: hours_utc_to_datetime(&tz, base_utc_date, transit),
1156 },
1157 };
1158
1159 let result = ensure_events_bracket_transit(result);
1160
1161 Ok((horizon, result))
1162 })
1163}
1164
1165#[cfg(feature = "chrono")]
1203#[cfg_attr(docsrs, doc(cfg(feature = "chrono")))]
1204#[allow(clippy::needless_pass_by_value)]
1205pub fn spa_time_dependent_parts<Tz: TimeZone>(
1206 datetime: DateTime<Tz>,
1207 delta_t: f64,
1208) -> Result<SpaTimeDependent> {
1209 let jd = JulianDate::from_datetime(&datetime, delta_t)?;
1210 spa_time_dependent_from_julian(jd)
1211}
1212
1213pub fn spa_time_dependent_from_julian(jd: JulianDate) -> Result<SpaTimeDependent> {
1223 let jme = jd.julian_ephemeris_millennium();
1224 let jce = jd.julian_ephemeris_century();
1225
1226 let l_terms = calculate_lbr_terms(jme, TERMS_L);
1228 let l_degrees = lbr_to_normalized_degrees(jme, &l_terms, TERMS_L.len());
1229
1230 let b_terms = calculate_lbr_terms(jme, TERMS_B);
1232 let b_degrees = lbr_to_normalized_degrees(jme, &b_terms, TERMS_B.len());
1233
1234 let r_terms = calculate_lbr_terms(jme, TERMS_R);
1236 let r = calculate_lbr_polynomial(jme, &r_terms, TERMS_R.len());
1237
1238 assert!(
1240 r != 0.0,
1241 "Earth radius vector is zero - astronomical impossibility"
1242 );
1243
1244 let theta_degrees = normalize_degrees_0_to_360(l_degrees + 180.0);
1246 let beta_degrees = -b_degrees;
1248
1249 let x_terms = calculate_nutation_terms(jce);
1251 let delta_psi_epsilon = calculate_delta_psi_epsilon(jce, &x_terms);
1252
1253 let epsilon_degrees =
1255 calculate_true_obliquity_of_ecliptic(&jd, delta_psi_epsilon.delta_epsilon);
1256
1257 let delta_tau = ABERRATION_CONSTANT / (SECONDS_PER_HOUR * r);
1259
1260 let lambda_degrees = theta_degrees + delta_psi_epsilon.delta_psi + delta_tau;
1262
1263 let nu_degrees = calculate_apparent_sidereal_time_at_greenwich(
1265 &jd,
1266 delta_psi_epsilon.delta_psi,
1267 epsilon_degrees,
1268 );
1269
1270 let beta = degrees_to_radians(beta_degrees);
1272 let epsilon = degrees_to_radians(epsilon_degrees);
1273 let lambda = degrees_to_radians(lambda_degrees);
1274 let alpha_degrees = calculate_geocentric_sun_right_ascension(beta, epsilon, lambda);
1275
1276 let delta_degrees =
1278 radians_to_degrees(calculate_geocentric_sun_declination(beta, epsilon, lambda));
1279
1280 Ok(SpaTimeDependent {
1281 r,
1282 nu_degrees,
1283 alpha_degrees,
1284 delta_degrees,
1285 })
1286}
1287
1288pub fn spa_with_time_dependent_parts(
1326 latitude: f64,
1327 longitude: f64,
1328 elevation: f64,
1329 refraction: Option<RefractionCorrection>,
1330 time_dependent: &SpaTimeDependent,
1331) -> Result<SolarPosition> {
1332 check_coordinates(latitude, longitude)?;
1333
1334 let nu_degrees = time_dependent.nu_degrees;
1337
1338 let h_degrees =
1340 normalize_degrees_0_to_360(nu_degrees + longitude - time_dependent.alpha_degrees);
1341 let h = degrees_to_radians(h_degrees);
1342
1343 let xi_degrees = 8.794 / (3600.0 * time_dependent.r);
1345 let xi = degrees_to_radians(xi_degrees);
1346 let phi = degrees_to_radians(latitude);
1347 let delta = degrees_to_radians(time_dependent.delta_degrees);
1348
1349 let u = atan(EARTH_FLATTENING_FACTOR * tan(phi));
1350 let y = mul_add(
1351 EARTH_FLATTENING_FACTOR,
1352 sin(u),
1353 (elevation / EARTH_RADIUS_METERS) * sin(phi),
1354 );
1355 let x = mul_add(elevation / EARTH_RADIUS_METERS, cos(phi), cos(u));
1356
1357 let delta_alpha_prime_degrees = radians_to_degrees(atan2(
1358 -x * sin(xi) * sin(h),
1359 mul_add(x * sin(xi), -cos(h), cos(delta)),
1360 ));
1361
1362 let delta_prime = radians_to_degrees(atan2(
1363 mul_add(y, -sin(xi), sin(delta)) * cos(degrees_to_radians(delta_alpha_prime_degrees)),
1364 mul_add(x * sin(xi), -cos(h), cos(delta)),
1365 ));
1366
1367 let h_prime_degrees = h_degrees - delta_alpha_prime_degrees;
1369
1370 let zenith_angle = radians_to_degrees(acos(mul_add(
1372 sin(degrees_to_radians(latitude)),
1373 sin(degrees_to_radians(delta_prime)),
1374 cos(degrees_to_radians(latitude))
1375 * cos(degrees_to_radians(delta_prime))
1376 * cos(degrees_to_radians(h_prime_degrees)),
1377 )));
1378
1379 let azimuth = normalize_degrees_0_to_360(
1381 180.0
1382 + radians_to_degrees(atan2(
1383 sin(degrees_to_radians(h_prime_degrees)),
1384 cos(degrees_to_radians(h_prime_degrees)) * sin(degrees_to_radians(latitude))
1385 - tan(degrees_to_radians(delta_prime)) * cos(degrees_to_radians(latitude)),
1386 )),
1387 );
1388
1389 let elevation_angle = 90.0 - zenith_angle;
1391 let final_zenith = refraction.map_or(zenith_angle, |correction| {
1392 if elevation_angle > SUNRISE_SUNSET_ANGLE {
1393 let pressure = correction.pressure();
1394 let temperature = correction.temperature();
1395 zenith_angle
1397 - (pressure / 1010.0) * (283.0 / (273.0 + temperature)) * 1.02
1398 / (60.0
1399 * tan(degrees_to_radians(
1400 elevation_angle + 10.3 / (elevation_angle + 5.11),
1401 )))
1402 } else {
1403 zenith_angle
1404 }
1405 });
1406
1407 SolarPosition::new(azimuth, final_zenith)
1408}
1409
1410#[cfg(all(test, feature = "chrono", feature = "std"))]
1411mod tests {
1412 use super::*;
1413 use chrono::{DateTime, FixedOffset};
1414 use std::collections::HashSet;
1415
1416 #[test]
1417 fn test_spa_basic_functionality() {
1418 let datetime = "2023-06-21T12:00:00Z"
1419 .parse::<DateTime<FixedOffset>>()
1420 .unwrap();
1421
1422 let result = solar_position(
1423 datetime,
1424 37.7749, -122.4194,
1426 0.0,
1427 69.0,
1428 Some(RefractionCorrection::new(1013.25, 15.0).unwrap()),
1429 );
1430
1431 assert!(result.is_ok());
1432 let position = result.unwrap();
1433 assert!(position.azimuth() >= 0.0 && position.azimuth() <= 360.0);
1434 assert!(position.zenith_angle() >= 0.0 && position.zenith_angle() <= 180.0);
1435 }
1436
1437 #[test]
1438 fn test_sunrise_sunset_multiple() {
1439 let datetime = "2023-06-21T12:00:00Z"
1440 .parse::<DateTime<FixedOffset>>()
1441 .unwrap();
1442 let horizons = [
1443 Horizon::SunriseSunset,
1444 Horizon::CivilTwilight,
1445 Horizon::NauticalTwilight,
1446 ];
1447
1448 let results: Result<Vec<_>> = sunrise_sunset_multiple(
1449 datetime,
1450 37.7749, -122.4194, 69.0, horizons.iter().copied(),
1454 )
1455 .collect();
1456
1457 let results = results.unwrap();
1458
1459 assert_eq!(results.len(), 3);
1461
1462 let returned_horizons: HashSet<_> = results.iter().map(|(h, _)| *h).collect();
1464 for expected_horizon in horizons {
1465 assert!(returned_horizons.contains(&expected_horizon));
1466 }
1467
1468 for (horizon, bulk_result) in &results {
1470 let individual_result =
1471 sunrise_sunset_for_horizon(datetime, 37.7749, -122.4194, 69.0, *horizon).unwrap();
1472
1473 match (&individual_result, bulk_result) {
1475 (
1476 crate::SunriseResult::RegularDay {
1477 sunrise: s1,
1478 transit: t1,
1479 sunset: ss1,
1480 },
1481 crate::SunriseResult::RegularDay {
1482 sunrise: s2,
1483 transit: t2,
1484 sunset: ss2,
1485 },
1486 ) => {
1487 assert_eq!(s1, s2);
1488 assert_eq!(t1, t2);
1489 assert_eq!(ss1, ss2);
1490 }
1491 (
1492 crate::SunriseResult::AllDay { transit: t1 },
1493 crate::SunriseResult::AllDay { transit: t2 },
1494 )
1495 | (
1496 crate::SunriseResult::AllNight { transit: t1 },
1497 crate::SunriseResult::AllNight { transit: t2 },
1498 ) => {
1499 assert_eq!(t1, t2);
1500 }
1501 _ => panic!("Bulk and individual results differ in type for {horizon:?}"),
1502 }
1503 }
1504 }
1505
1506 #[test]
1507 fn test_sunrise_sunset_multiple_polar_consistency() {
1508 let datetime = "2023-06-21T12:00:00Z"
1509 .parse::<DateTime<FixedOffset>>()
1510 .unwrap();
1511
1512 let individual = sunrise_sunset_for_horizon(
1513 datetime,
1514 80.0, 0.0,
1516 69.0,
1517 Horizon::SunriseSunset,
1518 )
1519 .unwrap();
1520
1521 let bulk_results: Result<Vec<_>> =
1522 sunrise_sunset_multiple(datetime, 80.0, 0.0, 69.0, [Horizon::SunriseSunset]).collect();
1523
1524 let (_, bulk) = bulk_results.unwrap().into_iter().next().unwrap();
1525
1526 match (bulk, individual) {
1527 (
1528 crate::SunriseResult::AllDay { transit: t1 },
1529 crate::SunriseResult::AllDay { transit: t2 },
1530 )
1531 | (
1532 crate::SunriseResult::AllNight { transit: t1 },
1533 crate::SunriseResult::AllNight { transit: t2 },
1534 ) => assert_eq!(t1, t2),
1535 _ => panic!("expected matching polar-day/night results between bulk and individual"),
1536 }
1537 }
1538
1539 #[test]
1540 fn test_spa_no_refraction() {
1541 let datetime = "2023-06-21T12:00:00Z"
1542 .parse::<DateTime<FixedOffset>>()
1543 .unwrap();
1544
1545 let result = solar_position(datetime, 37.7749, -122.4194, 0.0, 69.0, None);
1546
1547 assert!(result.is_ok());
1548 let position = result.unwrap();
1549 assert!(position.azimuth() >= 0.0 && position.azimuth() <= 360.0);
1550 assert!(position.zenith_angle() >= 0.0 && position.zenith_angle() <= 180.0);
1551 }
1552
1553 #[test]
1554 fn test_spa_coordinate_validation() {
1555 let datetime = "2023-06-21T12:00:00Z"
1556 .parse::<DateTime<FixedOffset>>()
1557 .unwrap();
1558
1559 assert!(solar_position(
1561 datetime,
1562 95.0,
1563 0.0,
1564 0.0,
1565 0.0,
1566 Some(RefractionCorrection::new(1013.25, 15.0).unwrap())
1567 )
1568 .is_err());
1569
1570 assert!(solar_position(
1572 datetime,
1573 0.0,
1574 185.0,
1575 0.0,
1576 0.0,
1577 Some(RefractionCorrection::new(1013.25, 15.0).unwrap())
1578 )
1579 .is_err());
1580 }
1581
1582 #[test]
1583 fn test_sunrise_sunset_basic() {
1584 let date = "2023-06-21T00:00:00Z"
1585 .parse::<DateTime<FixedOffset>>()
1586 .unwrap();
1587
1588 let result = sunrise_sunset(date, 37.7749, -122.4194, 69.0, -0.833);
1589 assert!(result.is_ok());
1590
1591 let result =
1592 sunrise_sunset_for_horizon(date, 37.7749, -122.4194, 69.0, Horizon::SunriseSunset);
1593 assert!(result.is_ok());
1594 }
1595
1596 #[test]
1597 fn test_horizon_enum() {
1598 assert_eq!(Horizon::SunriseSunset.elevation_angle(), -0.83337);
1599 assert_eq!(Horizon::CivilTwilight.elevation_angle(), -6.0);
1600 assert_eq!(Horizon::Custom(-10.5).elevation_angle(), -10.5);
1601 }
1602}