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