From 129766e71f2d99d009aaad96d1c75dcf0e716ea9 Mon Sep 17 00:00:00 2001 From: Kevin Anderson Date: Wed, 5 Aug 2026 13:15:55 +0200 Subject: [PATCH 01/13] update function to use new API endpoint --- pvlib/iotools/merra2.py | 76 +++++++++++++++++------------------------ 1 file changed, 31 insertions(+), 45 deletions(-) diff --git a/pvlib/iotools/merra2.py b/pvlib/iotools/merra2.py index 8a7770b9f4..adcc8723db 100644 --- a/pvlib/iotools/merra2.py +++ b/pvlib/iotools/merra2.py @@ -134,61 +134,47 @@ def _to_utc_dt_notz(dt): start = _to_utc_dt_notz(start) end = _to_utc_dt_notz(end) - if (year := start.year) != end.year: - raise ValueError("start and end must be in the same year (in UTC)") - - url = ( - "https://goldsmr4.gesdisc.eosdis.nasa.gov/thredds/ncss/grid/" - f"MERRA2_aggregation/{dataset}/{dataset}_Aggregation_{year}.ncml" + # login + login_url = "https://urs.earthdata.nasa.gov/api/users/find_or_create_token" + response = requests.post( + login_url, + auth = (username, password), + headers={"Accept": "application/json"}, + timeout=10, ) + response.raise_for_status() + token = response.json()["access_token"] + # data query + data_url = "https://api.giovanni.earthdata.nasa.gov/timeseries" parameters = { - 'var': ",".join(variables), - 'latitude': latitude, - 'longitude': longitude, - 'time_start': start.isoformat() + "Z", - 'time_end': end.isoformat() + "Z", - 'accept': 'csv', + "location": "[{},{}]".format(round(latitude, 4), round(longitude, 4)), + "time": "{}/{}".format(start.isoformat(), end.isoformat()) } + query_headers = { + 'Authorization': f'Bearer {token}' + } + meta = {} + data = {} + for variable in variables: + name = dataset.replace(".", "_") + "_" + variable + query_parameters = parameters.copy() + query_parameters["data"] = name - auth = (username, password) - - with requests.Session() as session: - session.auth = auth - login = session.request('get', url, params=parameters) - response = session.get(login.url, auth=auth, params=parameters) + response = requests.get(data_url, params=query_parameters, headers=query_headers) + response.raise_for_status() + buffer = StringIO(response.text) - response.raise_for_status() + while (line := buffer.readline().rstrip()) != "": + key, value = line.split(",", maxsplit=1) + meta[key] = value - content = response.content.decode('utf-8') - buffer = StringIO(content) - df = pd.read_csv(buffer) + df = pd.read_csv(buffer, index_col=0, parse_dates=True) + data[variable] = df["Data"] - df.index = pd.to_datetime(df['time']) + df = pd.DataFrame(data) - meta = {} meta['dataset'] = dataset - meta['station'] = df['station'].values[0] - meta['latitude'] = df['latitude[unit="degrees_north"]'].values[0] - meta['longitude'] = df['longitude[unit="degrees_east"]'].values[0] - - # drop the non-data columns - dropcols = ['time', 'station', 'latitude[unit="degrees_north"]', - 'longitude[unit="degrees_east"]'] - df = df.drop(columns=dropcols) - - # column names are like T2M[unit="K"] by default. extract the unit - # for the metadata, then rename col to just T2M - units = {} - rename = {} - for col in df.columns: - name, _ = col.split("[", maxsplit=1) - unit = col.split('"')[1] - units[name] = unit - rename[col] = name - - meta['units'] = units - df = df.rename(columns=rename) if map_variables: df = df.rename(columns=VARIABLE_MAP) From 6894e74608d282bf776287967bb125a76b63bbe1 Mon Sep 17 00:00:00 2001 From: Kevin Anderson Date: Wed, 5 Aug 2026 13:22:24 +0200 Subject: [PATCH 02/13] tweaks --- pvlib/iotools/merra2.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/pvlib/iotools/merra2.py b/pvlib/iotools/merra2.py index adcc8723db..a6a6d8bf89 100644 --- a/pvlib/iotools/merra2.py +++ b/pvlib/iotools/merra2.py @@ -1,4 +1,5 @@ import pandas as pd +import numpy as np import requests from io import StringIO @@ -154,7 +155,7 @@ def _to_utc_dt_notz(dt): query_headers = { 'Authorization': f'Bearer {token}' } - meta = {} + meta = {'dataset': dataset} data = {} for variable in variables: name = dataset.replace(".", "_") + "_" + variable @@ -165,16 +166,19 @@ def _to_utc_dt_notz(dt): response.raise_for_status() buffer = StringIO(response.text) + var_meta = {} while (line := buffer.readline().rstrip()) != "": key, value = line.split(",", maxsplit=1) - meta[key] = value + var_meta[key] = value + meta[variable] = var_meta df = pd.read_csv(buffer, index_col=0, parse_dates=True) + df = df.replace(float(var_meta["undef"]), np.nan) + data[variable] = df["Data"] df = pd.DataFrame(data) - meta['dataset'] = dataset if map_variables: df = df.rename(columns=VARIABLE_MAP) From 99074074860392678308e2a7d7fb69f8153a0578 Mon Sep 17 00:00:00 2001 From: Kevin Anderson Date: Wed, 5 Aug 2026 13:23:01 +0200 Subject: [PATCH 03/13] tweaks --- pvlib/iotools/merra2.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/pvlib/iotools/merra2.py b/pvlib/iotools/merra2.py index a6a6d8bf89..8cbf326284 100644 --- a/pvlib/iotools/merra2.py +++ b/pvlib/iotools/merra2.py @@ -122,9 +122,6 @@ def get_merra2(latitude, longitude, start, end, username, password, dataset, .. [3] https://disc.gsfc.nasa.gov/datasets?project=MERRA-2 """ - # general API info here: - # https://docs.unidata.ucar.edu/tds/5.0/userguide/netcdf_subset_service_ref.html # noqa: E501 - def _to_utc_dt_notz(dt): dt = pd.to_datetime(dt) if dt.tzinfo is not None: From 4a22229324a3ae2484cfe3e79b60d5e482f2a871 Mon Sep 17 00:00:00 2001 From: Kevin Anderson Date: Wed, 5 Aug 2026 14:45:06 +0200 Subject: [PATCH 04/13] fix tests --- pvlib/iotools/merra2.py | 2 +- tests/iotools/test_merra2.py | 61 +++++++++++++++++++++++++++++------- 2 files changed, 50 insertions(+), 13 deletions(-) diff --git a/pvlib/iotools/merra2.py b/pvlib/iotools/merra2.py index 8cbf326284..1d8349a77b 100644 --- a/pvlib/iotools/merra2.py +++ b/pvlib/iotools/merra2.py @@ -175,7 +175,7 @@ def _to_utc_dt_notz(dt): data[variable] = df["Data"] df = pd.DataFrame(data) - + df.index = df.index.tz_localize("UTC") if map_variables: df = df.rename(columns=VARIABLE_MAP) diff --git a/tests/iotools/test_merra2.py b/tests/iotools/test_merra2.py index 55f28b04e2..4c3b381df6 100644 --- a/tests/iotools/test_merra2.py +++ b/tests/iotools/test_merra2.py @@ -7,6 +7,7 @@ import pvlib import os import requests +from requests.exceptions import HTTPError from tests.conftest import RERUNS, RERUNS_DELAY, requires_earthdata_credentials @@ -25,11 +26,11 @@ def params(): @pytest.fixture def expected(): - index = pd.date_range("2020-06-01 15:30", "2020-06-01 20:30", freq="h", + index = pd.date_range("2020-06-01 15:30", "2020-06-01 19:30", freq="h", tz="UTC") - index.name = 'time' - albedo = [0.163931, 0.1609407, 0.1601474, 0.1612476, 0.164664, 0.1711341] - ghi = [ 930., 1002.75, 1020.25, 981.25, 886.5, 743.5] + index.name = 'Timestamp (UTC)' + albedo = [0.163931, 0.1609407, 0.1601474, 0.1612476, 0.164664] + ghi = [ 930., 1002.75, 1020.25, 981.25, 886.5] df = pd.DataFrame({'albedo': albedo, 'ghi': ghi}, index=index) return df @@ -38,10 +39,38 @@ def expected(): def expected_meta(): return { 'dataset': 'M2T1NXRAD.5.12.4', - 'station': 'GridPointRequestedAt[40.010N_80.010W]', - 'latitude': 40.0, - 'longitude': -80.0, - 'units': {'ALBEDO': '1', 'SWGDN': 'W m-2'} + 'ALBEDO': { + 'prod_name': 'M2T1NXRAD.5.12.4', + 'doi': '10.5067/Q9QMY5PBNV1T', + 'param_short_name': 'ALBEDO', + 'param_name': 'Surface albedo, time average', + 'unit': '1', + 'undef': '1e+15', + 'begin_time': '2020-06-01 15:30:00', + 'end_time': '2020-06-01 19:30:00', + 'lat': '40.0', + 'lon': '-80.0', + 'lat_resolution': '0.5', + 'lon_resolution': '0.625', + 'mean': '1.6219e-01', + #'Request_time': '2026-08-05 12:35:06' + }, + 'SWGDN': { + 'prod_name': 'M2T1NXRAD.5.12.4', + 'doi': '10.5067/Q9QMY5PBNV1T', + 'param_short_name': 'SWGDN', + 'param_name': 'Surface incoming shortwave flux, time average', + 'unit': 'W m-2', + 'undef': '1e+15', + 'begin_time': '2020-06-01 15:30:00', + 'end_time': '2020-06-01 19:30:00', + 'lat': '40.0', + 'lon': '-80.0', + 'lat_resolution': '0.5', + 'lon_resolution': '0.625', + 'mean': '9.6415e+02', + #'Request_time': '2026-08-05 12:35:09' + } } @@ -51,6 +80,8 @@ def expected_meta(): def test_get_merra2(params, expected, expected_meta): df, meta = pvlib.iotools.get_merra2(**params) pd.testing.assert_frame_equal(df, expected, check_freq=False) + meta["SWGDN"].pop("Request_time") # this changes from run to run, + meta["ALBEDO"].pop("Request_time") # so don't check it assert meta == expected_meta @@ -61,11 +92,15 @@ def test_get_merra2_map_variables(params, expected, expected_meta): df, meta = pvlib.iotools.get_merra2(**params, map_variables=False) expected = expected.rename(columns={'albedo': 'ALBEDO', 'ghi': 'SWGDN'}) pd.testing.assert_frame_equal(df, expected, check_freq=False) + meta["SWGDN"].pop("Request_time") # this changes from run to run, + meta["ALBEDO"].pop("Request_time") # so don't check it assert meta == expected_meta +@pytest.mark.remote_data +@pytest.mark.flaky(reruns=RERUNS, reruns_delay=RERUNS_DELAY) def test_get_merra2_error(): - with pytest.raises(ValueError, match='must be in the same year'): + with pytest.raises(HTTPError, match='Unauthorized for url'): pvlib.iotools.get_merra2(40, -80, '2019-12-31', '2020-01-02', username='anything', password='anything', dataset='anything', variables=[]) @@ -81,6 +116,8 @@ def test_get_merra2_timezones(params, expected, expected_meta): params[key] = dt.tz_localize('UTC').tz_convert('Etc/GMT+5') df, meta = pvlib.iotools.get_merra2(**params) pd.testing.assert_frame_equal(df, expected, check_freq=False) + meta["SWGDN"].pop("Request_time") # this changes from run to run, + meta["ALBEDO"].pop("Request_time") # so don't check it assert meta == expected_meta @@ -89,7 +126,7 @@ def test_get_merra2_timezones(params, expected, expected_meta): @pytest.mark.flaky(reruns=RERUNS, reruns_delay=RERUNS_DELAY) def test_get_merra2_bad_credentials(params, expected, expected_meta): params['username'] = 'nonexistent' - with pytest.raises(requests.exceptions.HTTPError, match='Unauthorized'): + with pytest.raises(HTTPError, match='Unauthorized'): pvlib.iotools.get_merra2(**params) @@ -98,7 +135,7 @@ def test_get_merra2_bad_credentials(params, expected, expected_meta): @pytest.mark.flaky(reruns=RERUNS, reruns_delay=RERUNS_DELAY) def test_get_merra2_bad_dataset(params, expected, expected_meta): params['dataset'] = 'nonexistent' - with pytest.raises(requests.exceptions.HTTPError, match='404'): + with pytest.raises(HTTPError, match='Forbidden for url'): pvlib.iotools.get_merra2(**params) @@ -107,5 +144,5 @@ def test_get_merra2_bad_dataset(params, expected, expected_meta): @pytest.mark.flaky(reruns=RERUNS, reruns_delay=RERUNS_DELAY) def test_get_merra2_bad_variables(params, expected, expected_meta): params['variables'] = ['nonexistent'] - with pytest.raises(requests.exceptions.HTTPError, match='400'): + with pytest.raises(HTTPError, match='Forbidden for url'): pvlib.iotools.get_merra2(**params) From e5e5db9961b0449d65c5125a3b2976da8c2de330 Mon Sep 17 00:00:00 2001 From: Kevin Anderson Date: Wed, 5 Aug 2026 14:48:18 +0200 Subject: [PATCH 05/13] whatsnew --- docs/sphinx/source/whatsnew/v0.16.0.rst | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/docs/sphinx/source/whatsnew/v0.16.0.rst b/docs/sphinx/source/whatsnew/v0.16.0.rst index 6d62b9e1c6..4c9d6ff5ab 100644 --- a/docs/sphinx/source/whatsnew/v0.16.0.rst +++ b/docs/sphinx/source/whatsnew/v0.16.0.rst @@ -2,6 +2,8 @@ v0.16.0 +------- + Breaking Changes ~~~~~~~~~~~~~~~~ * Change output type of :py:func:`pvlib.irradiance.perez` and @@ -19,6 +21,9 @@ Deprecations Bug fixes ~~~~~~~~~ +* Fix :py:func:`~pvlib.iotools.get_merra2` to work with a new API endpoint + now that the previous one is retired. Format of returned data and metadata + is slightly different. (:pull:`2839`) Enhancements @@ -48,3 +53,4 @@ Maintenance Contributors ~~~~~~~~~~~~ * Carolina Crespo (:ghuser:`cbcrespo`) +* Kevin Anderson (:ghuser:`kandersolar`) From 249ce2aa3bf9e9fa5039f9bf5e408e2874f7a32c Mon Sep 17 00:00:00 2001 From: Kevin Anderson Date: Wed, 5 Aug 2026 14:51:18 +0200 Subject: [PATCH 06/13] lint --- pvlib/iotools/merra2.py | 5 +++-- tests/iotools/test_merra2.py | 7 +++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/pvlib/iotools/merra2.py b/pvlib/iotools/merra2.py index 1d8349a77b..8ef2ead01b 100644 --- a/pvlib/iotools/merra2.py +++ b/pvlib/iotools/merra2.py @@ -136,7 +136,7 @@ def _to_utc_dt_notz(dt): login_url = "https://urs.earthdata.nasa.gov/api/users/find_or_create_token" response = requests.post( login_url, - auth = (username, password), + auth=(username, password), headers={"Accept": "application/json"}, timeout=10, ) @@ -159,7 +159,8 @@ def _to_utc_dt_notz(dt): query_parameters = parameters.copy() query_parameters["data"] = name - response = requests.get(data_url, params=query_parameters, headers=query_headers) + response = requests.get(data_url, params=query_parameters, + headers=query_headers) response.raise_for_status() buffer = StringIO(response.text) diff --git a/tests/iotools/test_merra2.py b/tests/iotools/test_merra2.py index 4c3b381df6..3eca087f48 100644 --- a/tests/iotools/test_merra2.py +++ b/tests/iotools/test_merra2.py @@ -6,7 +6,6 @@ import pytest import pvlib import os -import requests from requests.exceptions import HTTPError from tests.conftest import RERUNS, RERUNS_DELAY, requires_earthdata_credentials @@ -50,10 +49,10 @@ def expected_meta(): 'end_time': '2020-06-01 19:30:00', 'lat': '40.0', 'lon': '-80.0', - 'lat_resolution': '0.5', + 'lat_resolution': '0.5', 'lon_resolution': '0.625', 'mean': '1.6219e-01', - #'Request_time': '2026-08-05 12:35:06' + # 'Request_time': '2026-08-05 12:35:06' }, 'SWGDN': { 'prod_name': 'M2T1NXRAD.5.12.4', @@ -69,7 +68,7 @@ def expected_meta(): 'lat_resolution': '0.5', 'lon_resolution': '0.625', 'mean': '9.6415e+02', - #'Request_time': '2026-08-05 12:35:09' + # 'Request_time': '2026-08-05 12:35:09' } } From d9891857dc71ede171b4ee2514a76b676605a6b0 Mon Sep 17 00:00:00 2001 From: Kevin Anderson Date: Wed, 5 Aug 2026 16:26:32 +0200 Subject: [PATCH 07/13] copy lat/lon to top level --- pvlib/iotools/merra2.py | 5 +++++ tests/iotools/test_merra2.py | 2 ++ 2 files changed, 7 insertions(+) diff --git a/pvlib/iotools/merra2.py b/pvlib/iotools/merra2.py index 8ef2ead01b..4530fa6b6c 100644 --- a/pvlib/iotools/merra2.py +++ b/pvlib/iotools/merra2.py @@ -175,6 +175,11 @@ def _to_utc_dt_notz(dt): data[variable] = df["Data"] + # copy lat/lon to the top level, for consistency + # with other iotools functions + meta["latitude"] = float(var_meta["latitude"]) + meta["longitude"] = float(var_meta["longitude"]) + df = pd.DataFrame(data) df.index = df.index.tz_localize("UTC") diff --git a/tests/iotools/test_merra2.py b/tests/iotools/test_merra2.py index 3eca087f48..db00aad35a 100644 --- a/tests/iotools/test_merra2.py +++ b/tests/iotools/test_merra2.py @@ -38,6 +38,8 @@ def expected(): def expected_meta(): return { 'dataset': 'M2T1NXRAD.5.12.4', + 'latitude': 40.0, + 'longitude': -80.0, 'ALBEDO': { 'prod_name': 'M2T1NXRAD.5.12.4', 'doi': '10.5067/Q9QMY5PBNV1T', From 6835fde731a5c3be4fc164632cab3c6c4a67177e Mon Sep 17 00:00:00 2001 From: Kevin Anderson Date: Wed, 5 Aug 2026 16:37:47 +0200 Subject: [PATCH 08/13] allow variables from multiple datasets --- docs/sphinx/source/whatsnew/v0.16.0.rst | 2 ++ pvlib/iotools/merra2.py | 14 +++++++++++--- tests/iotools/test_merra2.py | 10 ++++++++++ 3 files changed, 23 insertions(+), 3 deletions(-) diff --git a/docs/sphinx/source/whatsnew/v0.16.0.rst b/docs/sphinx/source/whatsnew/v0.16.0.rst index 4c9d6ff5ab..178be9bd62 100644 --- a/docs/sphinx/source/whatsnew/v0.16.0.rst +++ b/docs/sphinx/source/whatsnew/v0.16.0.rst @@ -28,6 +28,8 @@ Bug fixes Enhancements ~~~~~~~~~~~~ +* Allow variables from multiple datasets to be requested at once in + :py:func:`~pvlib.iotools.get_merra2`. (:pull:`2839`) Documentation diff --git a/pvlib/iotools/merra2.py b/pvlib/iotools/merra2.py index 4530fa6b6c..9094cebc18 100644 --- a/pvlib/iotools/merra2.py +++ b/pvlib/iotools/merra2.py @@ -46,8 +46,11 @@ def get_merra2(latitude, longitude, start, end, username, password, dataset, NASA EarthData username. password : str NASA EarthData password. - dataset : str - Dataset name (with version), e.g. "M2T1NXRAD.5.12.4". + dataset : str or list of str + Dataset name (with version), e.g. "M2T1NXRAD.5.12.4". If all + variables are in the same dataset, this can be a single string. + Otherwise, pass a list of dataset names corresponding to + the list of requested variables. variables : list of str List of variable names to retrieve. See the documentation of the specific dataset you are accessing for options. @@ -144,6 +147,11 @@ def _to_utc_dt_notz(dt): token = response.json()["access_token"] # data query + if isinstance(dataset, str): + datasets = [dataset] * len(variables) + else: + datasets = dataset + data_url = "https://api.giovanni.earthdata.nasa.gov/timeseries" parameters = { "location": "[{},{}]".format(round(latitude, 4), round(longitude, 4)), @@ -154,7 +162,7 @@ def _to_utc_dt_notz(dt): } meta = {'dataset': dataset} data = {} - for variable in variables: + for variable, dataset in zip(variables, datasets): name = dataset.replace(".", "_") + "_" + variable query_parameters = parameters.copy() query_parameters["data"] = name diff --git a/tests/iotools/test_merra2.py b/tests/iotools/test_merra2.py index db00aad35a..a563ad0535 100644 --- a/tests/iotools/test_merra2.py +++ b/tests/iotools/test_merra2.py @@ -147,3 +147,13 @@ def test_get_merra2_bad_variables(params, expected, expected_meta): params['variables'] = ['nonexistent'] with pytest.raises(HTTPError, match='Forbidden for url'): pvlib.iotools.get_merra2(**params) + + +@requires_earthdata_credentials +@pytest.mark.remote_data +@pytest.mark.flaky(reruns=RERUNS, reruns_delay=RERUNS_DELAY) +def test_get_merra2_multiple_datasets(params): + params['variables'] = ["SWGDN", "T2M"] + params["dataset"] = ["M2T1NXRAD.5.12.4", "M2T1NXSLV.5.12.4"] + df, meta = pvlib.iotools.get_merra2(**params) + assert set(df.columns) == {"ghi", "temp_air"} From 5fbf1759780f3276fb2eb4c9e07f978a516596d3 Mon Sep 17 00:00:00 2001 From: Kevin Anderson Date: Wed, 5 Aug 2026 16:54:40 +0200 Subject: [PATCH 09/13] fix bug --- pvlib/iotools/merra2.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pvlib/iotools/merra2.py b/pvlib/iotools/merra2.py index 9094cebc18..ca2dc52d06 100644 --- a/pvlib/iotools/merra2.py +++ b/pvlib/iotools/merra2.py @@ -185,8 +185,8 @@ def _to_utc_dt_notz(dt): # copy lat/lon to the top level, for consistency # with other iotools functions - meta["latitude"] = float(var_meta["latitude"]) - meta["longitude"] = float(var_meta["longitude"]) + meta["latitude"] = float(var_meta["lat"]) + meta["longitude"] = float(var_meta["lon"]) df = pd.DataFrame(data) df.index = df.index.tz_localize("UTC") From 883f26ad4ce7d61d222f516f31ad1895596f1b46 Mon Sep 17 00:00:00 2001 From: Kevin Anderson Date: Thu, 6 Aug 2026 15:18:13 +0200 Subject: [PATCH 10/13] rework variable tables per review --- pvlib/iotools/merra2.py | 68 ++++++++++++++++++++--------------------- 1 file changed, 33 insertions(+), 35 deletions(-) diff --git a/pvlib/iotools/merra2.py b/pvlib/iotools/merra2.py index ca2dc52d06..16e5ff0943 100644 --- a/pvlib/iotools/merra2.py +++ b/pvlib/iotools/merra2.py @@ -15,6 +15,7 @@ 'T2MDEW': 'temp_dew', 'PS': 'pressure', 'TOTEXTTAU': 'aod550', + 'TQV': 'precipitable_water', } @@ -74,45 +75,42 @@ def get_merra2(latitude, longitude, start, end, username, password, dataset, ----- The following datasets provide quantities useful for PV modeling: - +------------------------------------+-----------+---------------+ - | Dataset | Variable | pvlib name | - +====================================+===========+===============+ - | `M2T1NXRAD.5.12.4 `_ | SWGDN | ghi | - | +-----------+---------------+ - | | SWGDNCLR | ghi_clear | - | +-----------+---------------+ - | | ALBEDO | albedo | - | +-----------+---------------+ - | | LWGAB | longwave_down | - | +-----------+---------------+ - | | LWGNT | longwave_net | - | +-----------+---------------+ - | | LWGEM | longwave_up | - +------------------------------------+-----------+---------------+ - | `M2T1NXSLV.5.12.4 `_ | T2M | temp_air | - | +-----------+---------------+ - | | U10 | n/a | - | +-----------+---------------+ - | | V10 | n/a | - | +-----------+---------------+ - | | T2MDEW | temp_dew | - | +-----------+---------------+ - | | PS | pressure | - | +-----------+---------------+ - | | TO3 | n/a | - | +-----------+---------------+ - | | TQV | n/a | - +------------------------------------+-----------+---------------+ - | `M2T1NXAER.5.12.4 `_ | TOTEXTTAU | aod550 | - | +-----------+---------------+ - | | TOTSCATAU | n/a | - | +-----------+---------------+ - | | TOTANGSTR | n/a | - +------------------------------------+-----------+---------------+ + +------------------------------------+-----------+--------------------+ + | Dataset | Variable | pvlib name | + +====================================+===========+====================+ + | `M2T1NXRAD.5.12.4 `_ | SWGDN | ghi | + | +-----------+--------------------+ + | | SWGDNCLR | ghi_clear | + | +-----------+--------------------+ + | | ALBEDO | albedo | + | +-----------+--------------------+ + | | LWGNT | longwave_net | + +------------------------------------+-----------+--------------------+ + | `M2T1NXLFO.5.12.4 `_ | LWGAB | longwave_down | + +------------------------------------+-----------+--------------------+ + | `M2T1NXSLV.5.12.4 `_ | T2M | temp_air | + | +-----------+--------------------+ + | | U10M | n/a | + | +-----------+--------------------+ + | | V10M | n/a | + | +-----------+--------------------+ + | | PS | pressure | + | +-----------+--------------------+ + | | TO3 | n/a | + | +-----------+--------------------+ + | | TQV | precipitable_water | + +------------------------------------+-----------+--------------------+ + | `M2T1NXAER.5.12.4 `_ | TOTEXTTAU | aod550 | + | +-----------+--------------------+ + | | TOTSCATAU | n/a | + | +-----------+--------------------+ + | | TOTANGSTR | n/a | + +------------------------------------+-----------+--------------------+ .. _M2T1NXRAD: https://disc.gsfc.nasa.gov/datasets/M2T1NXRAD_5.12.4/summary .. _M2T1NXSLV: https://disc.gsfc.nasa.gov/datasets/M2T1NXSLV_5.12.4/summary .. _M2T1NXAER: https://disc.gsfc.nasa.gov/datasets/M2T1NXAER_5.12.4/summary + .. _M2T1NXLFO: https://disc.gsfc.nasa.gov/datasets/M2T1NXLFO_5.12.4/summary A complete list of datasets and their documentation is available at [3]_. From 50f2ed5250b40ac4c2ca42f0b169728068e97ceb Mon Sep 17 00:00:00 2001 From: Kevin Anderson Date: Thu, 6 Aug 2026 15:33:38 +0200 Subject: [PATCH 11/13] more cleanup --- pvlib/iotools/merra2.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/pvlib/iotools/merra2.py b/pvlib/iotools/merra2.py index 16e5ff0943..5a278c640f 100644 --- a/pvlib/iotools/merra2.py +++ b/pvlib/iotools/merra2.py @@ -9,10 +9,8 @@ 'SWGDNCLR': 'ghi_clear', 'ALBEDO': 'albedo', 'LWGNT': 'longwave_net', - 'LWGEM': 'longwave_up', 'LWGAB': 'longwave_down', 'T2M': 'temp_air', - 'T2MDEW': 'temp_dew', 'PS': 'pressure', 'TOTEXTTAU': 'aod550', 'TQV': 'precipitable_water', From a16a6fd243e35ba8421dde26246ecdd9d644242c Mon Sep 17 00:00:00 2001 From: Kevin Anderson Date: Thu, 6 Aug 2026 15:33:52 +0200 Subject: [PATCH 12/13] T2M: convert K to C --- pvlib/iotools/merra2.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/pvlib/iotools/merra2.py b/pvlib/iotools/merra2.py index 5a278c640f..f513699eb8 100644 --- a/pvlib/iotools/merra2.py +++ b/pvlib/iotools/merra2.py @@ -16,6 +16,13 @@ 'TQV': 'precipitable_water', } +def _k_to_c(temp_k): + return temp_k - 273.15 + +UNITS = { + 'T2M': _k_to_c, +} + def get_merra2(latitude, longitude, start, end, username, password, dataset, variables, map_variables=True): @@ -188,6 +195,11 @@ def _to_utc_dt_notz(dt): df.index = df.index.tz_localize("UTC") if map_variables: + for col in df.columns: + if col in UNITS: + convert = UNITS[col] + df[col] = convert(df[col]) + df = df.rename(columns=VARIABLE_MAP) return df, meta From a1464e1f99c039518aa80007b35a9f70fa0a5803 Mon Sep 17 00:00:00 2001 From: Kevin Anderson Date: Thu, 6 Aug 2026 15:37:01 +0200 Subject: [PATCH 13/13] lint --- pvlib/iotools/merra2.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pvlib/iotools/merra2.py b/pvlib/iotools/merra2.py index f513699eb8..975024499b 100644 --- a/pvlib/iotools/merra2.py +++ b/pvlib/iotools/merra2.py @@ -16,9 +16,11 @@ 'TQV': 'precipitable_water', } + def _k_to_c(temp_k): return temp_k - 273.15 + UNITS = { 'T2M': _k_to_c, }