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