1use chrono::{DateTime, Utc};
2use chrono_tz::Tz;
3use spa::{solar_position, SpaError, StdFloatOps};
4use crate::location::Location;
5
6use std::f64::consts::PI;
7
8#[derive(Debug, Clone, PartialEq)]
10pub struct SolarPosition {
11 pub zenith: f64,
13 pub azimuth: f64,
15 pub elevation: f64,
17}
18
19#[inline]
22pub fn get_solarposition(location: &Location, time: DateTime<Tz>) -> Result<SolarPosition, SpaError> {
23 let utc_time: DateTime<Utc> = time.with_timezone(&Utc);
24
25 let result = solar_position::<StdFloatOps>(utc_time, location.latitude, location.longitude)?;
26
27 Ok(SolarPosition {
28 zenith: result.zenith_angle,
29 azimuth: result.azimuth,
30 elevation: 90.0 - result.zenith_angle,
31 })
32}
33
34fn calculate_simple_day_angle(day_of_year: f64, offset: f64) -> f64 {
39 (2.0 * PI / 365.0) * (day_of_year - offset)
40}
41
42#[inline]
48pub fn equation_of_time_spencer71(day_of_year: f64) -> f64 {
49 let day_angle = calculate_simple_day_angle(day_of_year, 1.0);
50 (1440.0 / (2.0 * PI))
51 * (0.0000075
52 + 0.001868 * day_angle.cos()
53 - 0.032077 * day_angle.sin()
54 - 0.014615 * (2.0 * day_angle).cos()
55 - 0.040849 * (2.0 * day_angle).sin())
56}
57
58#[inline]
62pub fn equation_of_time_pvcdrom(day_of_year: f64) -> f64 {
63 let bday = calculate_simple_day_angle(day_of_year, 1.0) - (2.0 * PI / 365.0) * 80.0;
64 9.87 * (2.0 * bday).sin() - 7.53 * bday.cos() - 1.5 * bday.sin()
65}
66
67#[inline]
74pub fn declination_spencer71(day_of_year: f64) -> f64 {
75 let day_angle = calculate_simple_day_angle(day_of_year, 1.0);
76 0.006918
77 - 0.399912 * day_angle.cos()
78 + 0.070257 * day_angle.sin()
79 - 0.006758 * (2.0 * day_angle).cos()
80 + 0.000907 * (2.0 * day_angle).sin()
81 - 0.002697 * (3.0 * day_angle).cos()
82 + 0.00148 * (3.0 * day_angle).sin()
83}
84
85#[inline]
89pub fn declination_cooper69(day_of_year: f64) -> f64 {
90 let day_angle = calculate_simple_day_angle(day_of_year, 1.0);
91 (23.45_f64).to_radians() * (day_angle + (2.0 * PI / 365.0) * 285.0).sin()
92}
93
94#[inline]
103pub fn hour_angle(hours_from_midnight_utc: f64, longitude: f64, equation_of_time: f64) -> f64 {
104 15.0 * (hours_from_midnight_utc - 12.0) + longitude + equation_of_time / 4.0
105}
106
107#[inline]
113pub fn solar_zenith_analytical(latitude: f64, hour_angle: f64, declination: f64) -> f64 {
114 let cos_zenith = declination.cos() * latitude.cos() * hour_angle.cos()
115 + declination.sin() * latitude.sin();
116 cos_zenith.clamp(-1.0, 1.0).acos()
118}
119
120#[inline]
124pub fn solar_azimuth_analytical(
125 latitude: f64,
126 hour_angle: f64,
127 declination: f64,
128 zenith: f64,
129) -> f64 {
130 let numer = zenith.cos() * latitude.sin() - declination.sin();
131 let denom = zenith.sin() * latitude.cos();
132
133 let mut cos_azi = if denom.abs() < 1e-8 {
134 1.0
135 } else {
136 numer / denom
137 };
138
139 if (cos_azi - 1.0).abs() < 1e-8 {
141 cos_azi = 1.0;
142 }
143 if (cos_azi + 1.0).abs() < 1e-8 {
144 cos_azi = -1.0;
145 }
146 cos_azi = cos_azi.clamp(-1.0, 1.0);
147
148 let sign_ha = if hour_angle > 0.0 {
149 1.0
150 } else if hour_angle < 0.0 {
151 -1.0
152 } else {
153 0.0
154 };
155
156 sign_ha * cos_azi.acos() + PI
157}
158
159#[derive(Debug, Clone, PartialEq)]
163pub struct SunRiseSetTransit {
164 pub sunrise: f64,
166 pub sunset: f64,
168 pub transit: f64,
170}
171
172#[inline]
183pub fn sun_rise_set_transit_geometric(
184 latitude: f64,
185 longitude: f64,
186 declination: f64,
187 equation_of_time: f64,
188 utc_offset_hours: f64,
189) -> Option<SunRiseSetTransit> {
190 let latitude_rad = latitude.to_radians();
191 let tan_product = -declination.tan() * latitude_rad.tan();
192
193 if tan_product.abs() > 1.0 {
195 return None;
196 }
197
198 let sunset_angle = tan_product.acos().to_degrees(); let sunrise_angle = -sunset_angle;
200
201 let sunrise_hour =
203 (sunrise_angle - longitude - equation_of_time / 4.0) / 15.0 + 12.0 + utc_offset_hours;
204 let sunset_hour =
205 (sunset_angle - longitude - equation_of_time / 4.0) / 15.0 + 12.0 + utc_offset_hours;
206 let transit_hour =
207 (0.0 - longitude - equation_of_time / 4.0) / 15.0 + 12.0 + utc_offset_hours;
208
209 Some(SunRiseSetTransit {
210 sunrise: sunrise_hour,
211 sunset: sunset_hour,
212 transit: transit_hour,
213 })
214}
215
216#[inline]
222pub fn nrel_earthsun_distance(day_of_year: f64) -> f64 {
223 let day_angle = calculate_simple_day_angle(day_of_year, 1.0);
224 let r_over_r0_squared = 1.000110
225 + 0.034221 * day_angle.cos()
226 + 0.001280 * day_angle.sin()
227 + 0.000719 * (2.0 * day_angle).cos()
228 + 0.000077 * (2.0 * day_angle).sin();
229 1.0 / r_over_r0_squared.sqrt()
231}