Source code for pvlib.location

"""
This module contains the Location class.
"""

# Will Holmgren, University of Arizona, 2014-2016.

import os
import datetime

import pandas as pd
import pytz
import h5py

from pvlib import solarposition, clearsky, atmosphere, irradiance
from pvlib.tools import _degrees_to_index

[docs]class Location: """ Location objects are convenient containers for latitude, longitude, timezone, and altitude data associated with a particular geographic location. You can also assign a name to a location object. Location objects have two timezone attributes: * ``tz`` is a IANA timezone string. * ``pytz`` is a pytz timezone object. Location objects support the print method. Parameters ---------- latitude : float. Positive is north of the equator. Use decimal degrees notation. longitude : float. Positive is east of the prime meridian. Use decimal degrees notation. tz : str, int, float, or pytz.timezone, default 'UTC'. See http://en.wikipedia.org/wiki/List_of_tz_database_time_zones for a list of valid time zones. pytz.timezone objects will be converted to strings. ints and floats must be in hours from UTC. altitude : float, default 0. Altitude from sea level in meters. name : string, optional Sets the name attribute of the Location object. See also -------- pvlib.pvsystem.PVSystem """
[docs] def __init__(self, latitude, longitude, tz='UTC', altitude=0, name=None): self.latitude = latitude self.longitude = longitude if isinstance(tz, str): self.tz = tz self.pytz = pytz.timezone(tz) elif isinstance(tz, datetime.timezone): self.tz = 'UTC' self.pytz = pytz.UTC elif isinstance(tz, datetime.tzinfo): self.tz = tz.zone self.pytz = tz elif isinstance(tz, (int, float)): self.tz = tz self.pytz = pytz.FixedOffset(tz*60) else: raise TypeError('Invalid tz specification') self.altitude = altitude self.name = name
def __repr__(self): attrs = ['name', 'latitude', 'longitude', 'altitude', 'tz'] return ('Location: \n ' + '\n '.join( f'{attr}: {getattr(self, attr)}' for attr in attrs))
[docs] @classmethod def from_tmy(cls, tmy_metadata, tmy_data=None, **kwargs): """ Create an object based on a metadata dictionary from tmy2 or tmy3 data readers. Parameters ---------- tmy_metadata : dict Returned from tmy.readtmy2 or tmy.readtmy3 tmy_data : DataFrame, optional Optionally attach the TMY data to this object. Returns ------- Location """ # not complete, but hopefully you get the idea. # might need code to handle the difference between tmy2 and tmy3 # determine if we're dealing with TMY2 or TMY3 data tmy2 = tmy_metadata.get('City', False) latitude = tmy_metadata['latitude'] longitude = tmy_metadata['longitude'] if tmy2: name = tmy_metadata['City'] else: name = tmy_metadata['Name'] tz = tmy_metadata['TZ'] altitude = tmy_metadata['altitude'] new_object = cls(latitude, longitude, tz=tz, altitude=altitude, name=name, **kwargs) # not sure if this should be assigned regardless of input. if tmy_data is not None: new_object.tmy_data = tmy_data new_object.weather = tmy_data return new_object
[docs] @classmethod def from_epw(cls, metadata, data=None, **kwargs): """ Create a Location object based on a metadata dictionary from epw data readers. Parameters ---------- metadata : dict Returned from epw.read_epw data : DataFrame, optional Optionally attach the epw data to this object. Returns ------- Location object (or the child class of Location that you called this method from). """ latitude = metadata['latitude'] longitude = metadata['longitude'] name = metadata['city'] tz = metadata['TZ'] altitude = metadata['altitude'] new_object = cls(latitude, longitude, tz=tz, altitude=altitude, name=name, **kwargs) if data is not None: new_object.weather = data return new_object
[docs] def get_solarposition(self, times, pressure=None, temperature=12, **kwargs): """ Uses the :py:func:`pvlib.solarposition.get_solarposition` function to calculate the solar zenith, azimuth, etc. at this location. Parameters ---------- times : pandas.DatetimeIndex Must be localized or UTC will be assumed. pressure : float, or array-like, optional If not specified, ``pressure`` is calculated using :py:func:`pvlib.atmosphere.alt2pres` and ``self.altitude``. temperature : float or array-like, default 12 kwargs passed to :py:func:`pvlib.solarposition.get_solarposition` Returns ------- solar_position : DataFrame Columns depend on the ``method`` kwarg, but always include ``zenith`` and ``azimuth``. The angles are in degrees. """ if pressure is None: pressure = atmosphere.alt2pres(self.altitude) return solarposition.get_solarposition(times, latitude=self.latitude, longitude=self.longitude, altitude=self.altitude, pressure=pressure, temperature=temperature, **kwargs)
[docs] def get_clearsky(self, times, model='ineichen', solar_position=None, dni_extra=None, **kwargs): """ Calculate the clear sky estimates of GHI, DNI, and/or DHI at this location. Parameters ---------- times: DatetimeIndex model: str, default 'ineichen' The clear sky model to use. Must be one of 'ineichen', 'haurwitz', 'simplified_solis'. solar_position : DataFrame, optional DataFrame with columns 'apparent_zenith', 'zenith', 'apparent_elevation'. dni_extra : numeric, optional If not specified, will be calculated from times. kwargs Extra parameters passed to the relevant functions. Climatological values are assumed in many cases. See source code for details! Returns ------- clearsky : DataFrame Column names are: ``ghi, dni, dhi``. """ if dni_extra is None: dni_extra = irradiance.get_extra_radiation(times) try: pressure = kwargs.pop('pressure') except KeyError: pressure = atmosphere.alt2pres(self.altitude) if solar_position is None: solar_position = self.get_solarposition(times, pressure=pressure) apparent_zenith = solar_position['apparent_zenith'] apparent_elevation = solar_position['apparent_elevation'] if model == 'ineichen': try: linke_turbidity = kwargs.pop('linke_turbidity') except KeyError: interp_turbidity = kwargs.pop('interp_turbidity', True) linke_turbidity = clearsky.lookup_linke_turbidity( times, self.latitude, self.longitude, interp_turbidity=interp_turbidity) try: airmass_absolute = kwargs.pop('airmass_absolute') except KeyError: airmass_absolute = self.get_airmass( times, solar_position=solar_position)['airmass_absolute'] cs = clearsky.ineichen(apparent_zenith, airmass_absolute, linke_turbidity, altitude=self.altitude, dni_extra=dni_extra, **kwargs) elif model == 'haurwitz': cs = clearsky.haurwitz(apparent_zenith) elif model == 'simplified_solis': cs = clearsky.simplified_solis( apparent_elevation, pressure=pressure, dni_extra=dni_extra, **kwargs) else: raise ValueError('{} is not a valid clear sky model. Must be ' 'one of ineichen, simplified_solis, haurwitz' .format(model)) return cs
[docs] def get_airmass(self, times=None, solar_position=None, model='kastenyoung1989'): """ Calculate the relative and absolute airmass. Automatically chooses zenith or apparant zenith depending on the selected model. Parameters ---------- times : DatetimeIndex, optional Only used if solar_position is not provided. solar_position : DataFrame, optional DataFrame with columns 'apparent_zenith', 'zenith'. model : str, default 'kastenyoung1989' Relative airmass model. See :py:func:`pvlib.atmosphere.get_relative_airmass` for a list of available models. Returns ------- airmass : DataFrame Columns are 'airmass_relative', 'airmass_absolute' See also -------- pvlib.atmosphere.get_relative_airmass """ if solar_position is None: solar_position = self.get_solarposition(times) if model in atmosphere.APPARENT_ZENITH_MODELS: zenith = solar_position['apparent_zenith'] elif model in atmosphere.TRUE_ZENITH_MODELS: zenith = solar_position['zenith'] else: raise ValueError(f'{model} is not a valid airmass model') airmass_relative = atmosphere.get_relative_airmass(zenith, model) pressure = atmosphere.alt2pres(self.altitude) airmass_absolute = atmosphere.get_absolute_airmass(airmass_relative, pressure) airmass = pd.DataFrame(index=solar_position.index) airmass['airmass_relative'] = airmass_relative airmass['airmass_absolute'] = airmass_absolute return airmass
[docs] def get_sun_rise_set_transit(self, times, method='pyephem', **kwargs): """ Calculate sunrise, sunset and transit times. Parameters ---------- times : DatetimeIndex Must be localized to the Location method : str, default 'pyephem' 'pyephem', 'spa', or 'geometric' kwargs : Passed to the relevant functions. See solarposition.sun_rise_set_transit_<method> for details. Returns ------- result : DataFrame Column names are: ``sunrise, sunset, transit``. """ if method == 'pyephem': result = solarposition.sun_rise_set_transit_ephem( times, self.latitude, self.longitude, **kwargs) elif method == 'spa': result = solarposition.sun_rise_set_transit_spa( times, self.latitude, self.longitude, **kwargs) elif method == 'geometric': sr, ss, tr = solarposition.sun_rise_set_transit_geometric( times, self.latitude, self.longitude, **kwargs) result = pd.DataFrame(index=times, data={'sunrise': sr, 'sunset': ss, 'transit': tr}) else: raise ValueError('{} is not a valid method. Must be ' 'one of pyephem, spa, geometric' .format(method)) return result
[docs]def lookup_altitude(latitude, longitude): """ Look up location altitude from low-resolution altitude map supplied with pvlib. The data for this map comes from multiple open data sources with varying resolutions aggregated by Mapzen. More details can be found here https://github.com/tilezen/joerd/blob/master/docs/data-sources.md Altitudes from this map are a coarse approximation and can have significant errors (100+ meters) introduced by downsampling and source data resolution. Parameters ---------- latitude : float. Positive is north of the equator. Use decimal degrees notation. longitude : float. Positive is east of the prime meridian. Use decimal degrees notation. Returns ------- altitude : float The altitude of the location in meters. Notes ----------- Attributions: * ArcticDEM terrain data DEM(s) were created from DigitalGlobe, Inc., imagery and funded under National Science Foundation awards 1043681, 1559691, and 1542736; * Australia terrain data © Commonwealth of Australia (Geoscience Australia) 2017; * Austria terrain data © offene Daten Österreichs - Digitales Geländemodell (DGM) Österreich; * Canada terrain data contains information licensed under the Open Government Licence - Canada; * Europe terrain data produced using Copernicus data and information funded by the European Union - EU-DEM layers; * Global ETOPO1 terrain data U.S. National Oceanic and Atmospheric Administration * Mexico terrain data source: INEGI, Continental relief, 2016; * New Zealand terrain data Copyright 2011 Crown copyright (c) Land Information New Zealand and the New Zealand Government (All rights reserved); * Norway terrain data © Kartverket; * United Kingdom terrain data © Environment Agency copyright and/or database right 2015. All rights reserved; * United States 3DEP (formerly NED) and global GMTED2010 and SRTM terrain data courtesy of the U.S. Geological Survey. References ---------- .. [1] `Mapzen, Linux foundation project for open data maps <https://www.mapzen.com/>`_ .. [2] `Joerd, tool for downloading and processing DEMs, Used by Mapzen <https://github.com/tilezen/joerd/>`_ .. [3] `AWS, Open Data Registry Terrain Tiles <https://registry.opendata.aws/terrain-tiles/>`_ """ pvlib_path = os.path.dirname(os.path.abspath(__file__)) filepath = os.path.join(pvlib_path, 'data', 'Altitude.h5') latitude_index = _degrees_to_index(latitude, coordinate='latitude') longitude_index = _degrees_to_index(longitude, coordinate='longitude') with h5py.File(filepath, 'r') as alt_h5_file: alt = alt_h5_file['Altitude'][latitude_index, longitude_index] # 255 is a special value that means nodata. Fallback to 0 if nodata. if alt == 255: return 0 # Altitude is encoded in 28 meter steps from -450 meters to 6561 meters # There are 0-254 possible altitudes, with 255 reserved for nodata. alt *= 28 alt -= 450 return float(alt)