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::{DateTime, TimeZone, Utc};
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#[allow(clippy::needless_pass_by_value)]
94pub fn solar_position<Tz: TimeZone>(
95 datetime: DateTime<Tz>,
96 latitude: f64,
97 longitude: f64,
98 elevation: f64,
99 delta_t: f64,
100 refraction: Option<RefractionCorrection>,
101) -> Result<SolarPosition> {
102 let jd = JulianDate::from_datetime(&datetime, delta_t)?;
103 solar_position_from_julian(jd, latitude, longitude, elevation, refraction)
104}
105
106pub fn solar_position_from_julian(
142 jd: JulianDate,
143 latitude: f64,
144 longitude: f64,
145 elevation: f64,
146 refraction: Option<RefractionCorrection>,
147) -> Result<SolarPosition> {
148 let time_dependent = spa_time_dependent_from_julian(jd)?;
149 spa_with_time_dependent_parts(latitude, longitude, elevation, refraction, &time_dependent)
150}
151
152#[derive(Debug, Clone)]
157#[allow(dead_code)]
158pub struct SpaTimeDependent {
159 pub(crate) theta_degrees: f64,
161 pub(crate) beta_degrees: f64,
163 pub(crate) r: f64,
165 pub(crate) delta_psi: f64,
167 pub(crate) epsilon_degrees: f64,
169 pub(crate) lambda_degrees: f64,
171 pub(crate) nu_degrees: f64,
173 pub(crate) alpha_degrees: f64,
175 pub(crate) delta_degrees: f64,
177}
178
179#[derive(Debug, Clone, Copy)]
180struct DeltaPsiEpsilon {
181 delta_psi: f64,
182 delta_epsilon: f64,
183}
184
185fn calculate_lbr_terms(jme: f64, term_coeffs: &[&[&[f64; 3]]]) -> [f64; 6] {
187 let mut lbr_terms = [0.0; 6];
190
191 for (i, term_set) in term_coeffs.iter().enumerate().take(6) {
192 let mut lbr_sum = 0.0;
193 for term in *term_set {
194 lbr_sum += term[0] * cos(mul_add(term[2], jme, term[1]));
195 }
196 lbr_terms[i] = lbr_sum;
197 }
198
199 lbr_terms
200}
201
202fn calculate_lbr_polynomial(jme: f64, terms: &[f64], num_terms: usize) -> f64 {
204 polynomial(&terms[..num_terms], jme) / 1e8
205}
206
207fn lbr_to_normalized_degrees(jme: f64, terms: &[f64], num_terms: usize) -> f64 {
209 normalize_degrees_0_to_360(radians_to_degrees(calculate_lbr_polynomial(
210 jme, terms, num_terms,
211 )))
212}
213
214fn calculate_nutation_terms(jce: f64) -> [f64; 5] {
216 [
219 polynomial(NUTATION_COEFFS[0], jce),
220 polynomial(NUTATION_COEFFS[1], jce),
221 polynomial(NUTATION_COEFFS[2], jce),
222 polynomial(NUTATION_COEFFS[3], jce),
223 polynomial(NUTATION_COEFFS[4], jce),
224 ]
225}
226
227fn calculate_delta_psi_epsilon(jce: f64, x: &[f64]) -> DeltaPsiEpsilon {
229 let mut delta_psi = 0.0;
230 let mut delta_epsilon = 0.0;
231
232 for (i, pe_term) in TERMS_PE.iter().enumerate() {
233 let xj_yterm_sum = degrees_to_radians(calculate_xj_yterm_sum(i, x));
234
235 let delta_psi_contrib = mul_add(pe_term[1], jce, pe_term[0]) * sin(xj_yterm_sum);
237 let delta_epsilon_contrib = mul_add(pe_term[3], jce, pe_term[2]) * cos(xj_yterm_sum);
238
239 delta_psi += delta_psi_contrib;
240 delta_epsilon += delta_epsilon_contrib;
241 }
242
243 DeltaPsiEpsilon {
244 delta_psi: delta_psi / 36_000_000.0,
245 delta_epsilon: delta_epsilon / 36_000_000.0,
246 }
247}
248
249fn calculate_xj_yterm_sum(i: usize, x: &[f64]) -> f64 {
251 let mut sum = 0.0;
252 for (j, &x_val) in x.iter().enumerate() {
253 sum += x_val * f64::from(TERMS_Y[i][j]);
254 }
255 sum
256}
257
258fn calculate_true_obliquity_of_ecliptic(jd: &JulianDate, delta_epsilon: f64) -> f64 {
260 let epsilon0 = polynomial(OBLIQUITY_COEFFS, jd.julian_ephemeris_millennium() / 10.0);
261 epsilon0 / 3600.0 + delta_epsilon
262}
263
264fn calculate_apparent_sidereal_time_at_greenwich(
266 jd: &JulianDate,
267 delta_psi: f64,
268 epsilon_degrees: f64,
269) -> f64 {
270 let nu0_degrees = normalize_degrees_0_to_360(mul_add(
271 powi(jd.julian_century(), 2),
272 0.000387933 - jd.julian_century() / 38710000.0,
273 mul_add(
274 360.98564736629f64,
275 jd.julian_date() - 2451545.0,
276 280.46061837,
277 ),
278 ));
279
280 mul_add(
281 delta_psi,
282 cos(degrees_to_radians(epsilon_degrees)),
283 nu0_degrees,
284 )
285}
286
287fn calculate_geocentric_sun_right_ascension(
289 beta_rad: f64,
290 epsilon_rad: f64,
291 lambda_rad: f64,
292) -> f64 {
293 let alpha = atan2(
294 mul_add(
295 sin(lambda_rad),
296 cos(epsilon_rad),
297 -(tan(beta_rad) * sin(epsilon_rad)),
298 ),
299 cos(lambda_rad),
300 );
301 normalize_degrees_0_to_360(radians_to_degrees(alpha))
302}
303
304fn calculate_geocentric_sun_declination(beta_rad: f64, epsilon_rad: f64, lambda_rad: f64) -> f64 {
306 asin(mul_add(
307 sin(beta_rad),
308 cos(epsilon_rad),
309 cos(beta_rad) * sin(epsilon_rad) * sin(lambda_rad),
310 ))
311}
312
313pub fn sunrise_sunset_utc(
354 year: i32,
355 month: u32,
356 day: u32,
357 latitude: f64,
358 longitude: f64,
359 delta_t: f64,
360 elevation_angle: f64,
361) -> Result<crate::SunriseResult<crate::HoursUtc>> {
362 check_coordinates(latitude, longitude)?;
363
364 let jd_midnight = JulianDate::from_utc(year, month, day, 0, 0, 0.0, delta_t)?;
366
367 calculate_sunrise_sunset_core(jd_midnight, latitude, longitude, delta_t, elevation_angle)
369}
370
371pub fn sunrise_sunset_utc_for_horizon(
412 year: i32,
413 month: u32,
414 day: u32,
415 latitude: f64,
416 longitude: f64,
417 delta_t: f64,
418 horizon: crate::Horizon,
419) -> Result<crate::SunriseResult<crate::HoursUtc>> {
420 sunrise_sunset_utc(
421 year,
422 month,
423 day,
424 latitude,
425 longitude,
426 delta_t,
427 horizon.elevation_angle(),
428 )
429}
430
431#[cfg(feature = "chrono")]
468#[allow(clippy::needless_pass_by_value)]
469pub fn sunrise_sunset<Tz: TimeZone>(
470 date: DateTime<Tz>,
471 latitude: f64,
472 longitude: f64,
473 delta_t: f64,
474 elevation_angle: f64,
475) -> Result<crate::SunriseResult<DateTime<Tz>>> {
476 check_coordinates(latitude, longitude)?;
477
478 let day_start = truncate_to_day_start(&date);
479
480 calculate_sunrise_sunset_spa_algorithm(day_start, latitude, longitude, delta_t, elevation_angle)
482}
483
484#[allow(clippy::unnecessary_wraps)]
488fn calculate_sunrise_sunset_core(
489 jd_midnight: JulianDate,
490 latitude: f64,
491 longitude: f64,
492 delta_t: f64,
493 elevation_angle: f64,
494) -> Result<crate::SunriseResult<crate::HoursUtc>> {
495 let jce_day = jd_midnight.julian_ephemeris_century();
497 let x_terms = calculate_nutation_terms(jce_day);
498 let delta_psi_epsilon = calculate_delta_psi_epsilon(jce_day, &x_terms);
499 let epsilon_degrees =
500 calculate_true_obliquity_of_ecliptic(&jd_midnight, delta_psi_epsilon.delta_epsilon);
501 let nu_degrees = calculate_apparent_sidereal_time_at_greenwich(
502 &jd_midnight,
503 delta_psi_epsilon.delta_psi,
504 epsilon_degrees,
505 );
506
507 let mut alpha_deltas = [AlphaDelta {
509 alpha: 0.0,
510 delta: 0.0,
511 }; 3];
512 for (i, alpha_delta) in alpha_deltas.iter_mut().enumerate() {
513 let current_jd = jd_midnight.add_days((i as f64) - 1.0);
514 let current_jme = current_jd.julian_ephemeris_millennium();
515 let ad = calculate_alpha_delta(current_jme, delta_psi_epsilon.delta_psi, epsilon_degrees);
516 *alpha_delta = ad;
517 }
518
519 let m0 = (alpha_deltas[1].alpha - longitude - nu_degrees) / 360.0;
521
522 let polar_type = check_polar_conditions_type(latitude, elevation_angle, alpha_deltas[1].delta);
524
525 let (m_values, _h0_degrees) =
527 calculate_approximate_times(m0, latitude, elevation_angle, alpha_deltas[1].delta);
528
529 let (t_frac, r_frac, s_frac) = calculate_final_time_fractions(
531 m_values,
532 nu_degrees,
533 delta_t,
534 latitude,
535 longitude,
536 elevation_angle,
537 alpha_deltas,
538 );
539
540 let transit_hours = crate::HoursUtc::from_hours(t_frac * 24.0);
542 let sunrise_hours = crate::HoursUtc::from_hours(r_frac * 24.0);
543 let sunset_hours = crate::HoursUtc::from_hours(s_frac * 24.0);
544
545 match polar_type {
547 Some(PolarType::AllDay) => Ok(crate::SunriseResult::AllDay {
548 transit: transit_hours,
549 }),
550 Some(PolarType::AllNight) => Ok(crate::SunriseResult::AllNight {
551 transit: transit_hours,
552 }),
553 None => Ok(crate::SunriseResult::RegularDay {
554 sunrise: sunrise_hours,
555 transit: transit_hours,
556 sunset: sunset_hours,
557 }),
558 }
559}
560
561#[cfg(feature = "chrono")]
563fn calculate_sunrise_sunset_spa_algorithm<Tz: TimeZone>(
564 day: DateTime<Tz>,
565 latitude: f64,
566 longitude: f64,
567 delta_t: f64,
568 elevation_angle: f64,
569) -> Result<crate::SunriseResult<DateTime<Tz>>> {
570 let day_start = truncate_to_day_start(&day);
571
572 let (nu_degrees, delta_psi_epsilon, epsilon_degrees) =
574 calculate_sidereal_time_and_nutation(day_start.clone(), delta_t)?;
575
576 let alpha_deltas = calculate_alpha_deltas_for_three_days(
578 day_start,
579 delta_t,
580 delta_psi_epsilon,
581 epsilon_degrees,
582 )?;
583
584 let m0 = (alpha_deltas[1].alpha - longitude - nu_degrees) / 360.0;
586
587 let polar_type = check_polar_conditions_type(latitude, elevation_angle, alpha_deltas[1].delta);
589
590 let (m_values, _h0_degrees) =
592 calculate_approximate_times(m0, latitude, elevation_angle, alpha_deltas[1].delta);
593
594 let final_result = calculate_final_times(FinalTimeParams {
596 day,
597 m_values,
598 nu_degrees,
599 delta_t,
600 latitude,
601 longitude,
602 elevation_angle,
603 alpha_deltas,
604 });
605
606 match polar_type {
608 Some(PolarType::AllDay) => {
609 if let crate::SunriseResult::RegularDay { transit, .. } = final_result {
610 Ok(crate::SunriseResult::AllDay { transit })
611 } else {
612 Ok(final_result)
613 }
614 }
615 Some(PolarType::AllNight) => {
616 if let crate::SunriseResult::RegularDay { transit, .. } = final_result {
617 Ok(crate::SunriseResult::AllNight { transit })
618 } else {
619 Ok(final_result)
620 }
621 }
622 None => Ok(final_result),
623 }
624}
625#[cfg(feature = "chrono")]
626#[allow(clippy::needless_pass_by_value)]
627fn calculate_sidereal_time_and_nutation<Tz: TimeZone>(
628 day_start: DateTime<Tz>,
629 delta_t: f64,
630) -> Result<(f64, DeltaPsiEpsilon, f64)> {
631 let jd = JulianDate::from_datetime(&day_start, delta_t)?;
632 let jce_day = jd.julian_ephemeris_century();
633 let x_terms = calculate_nutation_terms(jce_day);
634 let delta_psi_epsilon = calculate_delta_psi_epsilon(jce_day, &x_terms);
635 let epsilon_degrees =
636 calculate_true_obliquity_of_ecliptic(&jd, delta_psi_epsilon.delta_epsilon);
637 let nu_degrees = calculate_apparent_sidereal_time_at_greenwich(
638 &jd,
639 delta_psi_epsilon.delta_psi,
640 epsilon_degrees,
641 );
642 Ok((nu_degrees, delta_psi_epsilon, epsilon_degrees))
643}
644
645#[cfg(feature = "chrono")]
646#[allow(clippy::needless_pass_by_value)]
647fn calculate_alpha_deltas_for_three_days<Tz: TimeZone>(
648 day_start: DateTime<Tz>,
649 delta_t: f64,
650 delta_psi_epsilon: DeltaPsiEpsilon,
651 epsilon_degrees: f64,
652) -> Result<[AlphaDelta; 3]> {
653 let mut alpha_deltas = [AlphaDelta {
654 alpha: 0.0,
655 delta: 0.0,
656 }; 3];
657 for (i, alpha_delta) in alpha_deltas.iter_mut().enumerate() {
658 let current_jd = JulianDate::from_datetime(&day_start, delta_t)?.add_days((i as f64) - 1.0);
659 let current_jme = current_jd.julian_ephemeris_millennium();
660 let ad = calculate_alpha_delta(current_jme, delta_psi_epsilon.delta_psi, epsilon_degrees);
661 *alpha_delta = ad;
662 }
663 Ok(alpha_deltas)
664}
665
666#[derive(Debug, Clone, Copy)]
673enum PolarType {
674 AllDay,
675 AllNight,
676}
677
678fn check_polar_conditions_type(
680 latitude: f64,
681 elevation_angle: f64,
682 delta1: f64,
683) -> Option<PolarType> {
684 let phi = degrees_to_radians(latitude);
685 let elevation_rad = degrees_to_radians(elevation_angle);
686 let delta1_rad = degrees_to_radians(delta1);
687
688 let acos_arg =
689 mul_add(sin(phi), -sin(delta1_rad), sin(elevation_rad)) / (cos(phi) * cos(delta1_rad));
690
691 if acos_arg < -1.0 {
692 Some(PolarType::AllDay)
693 } else if acos_arg > 1.0 {
694 Some(PolarType::AllNight)
695 } else {
696 None
697 }
698}
699
700#[cfg(feature = "chrono")]
702fn check_polar_conditions<Tz: TimeZone>(
703 day: DateTime<Tz>,
704 m0: f64,
705 latitude: f64,
706 elevation_angle: f64,
707 delta1: f64,
708) -> Option<crate::SunriseResult<DateTime<Tz>>> {
709 check_polar_conditions_type(latitude, elevation_angle, delta1).map(|polar_type| {
710 let transit = add_fraction_of_day(day, m0);
711 match polar_type {
712 PolarType::AllDay => crate::SunriseResult::AllDay { transit },
713 PolarType::AllNight => crate::SunriseResult::AllNight { transit },
714 }
715 })
716}
717
718fn calculate_approximate_times(
720 m0: f64,
721 latitude: f64,
722 elevation_angle: f64,
723 delta1: f64,
724) -> ([f64; 3], f64) {
725 let phi = degrees_to_radians(latitude);
726 let delta1_rad = degrees_to_radians(delta1);
727 let elevation_rad = degrees_to_radians(elevation_angle);
728
729 let acos_arg =
730 mul_add(sin(phi), -sin(delta1_rad), sin(elevation_rad)) / (cos(phi) * cos(delta1_rad));
731 let h0 = acos(acos_arg);
732 let h0_degrees = radians_to_degrees(h0).min(180.0);
733
734 let mut m = [0.0; 3];
735 m[0] = normalize_to_unit_range(m0);
736 m[1] = normalize_to_unit_range(m0 - h0_degrees / 360.0);
737 m[2] = normalize_to_unit_range(m0 + h0_degrees / 360.0);
738
739 (m, h0_degrees)
740}
741
742fn calculate_final_time_fractions(
745 m_values: [f64; 3],
746 nu_degrees: f64,
747 delta_t: f64,
748 latitude: f64,
749 longitude: f64,
750 elevation_angle: f64,
751 alpha_deltas: [AlphaDelta; 3],
752) -> (f64, f64, f64) {
753 let mut nu = [0.0; 3];
755 for (i, nu_item) in nu.iter_mut().enumerate() {
756 *nu_item = mul_add(360.985647f64, m_values[i], nu_degrees);
757 }
758
759 let mut n = [0.0; 3];
761 for (i, n_item) in n.iter_mut().enumerate() {
762 *n_item = m_values[i] + delta_t / 86400.0;
763 }
764
765 let alpha_delta_primes = calculate_interpolated_alpha_deltas(&alpha_deltas, &n);
767
768 let mut h_prime = [0.0; 3];
770 for i in 0..3 {
771 let h_prime_i = nu[i] + longitude - alpha_delta_primes[i].alpha;
772 h_prime[i] = limit_h_prime(h_prime_i);
773 }
774
775 let phi = degrees_to_radians(latitude);
777 let mut h = [0.0; 3];
778 for i in 0..3 {
779 let delta_prime_rad = degrees_to_radians(alpha_delta_primes[i].delta);
780 h[i] = radians_to_degrees(asin(mul_add(
781 sin(phi),
782 sin(delta_prime_rad),
783 cos(phi) * cos(delta_prime_rad) * cos(degrees_to_radians(h_prime[i])),
784 )));
785 }
786
787 let t = m_values[0] - h_prime[0] / 360.0;
789 let r = m_values[1]
790 + (h[1] - elevation_angle)
791 / (360.0
792 * cos(degrees_to_radians(alpha_delta_primes[1].delta))
793 * cos(phi)
794 * sin(degrees_to_radians(h_prime[1])));
795 let s = m_values[2]
796 + (h[2] - elevation_angle)
797 / (360.0
798 * cos(degrees_to_radians(alpha_delta_primes[2].delta))
799 * cos(phi)
800 * sin(degrees_to_radians(h_prime[2])));
801
802 (t, r, s)
803}
804
805#[cfg(feature = "chrono")]
807fn calculate_final_times<Tz: TimeZone>(
808 params: FinalTimeParams<Tz>,
809) -> crate::SunriseResult<DateTime<Tz>> {
810 let mut nu = [0.0; 3];
812 for (i, nu_item) in nu.iter_mut().enumerate() {
813 *nu_item = mul_add(360.985647f64, params.m_values[i], params.nu_degrees);
814 }
815
816 let mut n = [0.0; 3];
818 for (i, n_item) in n.iter_mut().enumerate() {
819 *n_item = params.m_values[i] + params.delta_t / 86400.0;
820 }
821
822 let alpha_delta_primes = calculate_interpolated_alpha_deltas(¶ms.alpha_deltas, &n);
824
825 let mut h_prime = [0.0; 3];
827 for i in 0..3 {
828 let h_prime_i = nu[i] + params.longitude - alpha_delta_primes[i].alpha;
829 h_prime[i] = limit_h_prime(h_prime_i);
830 }
831
832 let phi = degrees_to_radians(params.latitude);
834 let mut h = [0.0; 3];
835 for i in 0..3 {
836 let delta_prime_rad = degrees_to_radians(alpha_delta_primes[i].delta);
837 h[i] = radians_to_degrees(asin(mul_add(
838 sin(phi),
839 sin(delta_prime_rad),
840 cos(phi) * cos(delta_prime_rad) * cos(degrees_to_radians(h_prime[i])),
841 )));
842 }
843
844 let t = params.m_values[0] - h_prime[0] / 360.0;
846 let r = params.m_values[1]
847 + (h[1] - params.elevation_angle)
848 / (360.0
849 * cos(degrees_to_radians(alpha_delta_primes[1].delta))
850 * cos(phi)
851 * sin(degrees_to_radians(h_prime[1])));
852 let s = params.m_values[2]
853 + (h[2] - params.elevation_angle)
854 / (360.0
855 * cos(degrees_to_radians(alpha_delta_primes[2].delta))
856 * cos(phi)
857 * sin(degrees_to_radians(h_prime[2])));
858
859 let sunrise = add_fraction_of_day(params.day.clone(), r);
860 let transit = add_fraction_of_day(params.day.clone(), t);
861 let sunset = add_fraction_of_day(params.day, s);
862
863 crate::SunriseResult::RegularDay {
864 sunrise,
865 transit,
866 sunset,
867 }
868}
869
870fn calculate_interpolated_alpha_deltas(
872 alpha_deltas: &[AlphaDelta; 3],
873 n: &[f64; 3],
874) -> [AlphaDelta; 3] {
875 let a = limit_if_necessary(alpha_deltas[1].alpha - alpha_deltas[0].alpha);
876 let a_prime = limit_if_necessary(alpha_deltas[1].delta - alpha_deltas[0].delta);
877
878 let b = limit_if_necessary(alpha_deltas[2].alpha - alpha_deltas[1].alpha);
879 let b_prime = limit_if_necessary(alpha_deltas[2].delta - alpha_deltas[1].delta);
880
881 let c = b - a;
882 let c_prime = b_prime - a_prime;
883
884 let mut alpha_delta_primes = [AlphaDelta {
885 alpha: 0.0,
886 delta: 0.0,
887 }; 3];
888 for i in 0..3 {
889 alpha_delta_primes[i].alpha =
890 alpha_deltas[1].alpha + (n[i] * (mul_add(c, n[i], a + b))) / 2.0;
891 alpha_delta_primes[i].delta =
892 alpha_deltas[1].delta + (n[i] * (mul_add(c_prime, n[i], a_prime + b_prime))) / 2.0;
893 }
894 alpha_delta_primes
895}
896
897#[derive(Debug, Clone, Copy)]
898struct AlphaDelta {
899 alpha: f64,
900 delta: f64,
901}
902
903#[derive(Debug, Clone)]
905#[cfg(feature = "chrono")]
906struct FinalTimeParams<Tz: TimeZone> {
907 day: DateTime<Tz>,
908 m_values: [f64; 3],
909 nu_degrees: f64,
910 delta_t: f64,
911 latitude: f64,
912 longitude: f64,
913 elevation_angle: f64,
914 alpha_deltas: [AlphaDelta; 3],
915}
916
917#[cfg(feature = "chrono")]
955pub fn sunrise_sunset_for_horizon<Tz: TimeZone>(
956 date: DateTime<Tz>,
957 latitude: f64,
958 longitude: f64,
959 delta_t: f64,
960 horizon: Horizon,
961) -> Result<crate::SunriseResult<DateTime<Tz>>> {
962 sunrise_sunset(
963 date,
964 latitude,
965 longitude,
966 delta_t,
967 horizon.elevation_angle(),
968 )
969}
970
971fn calculate_alpha_delta(jme: f64, delta_psi: f64, epsilon_degrees: f64) -> AlphaDelta {
974 let b_terms = calculate_lbr_terms(jme, TERMS_B);
978 let b_degrees = lbr_to_normalized_degrees(jme, &b_terms, TERMS_B.len());
979
980 let r_terms = calculate_lbr_terms(jme, TERMS_R);
982 let r = calculate_lbr_polynomial(jme, &r_terms, TERMS_R.len());
983 assert!(
984 r != 0.0,
985 "Earth radius vector is zero - astronomical impossibility"
986 );
987
988 let l_terms = calculate_lbr_terms(jme, TERMS_L);
990 let l_degrees = lbr_to_normalized_degrees(jme, &l_terms, TERMS_L.len());
991
992 let theta_degrees = normalize_degrees_0_to_360(l_degrees + 180.0);
994
995 let beta_degrees = -b_degrees;
997 let beta = degrees_to_radians(beta_degrees);
998 let epsilon = degrees_to_radians(epsilon_degrees);
999
1000 let delta_tau = ABERRATION_CONSTANT / (SECONDS_PER_HOUR * r);
1002
1003 let lambda_degrees = theta_degrees + delta_psi + delta_tau;
1005 let lambda = degrees_to_radians(lambda_degrees);
1006
1007 let alpha_degrees = calculate_geocentric_sun_right_ascension(beta, epsilon, lambda);
1009 let delta_degrees =
1010 radians_to_degrees(calculate_geocentric_sun_declination(beta, epsilon, lambda));
1011
1012 AlphaDelta {
1013 alpha: alpha_degrees,
1014 delta: delta_degrees,
1015 }
1016}
1017
1018fn normalize_to_unit_range(val: f64) -> f64 {
1020 let divided = val;
1021 let limited = divided - floor(divided);
1022 if limited < 0.0 {
1023 limited + 1.0
1024 } else {
1025 limited
1026 }
1027}
1028
1029#[cfg(feature = "chrono")]
1030fn truncate_to_day_start<Tz: TimeZone>(datetime: &DateTime<Tz>) -> DateTime<Tz> {
1031 let tz = datetime.timezone();
1032 let utc_midnight = datetime
1033 .with_timezone(&Utc)
1034 .date_naive()
1035 .and_hms_opt(0, 0, 0)
1036 .expect("midnight is always valid")
1037 .and_utc();
1038
1039 tz.from_utc_datetime(&utc_midnight.naive_utc())
1040}
1041
1042#[cfg(feature = "chrono")]
1043#[allow(clippy::needless_pass_by_value)]
1044fn add_fraction_of_day<Tz: TimeZone>(day: DateTime<Tz>, fraction: f64) -> DateTime<Tz> {
1045 const MS_PER_DAY: i32 = 24 * 60 * 60 * 1000; let millis_plus = (f64::from(MS_PER_DAY) * fraction) as i32; let day_start = truncate_to_day_start(&day);
1053
1054 day_start + chrono::Duration::milliseconds(i64::from(millis_plus))
1055}
1056
1057fn limit_if_necessary(val: f64) -> f64 {
1059 if val.abs() > 2.0 {
1060 normalize_to_unit_range(val)
1061 } else {
1062 val
1063 }
1064}
1065
1066fn limit_h_prime(h_prime: f64) -> f64 {
1068 let normalized = h_prime / 360.0;
1069 let limited = 360.0 * (normalized - floor(normalized));
1070
1071 if limited < -180.0 {
1072 limited + 360.0
1073 } else if limited > 180.0 {
1074 limited - 360.0
1075 } else {
1076 limited
1077 }
1078}
1079
1080#[cfg(feature = "chrono")]
1129pub fn sunrise_sunset_multiple<Tz, H>(
1130 date: DateTime<Tz>,
1131 latitude: f64,
1132 longitude: f64,
1133 delta_t: f64,
1134 horizons: H,
1135) -> impl Iterator<Item = Result<(Horizon, crate::SunriseResult<DateTime<Tz>>)>>
1136where
1137 Tz: TimeZone,
1138 H: IntoIterator<Item = Horizon>,
1139{
1140 let precomputed = (|| -> Result<_> {
1142 check_coordinates(latitude, longitude)?;
1143
1144 let day_start = truncate_to_day_start(&date);
1145 let (nu_degrees, delta_psi_epsilon, epsilon_degrees) =
1146 calculate_sidereal_time_and_nutation(day_start.clone(), delta_t)?;
1147 let alpha_deltas = calculate_alpha_deltas_for_three_days(
1148 day_start,
1149 delta_t,
1150 delta_psi_epsilon,
1151 epsilon_degrees,
1152 )?;
1153
1154 Ok((nu_degrees, alpha_deltas))
1155 })();
1156
1157 horizons.into_iter().map(move |horizon| match &precomputed {
1158 Ok((nu_degrees, alpha_deltas)) => {
1159 let result = calculate_sunrise_sunset_spa_algorithm_with_precomputed(
1160 date.clone(),
1161 latitude,
1162 longitude,
1163 delta_t,
1164 horizon.elevation_angle(),
1165 *nu_degrees,
1166 *alpha_deltas,
1167 );
1168 Ok((horizon, result))
1169 }
1170 Err(e) => Err(e.clone()),
1171 })
1172}
1173#[cfg(feature = "chrono")]
1174#[allow(clippy::needless_pass_by_value)]
1175fn calculate_sunrise_sunset_spa_algorithm_with_precomputed<Tz: TimeZone>(
1176 day: DateTime<Tz>,
1177 latitude: f64,
1178 longitude: f64,
1179 delta_t: f64,
1180 elevation_angle: f64,
1181 nu_degrees: f64,
1182 alpha_deltas: [AlphaDelta; 3],
1183) -> crate::SunriseResult<DateTime<Tz>> {
1184 let day_start = truncate_to_day_start(&day);
1185
1186 let m0 = (alpha_deltas[1].alpha - longitude - nu_degrees) / 360.0;
1188 if let Some(polar_result) = check_polar_conditions(
1189 day_start.clone(),
1190 m0,
1191 latitude,
1192 elevation_angle,
1193 alpha_deltas[1].delta,
1194 ) {
1195 return polar_result;
1196 }
1197
1198 let (m_values, _h0_degrees) =
1200 calculate_approximate_times(m0, latitude, elevation_angle, alpha_deltas[1].delta);
1201
1202 calculate_final_times(FinalTimeParams {
1204 day: day_start,
1205 m_values,
1206 nu_degrees,
1207 delta_t,
1208 latitude,
1209 longitude,
1210 elevation_angle,
1211 alpha_deltas,
1212 })
1213}
1214
1215#[cfg(feature = "chrono")]
1253#[allow(clippy::needless_pass_by_value)]
1254pub fn spa_time_dependent_parts<Tz: TimeZone>(
1255 datetime: DateTime<Tz>,
1256 delta_t: f64,
1257) -> Result<SpaTimeDependent> {
1258 let jd = JulianDate::from_datetime(&datetime, delta_t)?;
1259 spa_time_dependent_from_julian(jd)
1260}
1261
1262pub fn spa_time_dependent_from_julian(jd: JulianDate) -> Result<SpaTimeDependent> {
1272 let jme = jd.julian_ephemeris_millennium();
1273 let jce = jd.julian_ephemeris_century();
1274
1275 let l_terms = calculate_lbr_terms(jme, TERMS_L);
1277 let l_degrees = lbr_to_normalized_degrees(jme, &l_terms, TERMS_L.len());
1278
1279 let b_terms = calculate_lbr_terms(jme, TERMS_B);
1281 let b_degrees = lbr_to_normalized_degrees(jme, &b_terms, TERMS_B.len());
1282
1283 let r_terms = calculate_lbr_terms(jme, TERMS_R);
1285 let r = calculate_lbr_polynomial(jme, &r_terms, TERMS_R.len());
1286
1287 assert!(
1289 r != 0.0,
1290 "Earth radius vector is zero - astronomical impossibility"
1291 );
1292
1293 let theta_degrees = normalize_degrees_0_to_360(l_degrees + 180.0);
1295 let beta_degrees = -b_degrees;
1297
1298 let x_terms = calculate_nutation_terms(jce);
1300 let delta_psi_epsilon = calculate_delta_psi_epsilon(jce, &x_terms);
1301
1302 let epsilon_degrees =
1304 calculate_true_obliquity_of_ecliptic(&jd, delta_psi_epsilon.delta_epsilon);
1305
1306 let delta_tau = ABERRATION_CONSTANT / (SECONDS_PER_HOUR * r);
1308
1309 let lambda_degrees = theta_degrees + delta_psi_epsilon.delta_psi + delta_tau;
1311
1312 let nu_degrees = calculate_apparent_sidereal_time_at_greenwich(
1314 &jd,
1315 delta_psi_epsilon.delta_psi,
1316 epsilon_degrees,
1317 );
1318
1319 let beta = degrees_to_radians(beta_degrees);
1321 let epsilon = degrees_to_radians(epsilon_degrees);
1322 let lambda = degrees_to_radians(lambda_degrees);
1323 let alpha_degrees = calculate_geocentric_sun_right_ascension(beta, epsilon, lambda);
1324
1325 let delta_degrees =
1327 radians_to_degrees(calculate_geocentric_sun_declination(beta, epsilon, lambda));
1328
1329 Ok(SpaTimeDependent {
1330 theta_degrees,
1331 beta_degrees,
1332 r,
1333 delta_psi: delta_psi_epsilon.delta_psi,
1334 epsilon_degrees,
1335 lambda_degrees,
1336 nu_degrees,
1337 alpha_degrees,
1338 delta_degrees,
1339 })
1340}
1341
1342pub fn spa_with_time_dependent_parts(
1380 latitude: f64,
1381 longitude: f64,
1382 elevation: f64,
1383 refraction: Option<RefractionCorrection>,
1384 time_dependent: &SpaTimeDependent,
1385) -> Result<SolarPosition> {
1386 check_coordinates(latitude, longitude)?;
1387
1388 let nu_degrees = time_dependent.nu_degrees;
1391
1392 let h_degrees =
1394 normalize_degrees_0_to_360(nu_degrees + longitude - time_dependent.alpha_degrees);
1395 let h = degrees_to_radians(h_degrees);
1396
1397 let xi_degrees = 8.794 / (3600.0 * time_dependent.r);
1399 let xi = degrees_to_radians(xi_degrees);
1400 let phi = degrees_to_radians(latitude);
1401 let delta = degrees_to_radians(time_dependent.delta_degrees);
1402
1403 let u = atan(EARTH_FLATTENING_FACTOR * tan(phi));
1404 let y = mul_add(
1405 EARTH_FLATTENING_FACTOR,
1406 sin(u),
1407 (elevation / EARTH_RADIUS_METERS) * sin(phi),
1408 );
1409 let x = mul_add(elevation / EARTH_RADIUS_METERS, cos(phi), cos(u));
1410
1411 let delta_alpha_prime_degrees = radians_to_degrees(atan2(
1412 -x * sin(xi) * sin(h),
1413 mul_add(x * sin(xi), -cos(h), cos(delta)),
1414 ));
1415
1416 let delta_prime = radians_to_degrees(atan2(
1417 mul_add(y, -sin(xi), sin(delta)) * cos(degrees_to_radians(delta_alpha_prime_degrees)),
1418 mul_add(x * sin(xi), -cos(h), cos(delta)),
1419 ));
1420
1421 let h_prime_degrees = h_degrees - delta_alpha_prime_degrees;
1423
1424 let zenith_angle = radians_to_degrees(acos(mul_add(
1426 sin(degrees_to_radians(latitude)),
1427 sin(degrees_to_radians(delta_prime)),
1428 cos(degrees_to_radians(latitude))
1429 * cos(degrees_to_radians(delta_prime))
1430 * cos(degrees_to_radians(h_prime_degrees)),
1431 )));
1432
1433 let azimuth = normalize_degrees_0_to_360(
1435 180.0
1436 + radians_to_degrees(atan2(
1437 sin(degrees_to_radians(h_prime_degrees)),
1438 cos(degrees_to_radians(h_prime_degrees)) * sin(degrees_to_radians(latitude))
1439 - tan(degrees_to_radians(delta_prime)) * cos(degrees_to_radians(latitude)),
1440 )),
1441 );
1442
1443 let elevation_angle = 90.0 - zenith_angle;
1445 let final_zenith = refraction.map_or(zenith_angle, |correction| {
1446 if elevation_angle > SUNRISE_SUNSET_ANGLE {
1447 let pressure = correction.pressure();
1448 let temperature = correction.temperature();
1449 zenith_angle
1451 - (pressure / 1010.0) * (283.0 / (273.0 + temperature)) * 1.02
1452 / (60.0
1453 * tan(degrees_to_radians(
1454 elevation_angle + 10.3 / (elevation_angle + 5.11),
1455 )))
1456 } else {
1457 zenith_angle
1458 }
1459 });
1460
1461 SolarPosition::new(azimuth, final_zenith)
1462}
1463
1464#[cfg(all(test, feature = "chrono", feature = "std"))]
1465mod tests {
1466 use super::*;
1467 use chrono::{DateTime, FixedOffset};
1468 use std::collections::HashSet;
1469
1470 #[test]
1471 fn test_spa_basic_functionality() {
1472 let datetime = "2023-06-21T12:00:00Z"
1473 .parse::<DateTime<FixedOffset>>()
1474 .unwrap();
1475
1476 let result = solar_position(
1477 datetime,
1478 37.7749, -122.4194,
1480 0.0,
1481 69.0,
1482 Some(RefractionCorrection::new(1013.25, 15.0).unwrap()),
1483 );
1484
1485 assert!(result.is_ok());
1486 let position = result.unwrap();
1487 assert!(position.azimuth() >= 0.0 && position.azimuth() <= 360.0);
1488 assert!(position.zenith_angle() >= 0.0 && position.zenith_angle() <= 180.0);
1489 }
1490
1491 #[test]
1492 fn test_sunrise_sunset_multiple() {
1493 let datetime = "2023-06-21T12:00:00Z"
1494 .parse::<DateTime<FixedOffset>>()
1495 .unwrap();
1496 let horizons = [
1497 Horizon::SunriseSunset,
1498 Horizon::CivilTwilight,
1499 Horizon::NauticalTwilight,
1500 ];
1501
1502 let results: Result<Vec<_>> = sunrise_sunset_multiple(
1503 datetime,
1504 37.7749, -122.4194, 69.0, horizons.iter().copied(),
1508 )
1509 .collect();
1510
1511 let results = results.unwrap();
1512
1513 assert_eq!(results.len(), 3);
1515
1516 let returned_horizons: HashSet<_> = results.iter().map(|(h, _)| *h).collect();
1518 for expected_horizon in horizons {
1519 assert!(returned_horizons.contains(&expected_horizon));
1520 }
1521
1522 for (horizon, bulk_result) in &results {
1524 let individual_result =
1525 sunrise_sunset_for_horizon(datetime, 37.7749, -122.4194, 69.0, *horizon).unwrap();
1526
1527 match (&individual_result, bulk_result) {
1529 (
1530 crate::SunriseResult::RegularDay {
1531 sunrise: s1,
1532 transit: t1,
1533 sunset: ss1,
1534 },
1535 crate::SunriseResult::RegularDay {
1536 sunrise: s2,
1537 transit: t2,
1538 sunset: ss2,
1539 },
1540 ) => {
1541 assert_eq!(s1, s2);
1542 assert_eq!(t1, t2);
1543 assert_eq!(ss1, ss2);
1544 }
1545 (
1546 crate::SunriseResult::AllDay { transit: t1 },
1547 crate::SunriseResult::AllDay { transit: t2 },
1548 )
1549 | (
1550 crate::SunriseResult::AllNight { transit: t1 },
1551 crate::SunriseResult::AllNight { transit: t2 },
1552 ) => {
1553 assert_eq!(t1, t2);
1554 }
1555 _ => panic!("Bulk and individual results differ in type for {horizon:?}"),
1556 }
1557 }
1558 }
1559
1560 #[test]
1561 fn test_spa_no_refraction() {
1562 let datetime = "2023-06-21T12:00:00Z"
1563 .parse::<DateTime<FixedOffset>>()
1564 .unwrap();
1565
1566 let result = solar_position(datetime, 37.7749, -122.4194, 0.0, 69.0, None);
1567
1568 assert!(result.is_ok());
1569 let position = result.unwrap();
1570 assert!(position.azimuth() >= 0.0 && position.azimuth() <= 360.0);
1571 assert!(position.zenith_angle() >= 0.0 && position.zenith_angle() <= 180.0);
1572 }
1573
1574 #[test]
1575 fn test_spa_coordinate_validation() {
1576 let datetime = "2023-06-21T12:00:00Z"
1577 .parse::<DateTime<FixedOffset>>()
1578 .unwrap();
1579
1580 assert!(solar_position(
1582 datetime,
1583 95.0,
1584 0.0,
1585 0.0,
1586 0.0,
1587 Some(RefractionCorrection::new(1013.25, 15.0).unwrap())
1588 )
1589 .is_err());
1590
1591 assert!(solar_position(
1593 datetime,
1594 0.0,
1595 185.0,
1596 0.0,
1597 0.0,
1598 Some(RefractionCorrection::new(1013.25, 15.0).unwrap())
1599 )
1600 .is_err());
1601 }
1602
1603 #[test]
1604 fn test_sunrise_sunset_basic() {
1605 let date = "2023-06-21T00:00:00Z"
1606 .parse::<DateTime<FixedOffset>>()
1607 .unwrap();
1608
1609 let result = sunrise_sunset(date, 37.7749, -122.4194, 69.0, -0.833);
1610 assert!(result.is_ok());
1611
1612 let result =
1613 sunrise_sunset_for_horizon(date, 37.7749, -122.4194, 69.0, Horizon::SunriseSunset);
1614 assert!(result.is_ok());
1615 }
1616
1617 #[test]
1618 fn test_horizon_enum() {
1619 assert_eq!(Horizon::SunriseSunset.elevation_angle(), -0.83337);
1620 assert_eq!(Horizon::CivilTwilight.elevation_angle(), -6.0);
1621 assert_eq!(Horizon::Custom(-10.5).elevation_angle(), -10.5);
1622 }
1623}