solar_positioning/
lib.rs

1//! # Solar Positioning Library
2//!
3//! High-accuracy solar positioning algorithms for calculating sun position and sunrise/sunset times.
4//!
5//! This library provides implementations of two complementary solar positioning algorithms:
6//! - **SPA** (Solar Position Algorithm): NREL's high-accuracy algorithm (±0.0003° uncertainty, years -2000 to 6000)
7//! - **Grena3**: Simplified algorithm (±0.01° accuracy, years 2010-2110, ~10x faster)
8//!
9//! ## References
10//!
11//! - Reda, I.; Andreas, A. (2003). Solar position algorithm for solar radiation applications.
12//!   Solar Energy, 76(5), 577-589. DOI: <http://dx.doi.org/10.1016/j.solener.2003.12.003>
13//! - Grena, R. (2012). Five new algorithms for the computation of sun position from 2010 to 2110.
14//!   Solar Energy, 86(5), 1323-1337. DOI: <http://dx.doi.org/10.1016/j.solener.2012.01.024>
15//!
16//! ## Features
17//!
18//! - Thread-safe, immutable data structures
19//! - Performance optimizations for bulk calculations (SPA only, 6-7x speedup)
20//! - Comprehensive test suite with reference data validation
21//!
22//! ## Quick Start
23//!
24//! ```rust
25//! use solar_positioning::{spa, time::JulianDate, types::SolarPosition, RefractionCorrection};
26//! use chrono::{DateTime, FixedOffset, Utc, TimeZone};
27//!
28//! // Example with time calculations
29//! let jd = JulianDate::from_utc(2023, 6, 21, 12, 0, 0.0, 69.0).unwrap();
30//! println!("Julian Date: {:.6}", jd.julian_date());
31//! println!("Julian Century: {:.6}", jd.julian_century());
32//!
33//! // Example with flexible timezone support - any TimeZone trait implementor
34//! let datetime_fixed = "2023-06-21T12:00:00-07:00".parse::<DateTime<FixedOffset>>().unwrap();
35//! let datetime_utc = Utc.with_ymd_and_hms(2023, 6, 21, 19, 0, 0).unwrap(); // Same moment
36//!
37//! // Both calls produce identical results
38//! let position = spa::solar_position(datetime_fixed, 37.7749, -122.4194, 0.0, 69.0,
39//!     Some(RefractionCorrection::standard())).unwrap();
40//! println!("Azimuth: {:.3}°", position.azimuth());
41//! println!("Elevation: {:.3}°", position.elevation_angle());
42//! ```
43//!
44//! ## Algorithms
45//!
46//! ### SPA (Solar Position Algorithm)
47//!
48//! Based on the NREL algorithm by Reda & Andreas (2003). Provides the highest accuracy
49//! with uncertainties of ±0.0003 degrees, suitable for applications requiring precise
50//! solar positioning over long time periods.
51//!
52//! ### Grena3
53//!
54//! A simplified algorithm optimized for years 2010-2110. Approximately 10 times faster
55//! than SPA while maintaining good accuracy (maximum error 0.01 degrees).
56//!
57//! ## Coordinate System
58//!
59//! - **Azimuth**: 0° = North, measured clockwise (0° to 360°)
60//! - **Zenith angle**: 0° = directly overhead (zenith), 90° = horizon (0° to 180°)
61//! - **Elevation angle**: 0° = horizon, 90° = directly overhead (-90° to 90°)
62
63#![cfg_attr(not(feature = "std"), no_std)]
64#![deny(missing_docs)]
65#![deny(unsafe_code)]
66#![warn(clippy::pedantic, clippy::nursery, clippy::cargo, clippy::all)]
67#![allow(
68    clippy::module_name_repetitions,
69    clippy::cast_possible_truncation,
70    clippy::cast_precision_loss,
71    clippy::cargo_common_metadata,
72    clippy::multiple_crate_versions, // Acceptable for dev-dependencies
73    clippy::float_cmp, // Exact comparisons of mathematical constants in tests
74)]
75
76// Public API exports
77pub use crate::error::{Error, Result};
78pub use crate::spa::{SpaTimeDependent, spa_time_dependent_parts, spa_with_time_dependent_parts};
79pub use crate::types::{Horizon, RefractionCorrection, SolarPosition, SunriseResult};
80
81// Algorithm modules
82pub mod grena3;
83pub mod spa;
84
85// Core modules
86pub mod error;
87pub mod types;
88
89// Internal modules
90mod math;
91
92// Public modules
93pub mod time;
94
95#[cfg(test)]
96mod tests {
97    use super::*;
98
99    #[test]
100    fn test_basic_spa_calculation() {
101        use chrono::{DateTime, FixedOffset, TimeZone, Utc};
102
103        // Test with different timezone types
104        let datetime_fixed = "2023-06-21T12:00:00-07:00"
105            .parse::<DateTime<FixedOffset>>()
106            .unwrap();
107        let datetime_utc = Utc.with_ymd_and_hms(2023, 6, 21, 19, 0, 0).unwrap();
108
109        let position1 = spa::solar_position(
110            datetime_fixed,
111            37.7749,
112            -122.4194,
113            0.0,
114            69.0,
115            Some(RefractionCorrection::standard()),
116        )
117        .unwrap();
118        let position2 = spa::solar_position(
119            datetime_utc,
120            37.7749,
121            -122.4194,
122            0.0,
123            69.0,
124            Some(RefractionCorrection::standard()),
125        )
126        .unwrap();
127
128        // Both should produce identical results
129        assert!((position1.azimuth() - position2.azimuth()).abs() < 1e-10);
130        assert!((position1.zenith_angle() - position2.zenith_angle()).abs() < 1e-10);
131
132        assert!(position1.azimuth() >= 0.0);
133        assert!(position1.azimuth() <= 360.0);
134        assert!(position1.zenith_angle() >= 0.0);
135        assert!(position1.zenith_angle() <= 180.0);
136    }
137
138    #[test]
139    fn test_basic_grena3_calculation() {
140        use chrono::{DateTime, FixedOffset, TimeZone, Utc};
141
142        let datetime_fixed = "2023-06-21T12:00:00-07:00"
143            .parse::<DateTime<FixedOffset>>()
144            .unwrap();
145        let datetime_utc = Utc.with_ymd_and_hms(2023, 6, 21, 19, 0, 0).unwrap();
146
147        let position1 = grena3::solar_position(
148            datetime_fixed,
149            37.7749,
150            -122.4194,
151            69.0,
152            Some(RefractionCorrection::new(1013.25, 15.0).unwrap()),
153        )
154        .unwrap();
155
156        let position2 = grena3::solar_position(
157            datetime_utc,
158            37.7749,
159            -122.4194,
160            69.0,
161            Some(RefractionCorrection::new(1013.25, 15.0).unwrap()),
162        )
163        .unwrap();
164
165        // Both should produce identical results
166        assert!((position1.azimuth() - position2.azimuth()).abs() < 1e-6);
167        assert!((position1.zenith_angle() - position2.zenith_angle()).abs() < 1e-6);
168
169        assert!(position1.azimuth() >= 0.0);
170        assert!(position1.azimuth() <= 360.0);
171        assert!(position1.zenith_angle() >= 0.0);
172        assert!(position1.zenith_angle() <= 180.0);
173    }
174}