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}