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#![cfg_attr(not(feature = "std"), no_std)]
6//!
7//! This library provides implementations of two complementary solar positioning algorithms:
8//! - **SPA** (Solar Position Algorithm): NREL's authoritative algorithm (±0.0003°, years -2000 to 6000)
9//! - **Grena3**: Simplified algorithm (±0.01°, years 2010-2110, ~10x faster)
10//!
11//! In addition, it provides an estimator for Delta T (ΔT) values based on the work of F. Espenak & J. Meeus.
12//!
13//! ## Features
14//!
15//! - Multiple configurations: `std` or `no_std`, with or without `chrono`, math via native or `libm`
16//! - Maximum accuracy: Authentic NREL SPA implementation, validated against reference data
17//! - Performance optimized: Split functions for bulk calculations (SPA only)
18//! - Thread-safe: Stateless, immutable data structures
19//!
20//! ## Feature Flags
21//!
22//! - `std` (default): Use standard library for native math functions (usually faster than `libm`)
23//! - `chrono` (default): Enable `DateTime<Tz>` based convenience API
24//! - `libm`: Use pure Rust math for `no_std` environments
25//!
26//! **Configuration examples:**
27//! ```toml
28//! # Default: std + chrono (most convenient)
29//! solar-positioning = "0.3"
30//!
31//! # Minimal std (no chrono, smallest dependency tree)
32//! solar-positioning = { version = "0.3", default-features = false, features = ["std"] }
33//!
34//! # no_std + chrono (embedded with DateTime support)
35//! solar-positioning = { version = "0.3", default-features = false, features = ["libm", "chrono"] }
36//!
37//! # Minimal no_std (pure numeric API)
38//! solar-positioning = { version = "0.3", default-features = false, features = ["libm"] }
39//! ```
40//!
41//! ## References
42//!
43//! - Reda, I.; Andreas, A. (2003). Solar position algorithm for solar radiation applications.
44//!   Solar Energy, 76(5), 577-589. DOI: <http://dx.doi.org/10.1016/j.solener.2003.12.003>
45//! - Grena, R. (2012). Five new algorithms for the computation of sun position from 2010 to 2110.
46//!   Solar Energy, 86(5), 1323-1337. DOI: <http://dx.doi.org/10.1016/j.solener.2012.01.024>
47//!
48//! ## Quick Start
49//!
50//! ### Solar Position (with chrono)
51//! ```rust
52//! # #[cfg(feature = "chrono")] {
53//! use solar_positioning::{spa, RefractionCorrection, time::DeltaT};
54//! use chrono::{DateTime, FixedOffset};
55//!
56//! // Calculate sun position for Vienna at noon
57//! let datetime = "2026-06-21T12:00:00+02:00".parse::<DateTime<FixedOffset>>().unwrap();
58//! let position = spa::solar_position(
59//!     datetime,
60//!     48.21,   // Vienna latitude
61//!     16.37,   // Vienna longitude
62//!     190.0,   // elevation (meters)
63//!     DeltaT::estimate_from_date_like(datetime).unwrap(), // delta T
64//!     Some(RefractionCorrection::standard())
65//! ).unwrap();
66//!
67//! println!("Azimuth: {:.3}°", position.azimuth());
68//! println!("Elevation: {:.3}°", position.elevation_angle());
69//! # }
70//! ```
71//!
72//! ### Solar Position (numeric API, no chrono)
73//! ```rust
74//! use solar_positioning::{spa, time::JulianDate, RefractionCorrection};
75//!
76//! // Create Julian date from UTC components (2026-06-21 12:00:00 UTC + 69s ΔT)
77//! let jd = JulianDate::from_utc(2026, 6, 21, 12, 0, 0.0, 69.0).unwrap();
78//!
79//! // Calculate sun position (works in both std and no_std)
80//! let position = spa::solar_position_from_julian(
81//!     jd,
82//!     48.21,   // Vienna latitude
83//!     16.37,   // Vienna longitude
84//!     190.0,   // elevation (meters)
85//!     Some(RefractionCorrection::standard())
86//! ).unwrap();
87//!
88//! println!("Azimuth: {:.3}°", position.azimuth());
89//! println!("Elevation: {:.3}°", position.elevation_angle());
90//! ```
91//!
92//! ### Sunrise and Sunset (with chrono)
93//! ```rust
94//! # #[cfg(feature = "chrono")] {
95//! use solar_positioning::{spa, Horizon, time::DeltaT};
96//! use chrono::{DateTime, FixedOffset};
97//!
98//! // Calculate sunrise/sunset for San Francisco
99//! let date = "2026-06-21T00:00:00-07:00".parse::<DateTime<FixedOffset>>().unwrap();
100//! let result = spa::sunrise_sunset_for_horizon(
101//!     date,
102//!     37.7749,  // San Francisco latitude
103//!     -122.4194, // San Francisco longitude
104//!     DeltaT::estimate_from_date_like(date).unwrap(),
105//!     Horizon::SunriseSunset
106//! ).unwrap();
107//!
108//! match result {
109//!     solar_positioning::SunriseResult::RegularDay { sunrise, transit, sunset } => {
110//!         println!("Sunrise: {}", sunrise);
111//!         println!("Solar noon: {}", transit);
112//!         println!("Sunset: {}", sunset);
113//!     }
114//!     _ => println!("No sunrise/sunset (polar day/night)"),
115//! }
116//! # }
117//! ```
118//!
119//! ### Sunrise and Sunset (numeric API, no chrono)
120//! ```rust
121//! use solar_positioning::{spa, Horizon};
122//!
123//! // Calculate sunrise/sunset for San Francisco (returns hours since midnight UTC)
124//! let result = spa::sunrise_sunset_utc_for_horizon(
125//!     2026, 6, 21,  // June 21, 2026
126//!     37.7749,      // San Francisco latitude
127//!     -122.4194,    // San Francisco longitude
128//!     69.0,         // ΔT (seconds)
129//!     Horizon::SunriseSunset
130//! ).unwrap();
131//!
132//! match result {
133//!     solar_positioning::SunriseResult::RegularDay { sunrise, transit, sunset } => {
134//!         println!("Sunrise: {:.2} hours UTC", sunrise.hours());
135//!         println!("Solar noon: {:.2} hours UTC", transit.hours());
136//!         println!("Sunset: {:.2} hours UTC", sunset.hours());
137//!     }
138//!     _ => println!("No sunrise/sunset (polar day/night)"),
139//! }
140//! ```
141//!
142//! ## Algorithms
143//!
144//! ### SPA (Solar Position Algorithm)
145//!
146//! Based on the NREL algorithm by Reda & Andreas (2003). Provides the highest accuracy
147//! with uncertainties of ±0.0003 degrees, suitable for applications requiring precise
148//! solar positioning over long time periods.
149//!
150//! ### Grena3
151//!
152//! A simplified algorithm optimized for years 2010-2110. Approximately 10 times faster
153//! than SPA while maintaining good accuracy (maximum error 0.01°).
154//!
155//! ## Coordinate System
156//!
157//! - **Azimuth**: 0° = North, measured clockwise (0° to 360°)
158//! - **Zenith angle**: 0° = directly overhead (zenith), 90° = horizon (0° to 180°)
159//! - **Elevation angle**: 0° = horizon, 90° = directly overhead (-90° to +90°)
160
161#![deny(missing_docs)]
162#![deny(unsafe_code)]
163#![warn(clippy::pedantic, clippy::nursery, clippy::cargo, clippy::all)]
164#![allow(
165    clippy::module_name_repetitions,
166    clippy::cast_possible_truncation,
167    clippy::cast_precision_loss,
168    clippy::cargo_common_metadata,
169    clippy::multiple_crate_versions, // Acceptable for dev-dependencies
170    clippy::float_cmp, // Exact comparisons of mathematical constants in tests
171    clippy::incompatible_msrv, // Functions work fine in 1.70, const context only needs 1.85+
172)]
173
174// Public API exports - core types only
175pub use crate::error::{Error, Result};
176pub use crate::types::{Horizon, HoursUtc, RefractionCorrection, SolarPosition, SunriseResult};
177
178// Algorithm modules
179pub mod grena3;
180pub mod spa;
181
182// Supporting modules
183pub mod error;
184pub mod time;
185pub mod types;
186
187// Internal modules
188mod math;
189
190#[cfg(all(test, feature = "chrono"))]
191mod tests {
192    use super::*;
193    use chrono::{DateTime, FixedOffset, TimeZone, Utc};
194
195    #[test]
196    fn test_basic_spa_calculation() {
197        // Test with different timezone types
198        let datetime_fixed = "2023-06-21T12:00:00-07:00"
199            .parse::<DateTime<FixedOffset>>()
200            .unwrap();
201        let datetime_utc = Utc.with_ymd_and_hms(2023, 6, 21, 19, 0, 0).unwrap();
202
203        let position1 = spa::solar_position(
204            datetime_fixed,
205            37.7749,
206            -122.4194,
207            0.0,
208            69.0,
209            Some(RefractionCorrection::standard()),
210        )
211        .unwrap();
212        let position2 = spa::solar_position(
213            datetime_utc,
214            37.7749,
215            -122.4194,
216            0.0,
217            69.0,
218            Some(RefractionCorrection::standard()),
219        )
220        .unwrap();
221
222        // Both should produce identical results
223        assert!((position1.azimuth() - position2.azimuth()).abs() < 1e-10);
224        assert!((position1.zenith_angle() - position2.zenith_angle()).abs() < 1e-10);
225
226        assert!(position1.azimuth() >= 0.0);
227        assert!(position1.azimuth() <= 360.0);
228        assert!(position1.zenith_angle() >= 0.0);
229        assert!(position1.zenith_angle() <= 180.0);
230    }
231
232    #[test]
233    fn test_basic_grena3_calculation() {
234        use chrono::{DateTime, FixedOffset, TimeZone, Utc};
235
236        let datetime_fixed = "2023-06-21T12:00:00-07:00"
237            .parse::<DateTime<FixedOffset>>()
238            .unwrap();
239        let datetime_utc = Utc.with_ymd_and_hms(2023, 6, 21, 19, 0, 0).unwrap();
240
241        let position1 = grena3::solar_position(
242            datetime_fixed,
243            37.7749,
244            -122.4194,
245            69.0,
246            Some(RefractionCorrection::new(1013.25, 15.0).unwrap()),
247        )
248        .unwrap();
249
250        let position2 = grena3::solar_position(
251            datetime_utc,
252            37.7749,
253            -122.4194,
254            69.0,
255            Some(RefractionCorrection::new(1013.25, 15.0).unwrap()),
256        )
257        .unwrap();
258
259        // Both should produce identical results
260        assert!((position1.azimuth() - position2.azimuth()).abs() < 1e-6);
261        assert!((position1.zenith_angle() - position2.zenith_angle()).abs() < 1e-6);
262
263        assert!(position1.azimuth() >= 0.0);
264        assert!(position1.azimuth() <= 360.0);
265        assert!(position1.zenith_angle() >= 0.0);
266        assert!(position1.zenith_angle() <= 180.0);
267    }
268}