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