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//! ## Coordinate System
58//!
59//! - **Azimuth**: 0° = North, measured clockwise (0° to 360°)
60//! - **Zenith angle**: 0° = directly overhead (zenith), 90° = horizon (0° to 180°)
61//! - **Elevation angle**: 0° = horizon, 90° = directly overhead (-90° to 90°)
62
63#![cfg_attr(not(feature = "std"), no_std)]
64#![deny(missing_docs)]
65#![deny(unsafe_code)]
66#![warn(clippy::pedantic, clippy::nursery, clippy::cargo, clippy::all)]
67#![allow(
68 clippy::module_name_repetitions,
69 clippy::cast_possible_truncation,
70 clippy::cast_precision_loss,
71 clippy::cargo_common_metadata,
72 clippy::multiple_crate_versions, // Acceptable for dev-dependencies
73 clippy::float_cmp, // Exact comparisons of mathematical constants in tests
74)]
75
76// Public API exports
77pub use crate::error::{Error, Result};
78pub use crate::spa::{SpaTimeDependent, spa_time_dependent_parts, spa_with_time_dependent_parts};
79pub use crate::types::{Horizon, RefractionCorrection, SolarPosition, SunriseResult};
80
81// Algorithm modules
82pub mod grena3;
83pub mod spa;
84
85// Core modules
86pub mod error;
87pub mod types;
88
89// Internal modules
90mod math;
91
92// Public modules
93pub mod time;
94
95#[cfg(test)]
96mod tests {
97 use super::*;
98
99 #[test]
100 fn test_basic_spa_calculation() {
101 use chrono::{DateTime, FixedOffset, TimeZone, Utc};
102
103 // Test with different timezone types
104 let datetime_fixed = "2023-06-21T12:00:00-07:00"
105 .parse::<DateTime<FixedOffset>>()
106 .unwrap();
107 let datetime_utc = Utc.with_ymd_and_hms(2023, 6, 21, 19, 0, 0).unwrap();
108
109 let position1 = spa::solar_position(
110 datetime_fixed,
111 37.7749,
112 -122.4194,
113 0.0,
114 69.0,
115 Some(RefractionCorrection::standard()),
116 )
117 .unwrap();
118 let position2 = spa::solar_position(
119 datetime_utc,
120 37.7749,
121 -122.4194,
122 0.0,
123 69.0,
124 Some(RefractionCorrection::standard()),
125 )
126 .unwrap();
127
128 // Both should produce identical results
129 assert!((position1.azimuth() - position2.azimuth()).abs() < 1e-10);
130 assert!((position1.zenith_angle() - position2.zenith_angle()).abs() < 1e-10);
131
132 assert!(position1.azimuth() >= 0.0);
133 assert!(position1.azimuth() <= 360.0);
134 assert!(position1.zenith_angle() >= 0.0);
135 assert!(position1.zenith_angle() <= 180.0);
136 }
137
138 #[test]
139 fn test_basic_grena3_calculation() {
140 use chrono::{DateTime, FixedOffset, TimeZone, Utc};
141
142 let datetime_fixed = "2023-06-21T12:00:00-07:00"
143 .parse::<DateTime<FixedOffset>>()
144 .unwrap();
145 let datetime_utc = Utc.with_ymd_and_hms(2023, 6, 21, 19, 0, 0).unwrap();
146
147 let position1 = grena3::solar_position(
148 datetime_fixed,
149 37.7749,
150 -122.4194,
151 69.0,
152 Some(RefractionCorrection::new(1013.25, 15.0).unwrap()),
153 )
154 .unwrap();
155
156 let position2 = grena3::solar_position(
157 datetime_utc,
158 37.7749,
159 -122.4194,
160 69.0,
161 Some(RefractionCorrection::new(1013.25, 15.0).unwrap()),
162 )
163 .unwrap();
164
165 // Both should produce identical results
166 assert!((position1.azimuth() - position2.azimuth()).abs() < 1e-6);
167 assert!((position1.zenith_angle() - position2.zenith_angle()).abs() < 1e-6);
168
169 assert!(position1.azimuth() >= 0.0);
170 assert!(position1.azimuth() <= 360.0);
171 assert!(position1.zenith_angle() >= 0.0);
172 assert!(position1.zenith_angle() <= 180.0);
173 }
174}