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 high-accuracy 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//! Supports both `std` (with chrono) and `no_std` (with libm) environments.
14//!
15//! ## References
16//!
17//! - Reda, I.; Andreas, A. (2003). Solar position algorithm for solar radiation applications.
18//!   Solar Energy, 76(5), 577-589. DOI: <http://dx.doi.org/10.1016/j.solener.2003.12.003>
19//! - Grena, R. (2012). Five new algorithms for the computation of sun position from 2010 to 2110.
20//!   Solar Energy, 86(5), 1323-1337. DOI: <http://dx.doi.org/10.1016/j.solener.2012.01.024>
21//!
22//! ## Features
23//!
24//! - Thread-safe, immutable data structures
25//! - Performance optimizations for coordinate sweeps (SPA only)
26//! - `no_std` support with `libm` feature (sunrise/sunset require `std`)
27//!
28//! ## Quick Start
29//!
30//! ### Solar Position (with `std`)
31//! ```rust
32//! # #[cfg(feature = "std")] {
33//! use solar_positioning::{spa, RefractionCorrection, time::DeltaT};
34//! use chrono::{DateTime, FixedOffset};
35//!
36//! // Calculate sun position for Vienna at noon
37//! let datetime = "2026-06-21T12:00:00+02:00".parse::<DateTime<FixedOffset>>().unwrap();
38//! let position = spa::solar_position(
39//!     datetime,
40//!     48.21,   // Vienna latitude
41//!     16.37,   // Vienna longitude
42//!     190.0,   // elevation (meters)
43//!     DeltaT::estimate_from_date_like(datetime).unwrap(), // delta T
44//!     Some(RefractionCorrection::standard())
45//! ).unwrap();
46//!
47//! println!("Azimuth: {:.3}°", position.azimuth());
48//! println!("Elevation: {:.3}°", position.elevation_angle());
49//! # }
50//! ```
51//!
52//! ### Solar Position (`no_std` mode)
53//! ```rust
54//! use solar_positioning::{spa, time::JulianDate, RefractionCorrection};
55//!
56//! // Create Julian date from components (2026-06-21 12:00:00 UTC)
57//! let jd = JulianDate::from_utc(2026, 6, 21, 12, 0, 0.0, 69.0).unwrap();
58//!
59//! // Calculate sun position
60//! let position = spa::solar_position_from_julian(
61//!     jd,
62//!     48.21,   // Vienna latitude
63//!     16.37,   // Vienna longitude
64//!     190.0,   // elevation (meters)
65//!     Some(RefractionCorrection::standard())
66//! ).unwrap();
67//!
68//! println!("Azimuth: {:.3}°", position.azimuth());
69//! println!("Elevation: {:.3}°", position.elevation_angle());
70//! ```
71//!
72//! ### Sunrise and Sunset (requires `std`)
73//! ```rust
74//! # #[cfg(feature = "std")] {
75//! use solar_positioning::{spa, Horizon, time::DeltaT};
76//! use chrono::{DateTime, FixedOffset};
77//!
78//! // Calculate sunrise/sunset for San Francisco
79//! let date = "2026-06-21T00:00:00-07:00".parse::<DateTime<FixedOffset>>().unwrap();
80//! let result = spa::sunrise_sunset_for_horizon(
81//!     date,
82//!     37.7749,  // San Francisco latitude
83//!     -122.4194, // San Francisco longitude
84//!     DeltaT::estimate_from_date_like(date).unwrap(),
85//!     Horizon::SunriseSunset
86//! ).unwrap();
87//!
88//! match result {
89//!     solar_positioning::SunriseResult::RegularDay { sunrise, transit, sunset } => {
90//!         println!("Sunrise: {}", sunrise);
91//!         println!("Solar noon: {}", transit);
92//!         println!("Sunset: {}", sunset);
93//!     }
94//!     _ => println!("No sunrise/sunset (polar day/night)"),
95//! }
96//! # }
97//! ```
98//!
99//! ## Algorithms
100//!
101//! ### SPA (Solar Position Algorithm)
102//!
103//! Based on the NREL algorithm by Reda & Andreas (2003). Provides the highest accuracy
104//! with uncertainties of ±0.0003 degrees, suitable for applications requiring precise
105//! solar positioning over long time periods.
106//!
107//! ### Grena3
108//!
109//! A simplified algorithm optimized for years 2010-2110. Approximately 10 times faster
110//! than SPA while maintaining good accuracy (maximum error 0.01°).
111//!
112//! ## Coordinate System
113//!
114//! - **Azimuth**: 0° = North, measured clockwise (0° to 360°)
115//! - **Zenith angle**: 0° = directly overhead (zenith), 90° = horizon (0° to 180°)
116//! - **Elevation angle**: 0° = horizon, 90° = directly overhead (-90° to +90°)
117
118#![deny(missing_docs)]
119#![deny(unsafe_code)]
120#![warn(clippy::pedantic, clippy::nursery, clippy::cargo, clippy::all)]
121#![allow(
122    clippy::module_name_repetitions,
123    clippy::cast_possible_truncation,
124    clippy::cast_precision_loss,
125    clippy::cargo_common_metadata,
126    clippy::multiple_crate_versions, // Acceptable for dev-dependencies
127    clippy::float_cmp, // Exact comparisons of mathematical constants in tests
128)]
129
130// Public API exports
131pub use crate::error::{Error, Result};
132#[cfg(feature = "std")]
133pub use crate::spa::spa_time_dependent_parts;
134pub use crate::spa::{SpaTimeDependent, spa_with_time_dependent_parts};
135pub use crate::types::{Horizon, RefractionCorrection, SolarPosition, SunriseResult};
136
137// Algorithm modules
138pub mod grena3;
139pub mod spa;
140
141// Core modules
142pub mod error;
143pub mod types;
144
145// Internal modules
146mod math;
147
148// Public modules
149pub mod time;
150
151#[cfg(test)]
152mod tests {
153    use super::*;
154
155    #[test]
156    fn test_basic_spa_calculation() {
157        use chrono::{DateTime, FixedOffset, TimeZone, Utc};
158
159        // Test with different timezone types
160        let datetime_fixed = "2023-06-21T12:00:00-07:00"
161            .parse::<DateTime<FixedOffset>>()
162            .unwrap();
163        let datetime_utc = Utc.with_ymd_and_hms(2023, 6, 21, 19, 0, 0).unwrap();
164
165        let position1 = spa::solar_position(
166            datetime_fixed,
167            37.7749,
168            -122.4194,
169            0.0,
170            69.0,
171            Some(RefractionCorrection::standard()),
172        )
173        .unwrap();
174        let position2 = spa::solar_position(
175            datetime_utc,
176            37.7749,
177            -122.4194,
178            0.0,
179            69.0,
180            Some(RefractionCorrection::standard()),
181        )
182        .unwrap();
183
184        // Both should produce identical results
185        assert!((position1.azimuth() - position2.azimuth()).abs() < 1e-10);
186        assert!((position1.zenith_angle() - position2.zenith_angle()).abs() < 1e-10);
187
188        assert!(position1.azimuth() >= 0.0);
189        assert!(position1.azimuth() <= 360.0);
190        assert!(position1.zenith_angle() >= 0.0);
191        assert!(position1.zenith_angle() <= 180.0);
192    }
193
194    #[test]
195    fn test_basic_grena3_calculation() {
196        use chrono::{DateTime, FixedOffset, TimeZone, Utc};
197
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 = grena3::solar_position(
204            datetime_fixed,
205            37.7749,
206            -122.4194,
207            69.0,
208            Some(RefractionCorrection::new(1013.25, 15.0).unwrap()),
209        )
210        .unwrap();
211
212        let position2 = grena3::solar_position(
213            datetime_utc,
214            37.7749,
215            -122.4194,
216            69.0,
217            Some(RefractionCorrection::new(1013.25, 15.0).unwrap()),
218        )
219        .unwrap();
220
221        // Both should produce identical results
222        assert!((position1.azimuth() - position2.azimuth()).abs() < 1e-6);
223        assert!((position1.zenith_angle() - position2.zenith_angle()).abs() < 1e-6);
224
225        assert!(position1.azimuth() >= 0.0);
226        assert!(position1.azimuth() <= 360.0);
227        assert!(position1.zenith_angle() >= 0.0);
228        assert!(position1.zenith_angle() <= 180.0);
229    }
230}