Skip to main content

pvlib/
location.rs

1use chrono::DateTime;
2use chrono_tz::Tz;
3
4use crate::atmosphere;
5use crate::clearsky;
6use crate::solarposition::{self, SolarPosition};
7
8/// Represents a physical location on Earth.
9///
10/// `pvlib-python` equivalently uses `Location` class with attributes:
11/// latitude, longitude, tz, altitude, and name.
12#[derive(Debug, Clone, PartialEq)]
13pub struct Location {
14    pub latitude: f64,
15    pub longitude: f64,
16    pub tz: Tz,
17    pub altitude: f64,
18    pub name: String,
19}
20
21impl Location {
22    /// Create a new Location instance.
23    ///
24    /// # Arguments
25    ///
26    /// * `latitude` - Latitude in decimal degrees. Positive north of equator, negative to south.
27    /// * `longitude` - Longitude in decimal degrees. Positive east of prime meridian, negative to west.
28    /// * `tz` - Timezone as a `chrono_tz::Tz` enum variant.
29    /// * `altitude` - Altitude from sea level in meters.
30    /// * `name` - Name of the location.
31    pub fn new(latitude: f64, longitude: f64, tz: Tz, altitude: f64, name: &str) -> Self {
32        Self {
33            latitude,
34            longitude,
35            tz,
36            altitude,
37            name: name.to_string(),
38        }
39    }
40
41    /// Calculate the solar position for this location at the given time.
42    ///
43    /// Convenience wrapper around `solarposition::get_solarposition`.
44    pub fn get_solarposition(&self, time: DateTime<Tz>) -> Result<SolarPosition, spa::SpaError> {
45        solarposition::get_solarposition(self, time)
46    }
47
48    /// Calculate clear sky irradiance for this location at the given time.
49    ///
50    /// # Arguments
51    /// * `time` - Date and time with timezone.
52    /// * `model` - Clear sky model: "ineichen", "haurwitz", or "simplified_solis".
53    ///
54    /// # Returns
55    /// GHI, DNI, DHI in W/m^2. Returns zeros if the sun is below the horizon.
56    pub fn get_clearsky(&self, time: DateTime<Tz>, model: &str) -> clearsky::ClearSkyIrradiance {
57        let solar_pos = match self.get_solarposition(time) {
58            Ok(sp) => sp,
59            Err(_) => return clearsky::ClearSkyIrradiance { ghi: 0.0, dni: 0.0, dhi: 0.0 },
60        };
61
62        match model {
63            "haurwitz" => {
64                let ghi = clearsky::haurwitz(solar_pos.zenith);
65                clearsky::ClearSkyIrradiance { ghi, dni: 0.0, dhi: 0.0 }
66            }
67            "simplified_solis" => {
68                let apparent_elevation = 90.0 - solar_pos.zenith;
69                clearsky::simplified_solis(apparent_elevation, 0.1, 1.0, atmosphere::alt2pres(self.altitude))
70            }
71            _ => {
72                // Default to ineichen
73                let (_am_rel, am_abs) = self.get_airmass(time);
74                if am_abs.is_nan() || am_abs <= 0.0 {
75                    return clearsky::ClearSkyIrradiance { ghi: 0.0, dni: 0.0, dhi: 0.0 };
76                }
77                let month = {
78                    use chrono::Datelike;
79                    time.month()
80                };
81                let linke_turbidity = clearsky::lookup_linke_turbidity(self.latitude, self.longitude, month);
82                clearsky::ineichen(solar_pos.zenith, am_abs, linke_turbidity, self.altitude, 1364.0)
83            }
84        }
85    }
86
87    /// Calculate relative and absolute airmass for this location at the given time.
88    ///
89    /// Uses Kasten-Young model for relative airmass and site pressure derived
90    /// from altitude for absolute airmass.
91    ///
92    /// # Returns
93    /// `(airmass_relative, airmass_absolute)`. Values may be NaN if the sun is
94    /// below the horizon.
95    pub fn get_airmass(&self, time: DateTime<Tz>) -> (f64, f64) {
96        let solar_pos = match self.get_solarposition(time) {
97            Ok(sp) => sp,
98            Err(_) => return (f64::NAN, f64::NAN),
99        };
100
101        let am_rel = atmosphere::get_relative_airmass(solar_pos.zenith);
102        let pressure = atmosphere::alt2pres(self.altitude);
103        let am_abs = atmosphere::get_absolute_airmass(am_rel, pressure);
104        (am_rel, am_abs)
105    }
106}
107
108/// Lookup altitude for a given latitude and longitude.
109///
110/// This is a simplified approximation. Most populated areas are near sea level,
111/// so this returns 0.0 as a default. For accurate altitude data, use SRTM or
112/// similar elevation datasets.
113///
114/// # Arguments
115/// * `_latitude` - Latitude in decimal degrees.
116/// * `_longitude` - Longitude in decimal degrees.
117///
118/// # Returns
119/// Estimated altitude in meters above sea level.
120pub fn lookup_altitude(_latitude: f64, _longitude: f64) -> f64 {
121    // A proper implementation would query SRTM or similar elevation data.
122    // For now, return 0.0 (sea level) as a safe default.
123    0.0
124}