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