diff --git a/benchmarks/benchmarks/location.py b/benchmarks/benchmarks/location.py index 57ce125846..6b476eb44f 100644 --- a/benchmarks/benchmarks/location.py +++ b/benchmarks/benchmarks/location.py @@ -8,12 +8,12 @@ def set_solar_position(obj): - obj.location = pvlib.location.Location(32, -110, altitude=700, - tz='Etc/GMT+7') + tz = 'Etc/GMT+7' + obj.location = pvlib.location.Location(32, -110, altitude=700) obj.times = pd.date_range(start='20180601', freq='3min', - periods=1440) + periods=1440, tz=tz) obj.days = pd.date_range(start='20180101', freq='d', periods=365, - tz=obj.location.tz) + tz=tz) obj.solar_position = obj.location.get_solarposition(obj.times) diff --git a/docs/examples/bifacial/plot_bifi_model_mc.py b/docs/examples/bifacial/plot_bifi_model_mc.py index f40e4e5d7b..5c8e28eab2 100644 --- a/docs/examples/bifacial/plot_bifi_model_mc.py +++ b/docs/examples/bifacial/plot_bifi_model_mc.py @@ -60,7 +60,7 @@ cec_inverter = cec_inverters['ABB__MICRO_0_25_I_OUTD_US_208__208V_'] # create a location for site, and get solar position and clearsky data -site_location = location.Location(lat, lon, tz=tz, name='Greensboro, NC') +site_location = location.Location(lat, lon, name='Greensboro, NC') solar_position = site_location.get_solarposition(times) cs = site_location.get_clearsky(times) diff --git a/docs/examples/bifacial/plot_bifi_model_pvwatts.py b/docs/examples/bifacial/plot_bifi_model_pvwatts.py index b6a6a582fb..03bb26632d 100644 --- a/docs/examples/bifacial/plot_bifi_model_pvwatts.py +++ b/docs/examples/bifacial/plot_bifi_model_pvwatts.py @@ -35,7 +35,7 @@ times = pd.date_range('2021-06-21', '2021-06-22', freq='1min', tz=tz) # create location object and get clearsky data -site_location = location.Location(lat, lon, tz=tz, name='Greensboro, NC') +site_location = location.Location(lat, lon, name='Greensboro, NC') cs = site_location.get_clearsky(times) # get solar position data diff --git a/docs/examples/bifacial/plot_pvfactors_fixed_tilt.py b/docs/examples/bifacial/plot_pvfactors_fixed_tilt.py index 28b06110c2..865346a369 100644 --- a/docs/examples/bifacial/plot_pvfactors_fixed_tilt.py +++ b/docs/examples/bifacial/plot_pvfactors_fixed_tilt.py @@ -31,7 +31,7 @@ # First, generate the usual modeling inputs: times = pd.date_range('2021-06-21', '2021-06-22', freq='1min', tz='Etc/GMT+5') -loc = location.Location(latitude=40, longitude=-80, tz=times.tz) +loc = location.Location(latitude=40, longitude=-80) sp = loc.get_solarposition(times) cs = loc.get_clearsky(times) diff --git a/docs/examples/irradiance-transposition/plot_ghi_transposition.py b/docs/examples/irradiance-transposition/plot_ghi_transposition.py index 5eeb27ca36..45646ce4bc 100644 --- a/docs/examples/irradiance-transposition/plot_ghi_transposition.py +++ b/docs/examples/irradiance-transposition/plot_ghi_transposition.py @@ -21,8 +21,8 @@ tz = 'MST' lat, lon = 39.755, -105.221 -# Create location object to store lat, lon, timezone -site = location.Location(lat, lon, tz=tz) +# Create location object to store lat and lon +site = location.Location(lat, lon) # Calculate clear-sky GHI and transpose to plane of array @@ -31,7 +31,7 @@ def get_irradiance(site_location, date, tilt, surface_azimuth): # Creates one day's worth of 10 min intervals times = pd.date_range(date, freq='10min', periods=6*24, - tz=site_location.tz) + tz=tz) # Generate clearsky data using the Ineichen model, which is the default # The get_clearsky method returns a dataframe with values for GHI, DNI, # and DHI diff --git a/docs/examples/irradiance-transposition/plot_interval_transposition_error.py b/docs/examples/irradiance-transposition/plot_interval_transposition_error.py index 76adfed932..e3a605a175 100644 --- a/docs/examples/irradiance-transposition/plot_interval_transposition_error.py +++ b/docs/examples/irradiance-transposition/plot_interval_transposition_error.py @@ -93,7 +93,7 @@ def transpose(irradiance, timeshift): # is negligible. # baseline: all calculations done at 1-second scale -location = pvlib.location.Location(40, -80, tz='Etc/GMT+5') +location = pvlib.location.Location(40, -80) times = pd.date_range('2019-06-01 05:00', '2019-06-01 19:00', freq='1s', tz='Etc/GMT+5') solpos = location.get_solarposition(times) diff --git a/docs/examples/shading/plot_simple_irradiance_adjustment_for_horizon_shading.py b/docs/examples/shading/plot_simple_irradiance_adjustment_for_horizon_shading.py index 7aa868fe6c..41f2006f6a 100644 --- a/docs/examples/shading/plot_simple_irradiance_adjustment_for_horizon_shading.py +++ b/docs/examples/shading/plot_simple_irradiance_adjustment_for_horizon_shading.py @@ -31,7 +31,7 @@ ) # Create location object, and get solar position and clearsky irradiance data. -location = pvlib.location.Location(latitude, longitude, tz) +location = pvlib.location.Location(latitude, longitude) solar_position = location.get_solarposition(times) clearsky = location.get_clearsky(times) diff --git a/docs/examples/spectrum/spectral_factor.py b/docs/examples/spectrum/spectral_factor.py index 83ee488fb4..75ac4c1bb3 100644 --- a/docs/examples/spectrum/spectral_factor.py +++ b/docs/examples/spectrum/spectral_factor.py @@ -65,7 +65,7 @@ lat, lon = metadata['latitude'], metadata['longitude'] alt = altitude = metadata['altitude'] tz = 'Etc/GMT+5' -loc = location.Location(lat, lon, tz=tz, name='Greensboro, NC') +loc = location.Location(lat, lon, name='Greensboro, NC') # Calculate solar position parameters solpos = loc.get_solarposition( diff --git a/docs/sphinx/source/whatsnew/v0.15.3.rst b/docs/sphinx/source/whatsnew/v0.15.3.rst index 8cf8515577..575ca6be07 100644 --- a/docs/sphinx/source/whatsnew/v0.15.3.rst +++ b/docs/sphinx/source/whatsnew/v0.15.3.rst @@ -9,6 +9,11 @@ Breaking Changes * Changed output type of :py:func:`pvlib.irradiance.get_total_irradiance`, :py:func:`pvlib.irradiance.get_sky_diffuse`, and :py:func:`pvlib.irradiance.poa_components` from ``OrderedDict`` to ``dict``. (:issue:`2750`, :pull:`2800`) +* Removed timezone-related APIs from :py:class:`pvlib.location.Location`: + ``tz``, ``pytz``, and ``_zoneinfo``. Removed + :py:func:`pvlib.location.lookup_timezone` and + :py:func:`pvlib.tools.localize_to_utc`. Use timezone-aware timestamps + instead of storing timezone state on ``Location``. (:pull:`2800`) Deprecations diff --git a/pvlib/location.py b/pvlib/location.py index e339e752f0..ed52776fad 100644 --- a/pvlib/location.py +++ b/pvlib/location.py @@ -5,29 +5,25 @@ # Will Holmgren, University of Arizona, 2014-2016. import pathlib -import datetime -import zoneinfo +from dataclasses import dataclass import pandas as pd -import pytz import h5py from pvlib import solarposition, clearsky, atmosphere, irradiance from pvlib.tools import _degrees_to_index -from pvlib._deprecation import warn_deprecated +from typing import TypeAlias -class Location: - """ - Location objects are convenient containers for latitude, longitude, - time zone, and altitude data associated with a particular geographic - location. You can also assign a name to a location object. +numeric: TypeAlias = int | float - Location objects have a time-zone attribute ``tz`` (IANA timezone string). - .. deprecated:: 0.15.2 - The ``pytz`` attribute is deprecated. Use ``tz`` instead. +@dataclass +class Location: + """ + Location objects are convenient containers for latitude, longitude, + altitude, and name data associated with a geographic location. Location objects support the print method. @@ -41,17 +37,6 @@ class Location: Positive is east of the prime meridian. Use decimal degrees notation. - tz : time zone as str, int, float, or datetime.tzinfo, default 'UTC'. - See http://en.wikipedia.org/wiki/List_of_tz_database_time_zones for a - list of valid name strings. An ``int`` or ``float`` must be a - whole-number hour offsets from UTC that can be converted to the - IANA-supported 'Etc/GMT-N' format. (Note the limited range of the - offset N and its sign-change convention.) Time zones from the - ``zoneinfo`` packages may also be passed. - - The `tz` attribute is represented as a valid IANA time zone name - string. - altitude : float, optional Altitude from sea level in meters. If not specified, the altitude will be fetched from @@ -61,90 +46,43 @@ class Location: name : string, optional Sets the name attribute of the Location object. - Raises - ------ - ValueError - when the time zone ``tz`` cannot be converted. - - zoneinfo.ZoneInfoNotFoundError - when the time zone ``tz`` is not recognizable as an IANA time zone by - the ``zoneinfo.ZoneInfo`` initializer used for internal time-zone - representation. - See also -------- pvlib.pvsystem.PVSystem """ - def __init__( - self, latitude, longitude, tz='UTC', altitude=None, name=None - ): - self.latitude = latitude - self.longitude = longitude - self.tz = tz - - if altitude is None: - altitude = lookup_altitude(latitude, longitude) + latitude: numeric + longitude: numeric + altitude: numeric | None = None + name: str | None = None - self.altitude = altitude - self.name = name + def __post_init__(self): + self._validate_value(self.latitude, 'latitude', -90, 90) + self._validate_value(self.longitude, 'longitude', -180, 180) + if self.altitude is None: + self.altitude = lookup_altitude(self.latitude, self.longitude) + else: + self._validate_value( + self.altitude, 'altitude', -430, 8848, + ) def __repr__(self): - attrs = ['name', 'latitude', 'longitude', 'altitude', 'tz'] + attrs = ['name', 'latitude', 'longitude', 'altitude'] # Use None as getattr default in case __repr__ is called during # initialization before all attributes have been assigned. return ('Location: \n ' + '\n '.join( f'{attr}: {getattr(self, attr, None)}' for attr in attrs)) - @property - def tz(self): - """The location's IANA time-zone string.""" - return str(self._zoneinfo) - - @tz.setter - def tz(self, tz_): - # self._zoneinfo holds single source of time-zone truth as IANA name. - if isinstance(tz_, str): - self._zoneinfo = zoneinfo.ZoneInfo(tz_) - elif isinstance(tz_, int): - tz_str = f"Etc/GMT{-tz_:+d}" # noqa: E231 - self._zoneinfo = zoneinfo.ZoneInfo(tz_str) - elif isinstance(tz_, float): - if tz_ % 1 != 0: - raise TypeError( - "Floating-point tz has non-zero fractional part: " - f"{tz_}. Only whole-number offsets are supported." - ) - - tz_str = f"Etc/GMT{-int(tz_):+d}" # noqa: E231 - self._zoneinfo = zoneinfo.ZoneInfo(tz_str) - elif isinstance(tz_, datetime.tzinfo): - # Includes time zones generated by zoneinfo packages. - self._zoneinfo = zoneinfo.ZoneInfo(str(tz_)) - else: - raise TypeError( - f"invalid tz specification: {tz_}, must be an IANA time zone " - "string, a whole-number int/float UTC offset, or a " - "datetime.tzinfo object (including subclasses)" + @staticmethod + def _validate_value(value: numeric, name: str, lower: numeric, upper: numeric): + if not isinstance(value, numeric): + raise TypeError(f'{name} must be a number, got {value!r}') + if not lower <= value <= upper: + raise ValueError( + f'{name} must be between {lower} and {upper}, ' + f'got {value!r}' ) - @property - def pytz(self): # pragma: no cover - """The location's pytz time zone (read only). - - .. deprecated:: 0.15.2 - The ``pytz`` attribute is deprecated. Use the ``tz`` property - instead. - """ - warn_deprecated( - since='0.15.2', - removal='0.17.0', - name='pytz', - obj_type='attribute', - alternative='tz', - ) - return pytz.timezone(str(self._zoneinfo)) - @classmethod def from_tmy(cls, tmy_metadata, tmy_data=None, **kwargs): """ @@ -177,10 +115,9 @@ def from_tmy(cls, tmy_metadata, tmy_data=None, **kwargs): else: name = tmy_metadata['Name'] - tz = tmy_metadata['TZ'] altitude = tmy_metadata['altitude'] - new_object = cls(latitude, longitude, tz=tz, altitude=altitude, + new_object = cls(latitude, longitude, altitude=altitude, name=name, **kwargs) # not sure if this should be assigned regardless of input. @@ -213,10 +150,9 @@ def from_epw(cls, metadata, data=None, **kwargs): name = metadata['city'] - tz = metadata['TZ'] altitude = metadata['altitude'] - new_object = cls(latitude, longitude, tz=tz, altitude=altitude, + new_object = cls(latitude, longitude, altitude=altitude, name=name, **kwargs) if data is not None: @@ -422,7 +358,7 @@ def get_sun_rise_set_transit(self, times, method='spa', **kwargs): return result -def lookup_altitude(latitude, longitude): +def lookup_altitude(latitude: numeric, longitude: numeric) -> float: """ Look up location altitude from low-resolution altitude map supplied with pvlib. The data for this map comes from multiple open data diff --git a/pvlib/tools.py b/pvlib/tools.py index 1d9db70369..80c6743163 100644 --- a/pvlib/tools.py +++ b/pvlib/tools.py @@ -119,34 +119,6 @@ def atand(number): res = np.degrees(np.arctan(number)) return res - -def localize_to_utc(time, location): - """ - Converts ``time`` to UTC, localizing if necessary using location. - - Parameters - ---------- - time : datetime.datetime, pandas.DatetimeIndex, - or pandas.Series/DataFrame with a DatetimeIndex. - location : pvlib.Location object (unused if ``time`` is localized) - - Returns - ------- - datetime.datetime or pandas object localized to UTC. - """ - if isinstance(time, dt.datetime): - if time.tzinfo is None: - time = time.replace(tzinfo=zoneinfo.ZoneInfo(location.tz)) - time_utc = time.astimezone(timezone.utc) - else: - try: - time_utc = time.tz_convert('UTC') - except TypeError: - time_utc = time.tz_localize(location.tz).tz_convert('UTC') - - return time_utc - - def datetime_to_djd(time): """ Converts a datetime to the Dublin Julian Day diff --git a/tests/conftest.py b/tests/conftest.py index 8a1c1180d8..578b2324e2 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,3 +1,4 @@ +import zoneinfo from pathlib import Path import platform import warnings @@ -236,13 +237,16 @@ def has_spa_c(): @pytest.fixture() -def golden(): - return Location(39.742476, -105.1786, 'America/Denver', 1830.14) +def golden_location(): + return Location(39.742476, -105.1786, 1830.14) +@pytest.fixture() +def golden_tz(): + return 'America/Denver' @pytest.fixture() -def golden_mst(): - return Location(39.742476, -105.1786, 'MST', 1830.14) +def golden_mst_tz(): + return 'MST' @pytest.fixture() diff --git a/tests/test_irradiance.py b/tests/test_irradiance.py index f5f1c7ebd6..1993c4cfcb 100644 --- a/tests/test_irradiance.py +++ b/tests/test_irradiance.py @@ -21,7 +21,7 @@ from pvlib._deprecation import pvlibDeprecationWarning # fixtures create realistic test input data -# test input data generated at Location(32.2, -111, 'US/Arizona', 700) +# test input data generated at Location(32.2, -111, 700) # test input data is hard coded to avoid dependencies on other parts of pvlib diff --git a/tests/test_location.py b/tests/test_location.py index 580a49982f..7ec55fb1ea 100644 --- a/tests/test_location.py +++ b/tests/test_location.py @@ -19,100 +19,24 @@ from .conftest import requires_ephem -def test_location_required(): - Location(32.2, -111) - - def test_location_all(): - Location(32.2, -111, 'US/Arizona', 700, 'Tucson') + Location(32.2, -111, 700, 'Tucson') @pytest.fixture() def some_location() -> Location: - return Location(32.2, -111, 'US/Arizona', 700, 'Tucson') - - -@pytest.mark.parametrize( - 'tz,tz_expected', [ - pytest.param('UTC', 'UTC'), - pytest.param('Etc/GMT+5', 'Etc/GMT+5'), - pytest.param('US/Mountain', 'US/Mountain'), - pytest.param('America/Phoenix', 'America/Phoenix'), - pytest.param('Asia/Kathmandu', 'Asia/Kathmandu'), - pytest.param('Asia/Yangon', 'Asia/Yangon'), - pytest.param(datetime.timezone.utc, 'UTC'), - pytest.param(zoneinfo.ZoneInfo('Etc/GMT-7'), 'Etc/GMT-7'), - pytest.param(zoneinfo.ZoneInfo('US/Arizona'), 'US/Arizona'), - pytest.param(-6, 'Etc/GMT+6'), - pytest.param(-11.0, 'Etc/GMT+11'), - pytest.param(12, 'Etc/GMT-12'), - ], -) -def test_location_tz(tz, tz_expected): - loc = Location(32.2, -111, tz) - assert isinstance(loc._zoneinfo, datetime.tzinfo) # Abstract base class. - assert type(loc.tz) is str - assert loc.tz == tz_expected - - -def test_location_tz_update(): - loc = Location(32.2, -111, -11) - assert loc.tz == 'Etc/GMT+11' - - # Updating Location's tz updates read-only time-zone attributes. - loc.tz = 7 - assert loc.tz == 'Etc/GMT-7' - - -@pytest.mark.parametrize( - 'tz', [ - 'invalid', - 'Etc/GMT+20', # offset too large. - 20, # offset too large. - ] -) -def test_location_invalid_tz(tz): - with pytest.raises(zoneinfo.ZoneInfoNotFoundError): - Location(32.2, -111, tz) - - -@pytest.mark.parametrize( - 'tz', [ - -9.5, # float with non-zero fractional part. - b"bytes not str", - [5], - ] -) -def test_location_invalid_tz_type(tz): - with pytest.raises(TypeError): - Location(32.2, -111, tz) - + return Location(32.2, -111, 700, 'Tucson') def test_location_print_all(): - tus = Location(32.2, -111, 'US/Arizona', 700, 'Tucson') - expected_str = '\n'.join([ - 'Location: ', - ' name: Tucson', - ' latitude: 32.2', - ' longitude: -111', - ' altitude: 700', - ' tz: US/Arizona' - ]) - assert tus.__str__() == expected_str - - -def test_location_print(): - tus = Location(32.2, -111, zoneinfo.ZoneInfo('US/Arizona'), 700, 'Tucson') + tus = Location(32.2, -111, 700, 'Tucson') expected_str = '\n'.join([ 'Location: ', ' name: Tucson', ' latitude: 32.2', ' longitude: -111', ' altitude: 700', - ' tz: US/Arizona' ]) assert tus.__str__() == expected_str - @pytest.fixture def times(): return pd.date_range(start='20160101T0600-0700', @@ -121,7 +45,7 @@ def times(): def test_get_clearsky(mocker, times): - tus = Location(32.2, -111, 'US/Arizona', 700, 'Tucson') + tus = Location(32.2, -111, 700, 'Tucson') m = mocker.spy(pvlib.clearsky, 'ineichen') out = tus.get_clearsky(times) assert m.call_count == 1 @@ -135,7 +59,7 @@ def test_get_clearsky(mocker, times): def test_get_clearsky_ineichen_supply_linke(mocker): - tus = Location(32.2, -111, 'US/Arizona', 700) + tus = Location(32.2, -111, 700) times = pd.date_range(start='2014-06-24-0700', end='2014-06-25-0700', freq='3h') mocker.spy(pvlib.clearsky, 'ineichen') @@ -152,7 +76,7 @@ def test_get_clearsky_ineichen_supply_linke(mocker): def test_get_clearsky_haurwitz(times): - tus = Location(32.2, -111, 'US/Arizona', 700, 'Tucson') + tus = Location(32.2, -111, 700, 'Tucson') clearsky = tus.get_clearsky(times, model='haurwitz') expected = pd.DataFrame(data=np.array( [[ 0. ], @@ -166,7 +90,7 @@ def test_get_clearsky_haurwitz(times): def test_get_clearsky_simplified_solis(times): - tus = Location(32.2, -111, 'US/Arizona', 700, 'Tucson') + tus = Location(32.2, -111, 700, 'Tucson') clearsky = tus.get_clearsky(times, model='simplified_solis') expected = pd.DataFrame(data=np. array([[ 0. , 0. , 0. ], @@ -181,7 +105,7 @@ def test_get_clearsky_simplified_solis(times): def test_get_clearsky_simplified_solis_apparent_elevation(times): - tus = Location(32.2, -111, 'US/Arizona', 700, 'Tucson') + tus = Location(32.2, -111, 700, 'Tucson') solar_position = {'apparent_elevation': pd.Series(80, index=times), 'apparent_zenith': pd.Series(10, index=times)} clearsky = tus.get_clearsky(times, model='simplified_solis', @@ -199,7 +123,7 @@ def test_get_clearsky_simplified_solis_apparent_elevation(times): def test_get_clearsky_simplified_solis_dni_extra(times): - tus = Location(32.2, -111, 'US/Arizona', 700, 'Tucson') + tus = Location(32.2, -111, 700, 'Tucson') clearsky = tus.get_clearsky(times, model='simplified_solis', dni_extra=1370) expected = pd.DataFrame(data=np. @@ -215,7 +139,7 @@ def test_get_clearsky_simplified_solis_dni_extra(times): def test_get_clearsky_simplified_solis_pressure(times): - tus = Location(32.2, -111, 'US/Arizona', 700, 'Tucson') + tus = Location(32.2, -111, 700, 'Tucson') clearsky = tus.get_clearsky(times, model='simplified_solis', pressure=95000) expected = pd.DataFrame(data=np. @@ -231,7 +155,7 @@ def test_get_clearsky_simplified_solis_pressure(times): def test_get_clearsky_simplified_solis_aod_pw(times): - tus = Location(32.2, -111, 'US/Arizona', 700, 'Tucson') + tus = Location(32.2, -111, 700, 'Tucson') clearsky = tus.get_clearsky(times, model='simplified_solis', aod700=0.25, precipitable_water=2.) expected = pd.DataFrame(data=np. @@ -247,7 +171,7 @@ def test_get_clearsky_simplified_solis_aod_pw(times): def test_get_clearsky_valueerror(times): - tus = Location(32.2, -111, 'US/Arizona', 700, 'Tucson') + tus = Location(32.2, -111, 700, 'Tucson') with pytest.raises(ValueError): tus.get_clearsky(times, model='invalid_model') @@ -259,7 +183,6 @@ def test_from_tmy_3(): loc = Location.from_tmy(meta, data) assert loc.name is not None assert loc.altitude != 0 - assert loc.tz != 'UTC' assert_frame_equal(loc.weather, data) @@ -270,7 +193,6 @@ def test_from_tmy_2(): loc = Location.from_tmy(meta, data) assert loc.name is not None assert loc.altitude != 0 - assert loc.tz != 'UTC' assert_frame_equal(loc.weather, data) @@ -281,14 +203,13 @@ def test_from_epw(): loc = Location.from_epw(meta, data) assert loc.name is not None assert loc.altitude != 0 - assert loc.tz != 'UTC' assert_frame_equal(loc.weather, data) -def test_get_solarposition(expected_solpos, golden_mst): +def test_get_solarposition(expected_solpos, golden_location, golden_mst_tz): times = pd.date_range(datetime.datetime(2003, 10, 17, 12, 30, 30), - periods=1, freq='D', tz=golden_mst.tz) - ephem_data = golden_mst.get_solarposition(times, temperature=11) + periods=1, freq='D', tz=golden_mst_tz) + ephem_data = golden_location.get_solarposition(times, temperature=11) ephem_data = np.round(ephem_data, 3) expected_solpos.index = times expected_solpos = np.round(expected_solpos, 3) @@ -296,7 +217,7 @@ def test_get_solarposition(expected_solpos, golden_mst): def test_get_airmass(times): - tus = Location(32.2, -111, 'US/Arizona', 700, 'Tucson') + tus = Location(32.2, -111, 700, 'Tucson') airmass = tus.get_airmass(times) expected = pd.DataFrame(data=np.array( [[ nan, nan], @@ -321,13 +242,13 @@ def test_get_airmass(times): def test_get_airmass_valueerror(times): - tus = Location(32.2, -111, 'US/Arizona', 700, 'Tucson') + tus = Location(32.2, -111, 700, 'Tucson') with pytest.raises(ValueError): tus.get_airmass(times, model='invalid_model') def test_Location___repr__(): - tus = Location(32.2, -111, 'US/Arizona', 700, 'Tucson') + tus = Location(32.2, -111, 700, 'Tucson') expected = '\n'.join([ 'Location: ', @@ -335,35 +256,34 @@ def test_Location___repr__(): ' latitude: 32.2', ' longitude: -111', ' altitude: 700', - ' tz: US/Arizona' ]) assert tus.__repr__() == expected @requires_ephem -def test_get_sun_rise_set_transit(golden): +def test_get_sun_rise_set_transit(golden_location): times = pd.DatetimeIndex(['2015-01-01 07:00:00', '2015-01-01 23:00:00'], tz='MST') - result = golden.get_sun_rise_set_transit(times, method='pyephem') + result = golden_location.get_sun_rise_set_transit(times, method='pyephem') assert all(result.columns == ['sunrise', 'sunset', 'transit']) - result = golden.get_sun_rise_set_transit(times, method='spa') + result = golden_location.get_sun_rise_set_transit(times, method='spa') assert all(result.columns == ['sunrise', 'sunset', 'transit']) dayofyear = 1 declination = declination_spencer71(dayofyear) eot = equation_of_time_spencer71(dayofyear) - result = golden.get_sun_rise_set_transit(times, method='geometric', - declination=declination, - equation_of_time=eot) + result = golden_location.get_sun_rise_set_transit(times, method='geometric', + declination=declination, + equation_of_time=eot) assert all(result.columns == ['sunrise', 'sunset', 'transit']) -def test_get_sun_rise_set_transit_valueerror(golden): +def test_get_sun_rise_set_transit_valueerror(golden_location): times = pd.DatetimeIndex(['2015-01-01 07:00:00', '2015-01-01 23:00:00'], tz='MST') with pytest.raises(ValueError): - golden.get_sun_rise_set_transit(times, method='eyeball') + golden_location.get_sun_rise_set_transit(times, method='eyeball') def test_extra_kwargs(): @@ -388,17 +308,66 @@ def test_lookup_altitude(lat, lon, expected_alt): def test_location_lookup_altitude(mocker): mocker.spy(location, 'lookup_altitude') - tus = Location(32.2, -111, 'US/Arizona', 700, 'Tucson') + tus = Location(32.2, -111, 700, 'Tucson') location.lookup_altitude.assert_not_called() assert tus.altitude == 700 location.lookup_altitude.reset_mock() - tus = Location(32.2, -111, 'US/Arizona') + tus = Location(32.2, -111) location.lookup_altitude.assert_called_once_with(32.2, -111) assert tus.altitude == location.lookup_altitude(32.2, -111) -@fail_on_pvlib_version('0.17.0') -def test_location_pytz_warning(some_location): - with pytest.warns(pvlibDeprecationWarning): - assert str(some_location.pytz) == 'US/Arizona' +@pytest.mark.parametrize( + 'latitude,longitude', [ + pytest.param(-90, -180, id='min-bounds'), + pytest.param(90, 180, id='max-bounds'), + ] +) +def test_location_coordinate_bounds(latitude, longitude): + loc = Location(latitude, longitude, altitude=0) + assert loc.latitude == latitude + assert loc.longitude == longitude + + +@pytest.mark.parametrize( + 'latitude,longitude', [ + pytest.param(-90.1, 0, id='latitude-too-low'), + pytest.param(90.1, 0, id='latitude-too-high'), + pytest.param(0, -180.1, id='longitude-too-low'), + pytest.param(0, 180.1, id='longitude-too-high'), + ] +) +def test_location_invalid_coordinate_range(latitude, longitude): + with pytest.raises(ValueError): + Location(latitude, longitude, altitude=0) + + +@pytest.mark.parametrize('value,name', [ + pytest.param('32.2', 'latitude', id='latitude-string'), + pytest.param(float('nan'), 'latitude', id='latitude-nan'), + pytest.param(float('inf'), 'longitude', id='longitude-inf'), +]) +def test_location_invalid_coordinate_type_or_finiteness(value, name): + kwargs = {'latitude': 32.2, 'longitude': -111, 'tz': 'UTC', 'altitude': 0} + kwargs[name] = value + with pytest.raises((TypeError, ValueError)): + Location(**kwargs) + + +@pytest.mark.parametrize('altitude', [-430, 8848]) +def test_location_altitude_bounds(altitude): + loc = Location(32.2, -111, altitude=altitude) + assert loc.altitude == altitude + + +@pytest.mark.parametrize('altitude', [-500, 10000]) +def test_location_invalid_altitude_range(altitude): + with pytest.raises(ValueError): + Location(32.2, -111, altitude=altitude) + + +def test_location_dataclass_equality(): + loc_1 = Location(32.2, -111, 700, 'Tucson') + loc_2 = Location(32.2, -111, 700, 'Tucson') + assert loc_1 == loc_2 diff --git a/tests/test_solarposition.py b/tests/test_solarposition.py index 9dc0488fe5..07d3eb9dd1 100644 --- a/tests/test_solarposition.py +++ b/tests/test_solarposition.py @@ -20,8 +20,8 @@ times = pd.date_range(start=datetime.datetime(2014, 6, 24), end=datetime.datetime(2014, 6, 26), freq='15min') -tus = Location(32.2, -111, 'US/Arizona', 700) # no DST issues possible -times_localized = times.tz_localize(tus.tz) +tus = Location(32.2, -111, 700) # no DST issues possible +tus_tz = zoneinfo.ZoneInfo('US/Arizona') tol = 5 @@ -90,11 +90,11 @@ def expected_rise_set_ephem(): # this doesn't mean that one code is better than the other. @requires_spa_c -def test_spa_c_physical(expected_solpos, golden_mst): +def test_spa_c_physical(expected_solpos, golden_location, golden_tz): times = pd.date_range(datetime.datetime(2003, 10, 17, 12, 30, 30), - periods=1, freq='D', tz=golden_mst.tz) - ephem_data = solarposition.spa_c(times, golden_mst.latitude, - golden_mst.longitude, + periods=1, freq='D', tz=golden_tz) + ephem_data = solarposition.spa_c(times, golden_location.latitude, + golden_location.longitude, pressure=82000, temperature=11) expected_solpos.index = times @@ -102,22 +102,22 @@ def test_spa_c_physical(expected_solpos, golden_mst): @requires_spa_c -def test_spa_c_physical_dst(expected_solpos, golden): +def test_spa_c_physical_dst(expected_solpos, golden_location): times = pd.date_range(datetime.datetime(2003, 10, 17, 13, 30, 30), - periods=1, freq='D', tz=golden.tz) - ephem_data = solarposition.spa_c(times, golden.latitude, - golden.longitude, + periods=1, freq='D', tz=golden_location.tz) + ephem_data = solarposition.spa_c(times, golden_location.latitude, + golden_location.longitude, pressure=82000, temperature=11) expected_solpos.index = times assert_frame_equal(expected_solpos, ephem_data[expected_solpos.columns]) -def test_spa_python_numpy_physical(expected_solpos, golden_mst): +def test_spa_python_numpy_physical(expected_solpos, golden_location, golden_mst_tz): times = pd.date_range(datetime.datetime(2003, 10, 17, 12, 30, 30), - periods=1, freq='D', tz=golden_mst.tz) - ephem_data = solarposition.spa_python(times, golden_mst.latitude, - golden_mst.longitude, + periods=1, freq='D', tz=golden_mst_tz) + ephem_data = solarposition.spa_python(times, golden_location.latitude, + golden_location.longitude, pressure=82000, temperature=11, delta_t=67, atmos_refract=0.5667, @@ -126,11 +126,11 @@ def test_spa_python_numpy_physical(expected_solpos, golden_mst): assert_frame_equal(expected_solpos, ephem_data[expected_solpos.columns]) -def test_spa_python_numpy_physical_dst(expected_solpos, golden): +def test_spa_python_numpy_physical_dst(expected_solpos, golden_location, golden_tz): times = pd.date_range(datetime.datetime(2003, 10, 17, 13, 30, 30), - periods=1, freq='D', tz=golden.tz) - ephem_data = solarposition.spa_python(times, golden.latitude, - golden.longitude, + periods=1, freq='D', tz=golden_tz) + ephem_data = solarposition.spa_python(times, golden_location.latitude, + golden_location.longitude, pressure=82000, temperature=11, delta_t=67, atmos_refract=0.5667, @@ -140,9 +140,9 @@ def test_spa_python_numpy_physical_dst(expected_solpos, golden): @pytest.mark.parametrize('delta_t', [65.0, None, np.array([65, 65])]) -def test_sun_rise_set_transit_spa(expected_rise_set_spa, golden, delta_t): +def test_sun_rise_set_transit_spa(expected_rise_set_spa, golden_location, delta_t): # solution from NLR SPA web calculator - south = Location(-35.0, 0.0, tz='UTC') + south = Location(-35.0, 0.0) times = pd.to_datetime(["1996-07-05", "2004-12-04"], utc=True) sunrise = pd.to_datetime(["1996-07-05 07:08:15", "2004-12-04 04:38:57"], utc=True) @@ -169,7 +169,7 @@ def test_sun_rise_set_transit_spa(expected_rise_set_spa, golden, delta_t): # test for Golden, CO compare to NLR SPA result = solarposition.sun_rise_set_transit_spa( - expected_rise_set_spa.index, golden.latitude, golden.longitude, + expected_rise_set_spa.index, golden_location.latitude, golden_location.longitude, delta_t=delta_t) # round to nearest minute @@ -237,11 +237,11 @@ def test_sun_rise_set_transit_spa_local_day(): @requires_ephem -def test_sun_rise_set_transit_ephem(expected_rise_set_ephem, golden): +def test_sun_rise_set_transit_ephem(expected_rise_set_ephem, golden_location): # test for Golden, CO compare to USNO, using local midnight result = solarposition.sun_rise_set_transit_ephem( - expected_rise_set_ephem.index, golden.latitude, golden.longitude, - next_or_previous='next', altitude=golden.altitude, pressure=0, + expected_rise_set_ephem.index, golden_location.latitude, golden_location.longitude, + next_or_previous='next', altitude=golden_location.altitude, pressure=0, temperature=11, horizon='-0:34') # round to nearest minute result_rounded = pd.DataFrame(index=result.index) @@ -272,10 +272,10 @@ def test_sun_rise_set_transit_ephem(expected_rise_set_ephem, golden): expected_rise_set_ephem.loc[idx_transit, 'transit'].tolist() result = solarposition.sun_rise_set_transit_ephem(times, - golden.latitude, - golden.longitude, + golden_location.latitude, + golden_location.longitude, next_or_previous='next', - altitude=golden.altitude, + altitude=golden_location.altitude, pressure=0, temperature=11, horizon='-0:34') @@ -309,8 +309,8 @@ def test_sun_rise_set_transit_ephem(expected_rise_set_ephem, golden): result = solarposition.sun_rise_set_transit_ephem( times, - golden.latitude, golden.longitude, next_or_previous='previous', - altitude=golden.altitude, pressure=0, temperature=11, horizon='-0:34') + golden_location.latitude, golden_location.longitude, next_or_previous='previous', + altitude=golden_location.altitude, pressure=0, temperature=11, horizon='-0:34') # round to nearest minute result_rounded = pd.DataFrame(index=result.index) for col, data in result.items(): @@ -324,8 +324,8 @@ def test_sun_rise_set_transit_ephem(expected_rise_set_ephem, golden): expected[col] = data.dt.tz_convert('UTC') result = solarposition.sun_rise_set_transit_ephem( times, - golden.latitude, golden.longitude, next_or_previous='previous', - altitude=golden.altitude, pressure=0, temperature=11, horizon='-0:34') + golden_location.latitude, golden_location.longitude, next_or_previous='previous', + altitude=golden_location.altitude, pressure=0, temperature=11, horizon='-0:34') # round to nearest minute result_rounded = pd.DataFrame(index=result.index) for col, data in result.items(): @@ -334,31 +334,31 @@ def test_sun_rise_set_transit_ephem(expected_rise_set_ephem, golden): @requires_ephem -def test_sun_rise_set_transit_ephem_error(expected_rise_set_ephem, golden): +def test_sun_rise_set_transit_ephem_error(expected_rise_set_ephem, golden_location): with pytest.raises(ValueError): solarposition.sun_rise_set_transit_ephem(expected_rise_set_ephem.index, - golden.latitude, - golden.longitude, + golden_location.latitude, + golden_location.longitude, next_or_previous='other') tz_naive = pd.DatetimeIndex([datetime.datetime(2015, 1, 2, 3, 0, 0)]) with pytest.raises(ValueError): solarposition.sun_rise_set_transit_ephem(tz_naive, - golden.latitude, - golden.longitude, + golden_location.latitude, + golden_location.longitude, next_or_previous='next') @requires_ephem -def test_sun_rise_set_transit_ephem_horizon(golden): +def test_sun_rise_set_transit_ephem_horizon(golden_location): times = pd.DatetimeIndex([datetime.datetime(2016, 1, 3, 0, 0, 0) ]).tz_localize('MST') # center of sun disk center = solarposition.sun_rise_set_transit_ephem( times, - latitude=golden.latitude, longitude=golden.longitude) + latitude=golden_location.latitude, longitude=golden_location.longitude) edge = solarposition.sun_rise_set_transit_ephem( times, - latitude=golden.latitude, longitude=golden.longitude, horizon='-0:34') + latitude=golden_location.latitude, longitude=golden_location.longitude, horizon='-0:34') result_rounded = (edge['sunrise'] - center['sunrise']).dt.round('min') sunrise_delta = datetime.datetime(2016, 1, 3, 7, 17, 11) - \ @@ -370,11 +370,11 @@ def test_sun_rise_set_transit_ephem_horizon(golden): @requires_ephem -def test_pyephem_physical(expected_solpos, golden_mst): +def test_pyephem_physical(expected_solpos, golden_location, golden_mst_tz): times = pd.date_range(datetime.datetime(2003, 10, 17, 12, 30, 30), - periods=1, freq='D', tz=golden_mst.tz) - ephem_data = solarposition.pyephem(times, golden_mst.latitude, - golden_mst.longitude, pressure=82000, + periods=1, freq='D', tz=golden_mst_tz) + ephem_data = solarposition.pyephem(times, golden_location.latitude, + golden_location.longitude, pressure=82000, temperature=11) expected_solpos.index = times assert_frame_equal(expected_solpos.round(2), @@ -382,11 +382,11 @@ def test_pyephem_physical(expected_solpos, golden_mst): @requires_ephem -def test_pyephem_physical_dst(expected_solpos, golden): +def test_pyephem_physical_dst(expected_solpos, golden_location, golden_tz): times = pd.date_range(datetime.datetime(2003, 10, 17, 13, 30, 30), - periods=1, freq='D', tz=golden.tz) - ephem_data = solarposition.pyephem(times, golden.latitude, - golden.longitude, pressure=82000, + periods=1, freq='D', tz=golden_tz) + ephem_data = solarposition.pyephem(times, golden_location.latitude, + golden_location.longitude, pressure=82000, temperature=11) expected_solpos.index = times assert_frame_equal(expected_solpos.round(2), @@ -401,10 +401,9 @@ def test_calc_time(): loc = tus loc.pressure = 0 - tz = zoneinfo.ZoneInfo(loc.tz) - actual_time = datetime.datetime(2014, 10, 10, 8, 30, tzinfo=tz) - lb = datetime.datetime(2014, 10, 10, tol, tzinfo=tz) - ub = datetime.datetime(2014, 10, 10, 10, tzinfo=tz) + actual_time = datetime.datetime(2014, 10, 10, 8, 30, tzinfo=tus_tz) + lb = datetime.datetime(2014, 10, 10, tol, tzinfo=tus_tz) + ub = datetime.datetime(2014, 10, 10, 10, tzinfo=tus_tz) alt = solarposition.calc_time(lb, ub, loc.latitude, loc.longitude, 'alt', math.radians(24.7)) az = solarposition.calc_time(lb, ub, loc.latitude, loc.longitude, @@ -425,11 +424,11 @@ def test_earthsun_distance(): assert_allclose(1, distance, atol=0.1) -def test_ephemeris_physical(expected_solpos, golden_mst): +def test_ephemeris_physical(expected_solpos, golden_location, golden_mst_tz): times = pd.date_range(datetime.datetime(2003, 10, 17, 12, 30, 30), - periods=1, freq='D', tz=golden_mst.tz) - ephem_data = solarposition.ephemeris(times, golden_mst.latitude, - golden_mst.longitude, + periods=1, freq='D', tz=golden_mst_tz) + ephem_data = solarposition.ephemeris(times, golden_location.latitude, + golden_location.longitude, pressure=82000, temperature=11) expected_solpos.index = times @@ -438,11 +437,11 @@ def test_ephemeris_physical(expected_solpos, golden_mst): assert_frame_equal(expected_solpos, ephem_data[expected_solpos.columns]) -def test_ephemeris_physical_dst(expected_solpos, golden): +def test_ephemeris_physical_dst(expected_solpos, golden_location, golden_tz): times = pd.date_range(datetime.datetime(2003, 10, 17, 13, 30, 30), - periods=1, freq='D', tz=golden.tz) - ephem_data = solarposition.ephemeris(times, golden.latitude, - golden.longitude, pressure=82000, + periods=1, freq='D', tz=golden_tz) + ephem_data = solarposition.ephemeris(times, golden_location.latitude, + golden_location.longitude, pressure=82000, temperature=11) expected_solpos.index = times expected_solpos = np.round(expected_solpos, 2) @@ -450,11 +449,11 @@ def test_ephemeris_physical_dst(expected_solpos, golden): assert_frame_equal(expected_solpos, ephem_data[expected_solpos.columns]) -def test_ephemeris_physical_no_tz(expected_solpos, golden_mst): +def test_ephemeris_physical_no_tz(expected_solpos, golden_location): times = pd.date_range(datetime.datetime(2003, 10, 17, 19, 30, 30), periods=1, freq='D') - ephem_data = solarposition.ephemeris(times, golden_mst.latitude, - golden_mst.longitude, + ephem_data = solarposition.ephemeris(times, golden_location.latitude, + golden_location.longitude, pressure=82000, temperature=11) expected_solpos.index = times @@ -463,12 +462,12 @@ def test_ephemeris_physical_no_tz(expected_solpos, golden_mst): assert_frame_equal(expected_solpos, ephem_data[expected_solpos.columns]) -def test_get_solarposition_error(golden): +def test_get_solarposition_error(golden_location, golden_tz): times = pd.date_range(datetime.datetime(2003, 10, 17, 13, 30, 30), - periods=1, freq='D', tz=golden.tz) + periods=1, freq='D', tz=golden_tz) with pytest.raises(ValueError): - solarposition.get_solarposition(times, golden.latitude, - golden.longitude, + solarposition.get_solarposition(times, golden_location.latitude, + golden_location.longitude, pressure=82000, temperature=11, method='error this') @@ -484,11 +483,11 @@ def test_get_solarposition_error(golden): index=['2003-10-17T12:30:30Z'])) ]) def test_get_solarposition_pressure( - pressure, expected, golden, expected_solpos): + pressure, expected, golden_location, golden_tz, expected_solpos): times = pd.date_range(datetime.datetime(2003, 10, 17, 13, 30, 30), - periods=1, freq='D', tz=golden.tz) - ephem_data = solarposition.get_solarposition(times, golden.latitude, - golden.longitude, + periods=1, freq='D', tz=golden_tz) + ephem_data = solarposition.get_solarposition(times, golden_location.latitude, + golden_location.longitude, pressure=pressure, temperature=11) if isinstance(expected, str) and expected == 'expected_solpos': @@ -510,11 +509,11 @@ def test_get_solarposition_pressure( index=['2003-10-17T12:30:30Z'])) ]) def test_get_solarposition_altitude( - altitude, expected, golden, expected_solpos): + altitude, expected, golden_location, golden_tz, expected_solpos): times = pd.date_range(datetime.datetime(2003, 10, 17, 13, 30, 30), - periods=1, freq='D', tz=golden.tz) - ephem_data = solarposition.get_solarposition(times, golden.latitude, - golden.longitude, + periods=1, freq='D', tz=golden_tz) + ephem_data = solarposition.get_solarposition(times, golden_location.latitude, + golden_location.longitude, altitude=altitude, temperature=11) if isinstance(expected, str) and expected == 'expected_solpos': @@ -536,14 +535,14 @@ def test_get_solarposition_altitude( (np.array([67.0, 67.0]), 'nrel_numpy'), ]) def test_get_solarposition_deltat(delta_t, method, expected_solpos_multi, - golden): + golden_location, golden_tz): times = pd.date_range(datetime.datetime(2003, 10, 17, 13, 30, 30), - periods=2, freq='D', tz=golden.tz) + periods=2, freq='D', tz=golden_tz) with warnings.catch_warnings(): # don't warn on method reload warnings.simplefilter("ignore") - ephem_data = solarposition.get_solarposition(times, golden.latitude, - golden.longitude, + ephem_data = solarposition.get_solarposition(times, golden_location.latitude, + golden_location.longitude, pressure=82000, delta_t=delta_t, temperature=11, @@ -570,11 +569,11 @@ def test_spa_array_delta_t(method): assert_series_equal(ephem_data['azimuth'], expected, check_names=False) -def test_get_solarposition_no_kwargs(expected_solpos, golden): +def test_get_solarposition_no_kwargs(expected_solpos, golden_location, golden_tz): times = pd.date_range(datetime.datetime(2003, 10, 17, 13, 30, 30), - periods=1, freq='D', tz=golden.tz) - ephem_data = solarposition.get_solarposition(times, golden.latitude, - golden.longitude) + periods=1, freq='D', tz=golden_tz) + ephem_data = solarposition.get_solarposition(times, golden_location.latitude, + golden_location.longitude) expected_solpos.index = times expected_solpos = np.round(expected_solpos, 2) ephem_data = np.round(ephem_data, 2) @@ -582,11 +581,11 @@ def test_get_solarposition_no_kwargs(expected_solpos, golden): @requires_ephem -def test_get_solarposition_method_pyephem(expected_solpos, golden): +def test_get_solarposition_method_pyephem(expected_solpos, golden_location, golden_tz): times = pd.date_range(datetime.datetime(2003, 10, 17, 13, 30, 30), - periods=1, freq='D', tz=golden.tz) - ephem_data = solarposition.get_solarposition(times, golden.latitude, - golden.longitude, + periods=1, freq='D', tz=golden_tz) + ephem_data = solarposition.get_solarposition(times, golden_location.latitude, + golden_location.longitude, method='pyephem') expected_solpos.index = times expected_solpos = np.round(expected_solpos, 2) @@ -807,12 +806,12 @@ def test_hour_angle_with_tricky_timezones(): solarposition.hour_angle(times, longitude, eot) -def test_sun_rise_set_transit_geometric(expected_rise_set_spa, golden_mst): +def test_sun_rise_set_transit_geometric(expected_rise_set_spa, golden_location, golden_mst_tz): """Test geometric calculations for sunrise, sunset, and transit times""" times = expected_rise_set_spa.index times_utc = times.tz_convert('UTC') - latitude = golden_mst.latitude - longitude = golden_mst.longitude + latitude = golden_location.latitude + longitude = golden_location.longitude eot = solarposition.equation_of_time_spencer71( times_utc.dayofyear) # minutes decl = solarposition.declination_spencer71(times_utc.dayofyear) # radians @@ -831,11 +830,11 @@ def test_sun_rise_set_transit_geometric(expected_rise_set_spa, golden_mst): test_transit = solarposition._times_to_hours_after_local_midnight(st) # convert expected SPA sunrise, sunset, transit to local datetime indices expected_sunrise = pd.DatetimeIndex(expected_rise_set_spa.sunrise.values, - tz='UTC').tz_convert(golden_mst.tz) + tz='UTC').tz_convert(golden_mst_tz) expected_sunset = pd.DatetimeIndex(expected_rise_set_spa.sunset.values, - tz='UTC').tz_convert(golden_mst.tz) + tz='UTC').tz_convert(golden_mst_tz) expected_transit = pd.DatetimeIndex(expected_rise_set_spa.transit.values, - tz='UTC').tz_convert(golden_mst.tz) + tz='UTC').tz_convert(golden_mst_tz) # convert expected times to hours since midnight as arrays of floats expected_sunrise = solarposition._times_to_hours_after_local_midnight( expected_sunrise) @@ -983,23 +982,23 @@ def test_rise_set_transit_geometric_microsecond_index(tz): # put numba tests at end of file to minimize reloading @requires_numba -def test_spa_python_numba_physical(expected_solpos, golden_mst): +def test_spa_python_numba_physical(expected_solpos, golden_location, golden_mst_tz): times = pd.date_range(datetime.datetime(2003, 10, 17, 12, 30, 30), - periods=1, freq='D', tz=golden_mst.tz) + periods=1, freq='D', tz=golden_mst_tz) with warnings.catch_warnings(): # don't warn on method reload # ensure that numpy is the most recently used method so that # we can use the warns filter below warnings.simplefilter("ignore") - ephem_data = solarposition.spa_python(times, golden_mst.latitude, - golden_mst.longitude, + ephem_data = solarposition.spa_python(times, golden_location.latitude, + golden_location.longitude, pressure=82000, temperature=11, delta_t=67, atmos_refract=0.5667, how='numpy', numthreads=1) with pytest.warns(UserWarning): - ephem_data = solarposition.spa_python(times, golden_mst.latitude, - golden_mst.longitude, + ephem_data = solarposition.spa_python(times, golden_location.latitude, + golden_location.longitude, pressure=82000, temperature=11, delta_t=67, atmos_refract=0.5667, @@ -1009,15 +1008,15 @@ def test_spa_python_numba_physical(expected_solpos, golden_mst): @requires_numba -def test_spa_python_numba_physical_dst(expected_solpos, golden): +def test_spa_python_numba_physical_dst(expected_solpos, golden_location, golden_tz): times = pd.date_range(datetime.datetime(2003, 10, 17, 13, 30, 30), - periods=1, freq='D', tz=golden.tz) + periods=1, freq='D', tz=golden_tz) with warnings.catch_warnings(): # don't warn on method reload warnings.simplefilter("ignore") - ephem_data = solarposition.spa_python(times, golden.latitude, - golden.longitude, pressure=82000, + ephem_data = solarposition.spa_python(times, golden_location.latitude, + golden_location.longitude, pressure=82000, temperature=11, delta_t=67, atmos_refract=0.5667, how='numba', numthreads=1) @@ -1026,8 +1025,8 @@ def test_spa_python_numba_physical_dst(expected_solpos, golden): with pytest.warns(UserWarning): # test that we get a warning when reloading to use numpy only - ephem_data = solarposition.spa_python(times, golden.latitude, - golden.longitude, + ephem_data = solarposition.spa_python(times, golden_location.latitude, + golden_location.longitude, pressure=82000, temperature=11, delta_t=67, atmos_refract=0.5667, diff --git a/tests/test_tools.py b/tests/test_tools.py index 4b733ad711..e9ab531731 100644 --- a/tests/test_tools.py +++ b/tests/test_tools.py @@ -154,92 +154,6 @@ def test_normalize_max2one(data_in, expected): assert_allclose(result, expected) -def test_localize_to_utc(): - lat, lon = 43.2, -77.6 - tz = "Etc/GMT+5" - loc = location.Location(lat, lon, tz=tz) - year, month, day, hour, minute, second = 1974, 6, 22, 18, 30, 15 - hour_utc = hour + 5 - - # Test all combinations of supported inputs. - dt_time_aware_utc = datetime( - year, month, day, hour_utc, minute, second, tzinfo=ZoneInfo("UTC") - ) - dt_time_aware = datetime( - year, month, day, hour, minute, second, tzinfo=ZoneInfo(tz) - ) - assert tools.localize_to_utc(dt_time_aware, None) == dt_time_aware_utc - dt_time_naive = datetime(year, month, day, hour, minute, second) - assert tools.localize_to_utc(dt_time_naive, loc) == dt_time_aware_utc - - # FIXME Derive timestamp strings from above variables. - dt_index_aware_utc = pd.DatetimeIndex( - [dt_time_aware_utc.strftime("%Y-%m-%dT%H:%M:%S")], tz=ZoneInfo("UTC") - ) - dt_index_aware = pd.DatetimeIndex( - [dt_time_aware.strftime("%Y-%m-%dT%H:%M:%S")], tz=ZoneInfo(tz) - ) - assert tools.localize_to_utc(dt_index_aware, None) == dt_index_aware_utc - dt_index_naive = pd.DatetimeIndex( - [dt_time_naive.strftime("%Y-%m-%dT%H:%M:%S")] - ) - assert tools.localize_to_utc(dt_index_naive, loc) == dt_index_aware_utc - - # Older pandas versions have wonky dtype equality check on timestamp - # index, so check the values as numpy.ndarray and indices one by one. - series_time_aware_utc_expected = pd.Series([24.42], dt_index_aware_utc) - series_time_aware = pd.Series([24.42], index=dt_index_aware) - series_time_aware_utc_got = tools.localize_to_utc(series_time_aware, None) - np.testing.assert_array_equal( - series_time_aware_utc_got.to_numpy(), - series_time_aware_utc_expected.to_numpy(), - ) - - for index_got, index_expected in zip( - series_time_aware_utc_got.index, series_time_aware_utc_expected.index - ): - assert index_got == index_expected - - series_time_naive = pd.Series([24.42], index=dt_index_naive) - series_time_naive_utc_got = tools.localize_to_utc(series_time_naive, loc) - np.testing.assert_array_equal( - series_time_naive_utc_got.to_numpy(), - series_time_aware_utc_expected.to_numpy(), - ) - - for index_got, index_expected in zip( - series_time_naive_utc_got.index, series_time_aware_utc_expected.index - ): - assert index_got == index_expected - - # Older pandas versions have wonky dtype equality check on timestamp - # index, so check the values as numpy.ndarray and indices one by one. - df_time_aware_utc_expected = pd.DataFrame([[24.42]], dt_index_aware) - df_time_naive = pd.DataFrame([[24.42]], index=dt_index_naive) - df_time_naive_utc_got = tools.localize_to_utc(df_time_naive, loc) - np.testing.assert_array_equal( - df_time_naive_utc_got.to_numpy(), - df_time_aware_utc_expected.to_numpy(), - ) - - for index_got, index_expected in zip( - df_time_naive_utc_got.index, df_time_aware_utc_expected.index - ): - assert index_got == index_expected - - df_time_aware = pd.DataFrame([[24.42]], index=dt_index_aware) - df_time_aware_utc_got = tools.localize_to_utc(df_time_aware, None) - np.testing.assert_array_equal( - df_time_aware_utc_got.to_numpy(), - df_time_aware_utc_expected.to_numpy(), - ) - - for index_got, index_expected in zip( - df_time_aware_utc_got.index, df_time_aware_utc_expected.index - ): - assert index_got == index_expected - - def test_datetime_to_djd(): expected = 27201.47934027778 dt_aware = datetime(1974, 6, 22, 18, 30, 15, tzinfo=ZoneInfo("Etc/GMT+5"))