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}