diff --git a/.gitignore b/.gitignore index c4ae6e8b..6393f2f3 100644 --- a/.gitignore +++ b/.gitignore @@ -23,3 +23,6 @@ atlite/version.py paper .coverage* !.coveragerc + +# Ignore PyCharm / JetBrains IDE project files +.idea/ diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 71fc03d6..d036d4b3 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -8,7 +8,7 @@ repos: hooks: - id: check-yaml - repo: https://github.com/psf/black - rev: 22.8.0 + rev: 22.10.0 hooks: - id: black - repo: https://github.com/fsfe/reuse-tool diff --git a/RELEASE_NOTES.rst b/RELEASE_NOTES.rst index 451f5b26..a84ecfcf 100644 --- a/RELEASE_NOTES.rst +++ b/RELEASE_NOTES.rst @@ -11,6 +11,13 @@ Release Notes .. Upcoming Release .. ================= +Upcoming Release +================ + +* Bugfix: For certain time spans, the ERA5 influx data would be incorrectly shifted by 12 hours. + This is now fixed and influx data is **always** shifted by minus 30 minutes. + See `#256 `_ for details. + Version 0.2.9 ============= diff --git a/atlite/datasets/era5.py b/atlite/datasets/era5.py index 9c608ffd..c65ac71e 100644 --- a/atlite/datasets/era5.py +++ b/atlite/datasets/era5.py @@ -160,17 +160,7 @@ def get_data_influx(retrieval_params): # Do not show DeprecationWarning from new SolarPosition calculation (#199) with warnings.catch_warnings(): warnings.simplefilter("ignore", DeprecationWarning) - # Convert dt / time frequency to timedelta and shift solar position by half - # (freqs like ["H","30T"] do not work with pd.to_timedelta(...) - time_shift = ( - -1 - / 2 - * pd.to_timedelta( - pd.date_range( - "1970-01-01", periods=1, freq=pd.infer_freq(ds["time"]) - ).freq - ) - ) + time_shift = pd.to_timedelta("-30 minutes") sp = SolarPosition(ds, time_shift=time_shift) sp = sp.rename({v: f"solar_{v}" for v in sp.data_vars}) diff --git a/test/test_preparation_and_conversion.py b/test/test_preparation_and_conversion.py index ff4aa3db..522a8be8 100755 --- a/test/test_preparation_and_conversion.py +++ b/test/test_preparation_and_conversion.py @@ -29,6 +29,11 @@ # %% Predefine tests for cutout +TIME = "2013-01-01" +BOUNDS = (-4, 56, 1.5, 62) +SARAH_DIR = os.getenv("SARAH_DIR", "/home/vres/climate-data/sarah_v2") +GEBCO_PATH = os.getenv("GEBCO_PATH", "/home/vres/climate-data/GEBCO_2014_2D.nc") + def all_notnull_test(cutout): """Test if no nan's in the prepared data occur""" @@ -59,7 +64,7 @@ def wrong_recreation(cutout): Cutout(path=cutout.path, module="somethingelse") -def pv_test(cutout): +def pv_test(cutout, time=TIME): """ Test the atlite.Cutout.pv function with different settings. Compare optimal orientation with flat orientation. @@ -76,7 +81,7 @@ def pv_test(cutout): ) assert production.notnull().all() - assert production.sel(time=TIME + " 00:00") == 0 + assert production.sel(time=time + " 00:00") == 0 cells = cutout.grid cells = cells.assign(regions=["lower"] * 200 + ["upper"] * (len(cells) - 200)) @@ -105,7 +110,7 @@ def pv_test(cutout): atlite.resource.solarpanels.CdTe, "latitude_optimal", layout=cap_factor_opt ) - assert production_opt.sel(time=TIME + " 00:00") == 0 + assert production_opt.sel(time=time + " 00:00") == 0 assert production_opt.sum() > production.sum() @@ -117,7 +122,7 @@ def pv_test(cutout): trigon_model="other", ) - assert production_other.sel(time=TIME + " 00:00") == 0 + assert production_other.sel(time=time + " 00:00") == 0 # should be roughly the same assert (production_other.sum() / production_opt.sum()).round(0) == 1 @@ -128,7 +133,7 @@ def pv_test(cutout): layout=cap_factor_opt, ) - assert production_other.sel(time=TIME + " 00:00") == 0 + assert production_other.sel(time=time + " 00:00") == 0 # should be roughly the same assert (production_other.sum() / production_opt.sum()).round(0) == 1 @@ -293,12 +298,6 @@ def coefficient_of_performance_test(cutout): # %% Prepare cutouts to test -TIME = "2013-01-01" -BOUNDS = (-4, 56, 1.5, 62) -SARAH_DIR = os.getenv("SARAH_DIR", "/home/vres/climate-data/sarah_v2") -GEBCO_PATH = os.getenv("GEBCO_PATH", "/home/vres/climate-data/GEBCO_2014_2D.nc") - - @pytest.fixture(scope="session") def cutout_era5(tmp_path_factory): tmp_path = tmp_path_factory.mktemp("era5") @@ -307,6 +306,33 @@ def cutout_era5(tmp_path_factory): return cutout +@pytest.fixture(scope="session") +def cutout_era5_3h_sampling(tmp_path_factory): + tmp_path = tmp_path_factory.mktemp("era5") + time = [ + f"{TIME} 00:00", + f"{TIME} 03:00", + f"{TIME} 06:00", + f"{TIME} 09:00", + f"{TIME} 12:00", + f"{TIME} 15:00", + f"{TIME} 18:00", + f"{TIME} 21:00", + ] + cutout = Cutout(path=tmp_path / "era5", module="era5", bounds=BOUNDS, time=time) + cutout.prepare() + return cutout + + +@pytest.fixture(scope="session") +def cutout_era5_2days_crossing_months(tmp_path_factory): + tmp_path = tmp_path_factory.mktemp("era5") + time = slice("2013-02-28", "2013-03-01") + cutout = Cutout(path=tmp_path / "era5", module="era5", bounds=BOUNDS, time=time) + cutout.prepare() + return cutout + + @pytest.fixture(scope="session") def cutout_era5_coarse(tmp_path_factory): tmp_path = tmp_path_factory.mktemp("era5_coarse") @@ -490,6 +516,16 @@ def test_wrong_loading(cutout_era5): def test_pv_era5(cutout_era5): return pv_test(cutout_era5) + @staticmethod + def test_pv_era5_2days_crossing_months(cutout_era5_2days_crossing_months): + """See https://github.com/PyPSA/atlite/issues/256""" + return pv_test(cutout_era5_2days_crossing_months, time="2013-03-01") + + @staticmethod + def test_pv_era5_3h_sampling(cutout_era5_3h_sampling): + assert pd.infer_freq(cutout_era5_3h_sampling.data.time) == "3H" + return pv_test(cutout_era5_3h_sampling) + @staticmethod def test_wind_era5(cutout_era5): return wind_test(cutout_era5)