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