Пример #1
0
def test_convert_calendar(source, target, target_as_str, freq):
    src = xr.DataArray(
        date_range("2004-01-01", "2004-12-31", freq=freq, calendar=source),
        dims=("time",),
        name="time",
    )
    da_src = xr.DataArray(
        np.linspace(0, 1, src.size), dims=("time",), coords={"time": src}
    )
    tgt = xr.DataArray(
        date_range("2004-01-01", "2004-12-31", freq=freq, calendar=target),
        dims=("time",),
        name="time",
    )

    conv = convert_calendar(da_src, target if target_as_str else tgt)

    assert get_calendar(conv) == target

    if target_as_str and max_doy[source] < max_doy[target]:
        assert conv.size == src.size
    elif not target_as_str:
        assert conv.size == tgt.size

        assert conv.isnull().sum() == max(max_doy[target] - max_doy[source], 0)
Пример #2
0
    def test_calendars(self):
        # generate test DataArray
        time_std = date_range("1991-07-01", "1993-06-30", freq="D", calendar="standard")
        time_365 = date_range("1991-07-01", "1993-06-30", freq="D", calendar="noleap")
        data_std = xr.DataArray(
            np.ones((time_std.size, 4)),
            dims=("time", "lon"),
            coords={"time": time_std, "lon": [-72, -71, -70, -69]},
        )
        # generate test start and end dates
        start_v = [[200, 200, np.nan, np.nan], [200, 200, 60, 60]]
        end_v = [[200, np.nan, 60, np.nan], [360, 60, 360, 80]]
        start_std = xr.DataArray(
            start_v,
            dims=("time", "lon"),
            coords={"time": [time_std[0], time_std[366]], "lon": data_std.lon},
            attrs={"calendar": "standard", "is_dayofyear": 1},
        )
        end_std = xr.DataArray(
            end_v,
            dims=("time", "lon"),
            coords={"time": [time_std[0], time_std[366]], "lon": data_std.lon},
            attrs={"calendar": "standard", "is_dayofyear": 1},
        )

        end_noleap = xr.DataArray(
            end_v,
            dims=("time", "lon"),
            coords={"time": [time_365[0], time_365[365]], "lon": data_std.lon},
            attrs={"calendar": "noleap", "is_dayofyear": 1},
        )

        out = generic.aggregate_between_dates(
            data_std, start_std, end_std, op="sum", freq="AS-JUL"
        )

        # expected output
        s = doy_to_days_since(start_std)
        e = doy_to_days_since(end_std)
        expected = e - s
        expected = xr.where(((s > e) | (s.isnull()) | (e.isnull())), np.nan, expected)

        np.testing.assert_allclose(out, expected)

        # check calendar convertion
        out_noleap = generic.aggregate_between_dates(
            data_std, start_std, end_noleap, op="sum", freq="AS-JUL"
        )

        np.testing.assert_allclose(out, out_noleap)
Пример #3
0
    def test_multiple_lats(self):
        time_data = date_range(
            "1992-12-01", "1994-01-01", freq="D", calendar="standard"
        )
        data = xr.DataArray(
            np.ones((time_data.size, 7)),
            dims=("time", "lat"),
            coords={"time": time_data, "lat": [-60, -45, -30, 0, 30, 45, 60]},
        )

        dl = generic.day_lengths(dates=data.time, lat=data.lat)

        events = dict(
            solstice=[
                ["1992-12-21", [[18.49, 15.43, 13.93, 12.0, 10.07, 8.57, 5.51]]],
                ["1993-06-21", [[5.51, 8.57, 10.07, 12.0, 13.93, 15.43, 18.49]]],
                ["1993-12-21", [[18.49, 15.43, 13.93, 12.0, 10.07, 8.57, 5.51]]],
            ],
            equinox=[
                ["1993-03-20", [[12] * 7]]
            ],  # True equinox on 1993-03-20 at 14:41 GMT. Some relative tolerance is needed.
        )

        for event, evaluations in events.items():
            for e in evaluations:
                if event == "solstice":
                    np.testing.assert_array_almost_equal(
                        dl.sel(time=e[0]).transpose(), np.array(e[1]), 2
                    )
                elif event == "equinox":
                    np.testing.assert_allclose(
                        dl.sel(time=e[0]).transpose(), np.array(e[1]), rtol=2e-1
                    )
Пример #4
0
    def test_day_of_year_strings(self):
        # generate test DataArray
        time_data = date_range(
            "1990-08-01", "1995-06-01", freq="D", calendar="standard"
        )
        data = xr.DataArray(
            np.ones(time_data.size),
            dims="time",
            coords={"time": time_data},
        )
        # set start and end dates
        start = "02-01"
        end = "10-31"

        out = generic.aggregate_between_dates(data, start, end, op="sum", freq="YS")

        np.testing.assert_allclose(out, np.array([np.nan, 272, 273, 272, 272, np.nan]))

        # given no freq and only strings for start and end dates
        with pytest.raises(ValueError):
            generic.aggregate_between_dates(data, start, end, op="sum")

        # given a malformed date string
        bad_start = "02-31"
        with pytest.raises(ValueError):
            generic.aggregate_between_dates(data, bad_start, end, op="sum", freq="YS")
Пример #5
0
def test_convert_calendar_360_days(source, target, freq, align_on):
    src = xr.DataArray(
        date_range("2004-01-01", "2004-12-30", freq=freq, calendar=source),
        dims=("time",),
        name="time",
    )
    da_src = xr.DataArray(
        np.linspace(0, 1, src.size), dims=("time",), coords={"time": src}
    )

    conv = convert_calendar(da_src, target, align_on=align_on)

    assert get_calendar(conv) == target

    if align_on == "date":
        np.testing.assert_array_equal(
            conv.time.resample(time="M").last().dt.day,
            [30, 29, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30],
        )
    elif target == "360_day":
        np.testing.assert_array_equal(
            conv.time.resample(time="M").last().dt.day,
            [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 29],
        )
    else:
        np.testing.assert_array_equal(
            conv.time.resample(time="M").last().dt.day,
            [30, 29, 30, 30, 31, 30, 30, 31, 30, 31, 29, 31],
        )
    if source == "360_day" and align_on == "year":
        assert conv.size == 360 if freq == "D" else 360 * 4
    else:
        assert conv.size == 359 if freq == "D" else 359 * 4
Пример #6
0
    def test_time_length(self):
        # generate test DataArray
        time_data = date_range(
            "1991-01-01", "1993-12-31", freq="D", calendar="standard"
        )
        time_start = date_range(
            "1990-01-01", "1992-12-31", freq="D", calendar="standard"
        )
        time_end = date_range("1991-01-01", "1993-12-31", freq="D", calendar="standard")
        data = xr.DataArray(
            np.ones((time_data.size, 4)),
            dims=("time", "lon"),
            coords={"time": time_data, "lon": [-72, -71, -70, -69]},
        )
        # generate test start and end dates
        start_v = [[200, 200, np.nan, np.nan], [200, 200, 60, 60], [150, 100, 40, 10]]
        end_v = [[200, np.nan, 60, np.nan], [360, 60, 360, 80], [200, 200, 60, 50]]
        start = xr.DataArray(
            start_v,
            dims=("time", "lon"),
            coords={
                "time": [time_start[0], time_start[365], time_start[730]],
                "lon": data.lon,
            },
            attrs={"calendar": "standard", "is_dayofyear": 1},
        )
        end = xr.DataArray(
            end_v,
            dims=("time", "lon"),
            coords={
                "time": [time_end[0], time_end[365], time_end[731]],
                "lon": data.lon,
            },
            attrs={"calendar": "standard", "is_dayofyear": 1},
        )

        out = generic.aggregate_between_dates(data, start, end, op="sum", freq="YS")

        # expected output
        s = doy_to_days_since(start)
        e = doy_to_days_since(end)
        expected = e - s
        expected[1, 1] = np.nan

        np.testing.assert_allclose(out[0:2], expected)
        np.testing.assert_allclose(out[2], np.array([np.nan, np.nan, np.nan, np.nan]))
Пример #7
0
def test_datetime_to_decimal_year(source_cal, exp180):
    times = xr.DataArray(
        date_range(
            "2004-01-01", "2004-12-30", freq="D", calendar=source_cal or "default"
        ),
        dims=("time",),
        name="time",
    )
    decy = datetime_to_decimal_year(times, calendar=source_cal)
    np.testing.assert_almost_equal(decy[180] - 2004, exp180)
Пример #8
0
def test_interp_calendar(source, target):
    src = xr.DataArray(
        date_range("2004-01-01", "2004-07-30", freq="D", calendar=source),
        dims=("time",),
        name="time",
    )
    tgt = xr.DataArray(
        date_range("2004-01-01", "2004-07-30", freq="D", calendar=target),
        dims=("time",),
        name="time",
    )
    da_src = xr.DataArray(
        np.linspace(0, 1, src.size), dims=("time",), coords={"time": src}
    )
    conv = interp_calendar(da_src, tgt)

    assert conv.size == tgt.size
    assert get_calendar(conv) == target

    np.testing.assert_almost_equal(conv.max(), 1, 2)
    assert conv.min() == 0
Пример #9
0
def test_convert_calendar_360_days_random():
    da_std = xr.DataArray(
        np.linspace(0, 1, 366 * 2),
        dims=("time",),
        coords={
            "time": date_range(
                "2004-01-01", "2004-12-31T23:59:59", freq="12H", calendar="default"
            )
        },
    )
    da_360 = xr.DataArray(
        np.linspace(0, 1, 360 * 2),
        dims=("time",),
        coords={
            "time": date_range(
                "2004-01-01", "2004-12-30T23:59:59", freq="12H", calendar="360_day"
            )
        },
    )

    conv = convert_calendar(da_std, "360_day", align_on="random")
    assert get_calendar(conv) == "360_day"
    assert conv.size == 720
    conv2 = convert_calendar(da_std, "360_day", align_on="random")
    assert (conv != conv2).any()

    conv = convert_calendar(da_360, "default", align_on="random")
    assert get_calendar(conv) == "default"
    assert conv.size == 720
    assert np.datetime64("2004-02-29") not in conv.time
    conv2 = convert_calendar(da_360, "default", align_on="random")
    assert (conv2 != conv).any()

    conv = convert_calendar(da_360, "noleap", align_on="random", missing=np.NaN)
    conv = conv.where(conv.isnull(), drop=True)
    nandoys = conv.time.dt.dayofyear[::2]
    assert all(nandoys < np.array([74, 147, 220, 293, 366]))
    assert all(nandoys > np.array([0, 73, 146, 219, 292]))
Пример #10
0
def test_convert_calendar_missing(source, target, freq):
    src = xr.DataArray(
        date_range(
            "2004-01-01",
            "2004-12-31" if source != "360_day" else "2004-12-30",
            freq=freq,
            calendar=source,
        ),
        dims=("time",),
        name="time",
    )
    da_src = xr.DataArray(
        np.linspace(0, 1, src.size), dims=("time",), coords={"time": src}
    )
    out = convert_calendar(da_src, target, missing=np.nan, align_on="date")
    assert xr.infer_freq(out.time) == freq
    if source == "360_day":
        assert out.time[-1].dt.day == 31
Пример #11
0
    def prepare(self, da, freq, src_timestep, **indexer):
        """Prepare arrays to be fed to the `is_missing` function.

        Parameters
        ----------
        da : xr.DataArray
          Input data.
        freq : str
          Resampling frequency defining the periods defined in
          http://pandas.pydata.org/pandas-docs/stable/timeseries.html#resampling.
        src_timestep : {"D", "H"}
          Expected input frequency.
        **indexer : {dim: indexer, }, optional
          Time attribute and values over which to subset the array. For example, use season='DJF' to select winter
          values, month=1 to select January, or month=[6,7,8] to select summer months. If not indexer is given,
          all values are considered.

        Returns
        -------
        xr.DataArray, xr.DataArray
          Boolean array indicating which values are null, array of expected number of valid values.

        Notes
        -----
        If `freq=None` and an indexer is given, then missing values during period at the start or end of array won't be
        flagged.
        """
        # This function can probably be made simpler once CFPeriodIndex is implemented.
        null = self.is_null(da, freq, **indexer)

        pfreq, anchor = self.split_freq(freq)

        c = null.sum(dim="time")

        # Otherwise simply use the start and end dates to find the expected number of days.
        if pfreq.endswith("S"):
            start_time = c.indexes["time"]
            end_time = start_time.shift(1, freq=freq)
        elif pfreq:
            end_time = c.indexes["time"]
            start_time = end_time.shift(-1, freq=freq)
        else:
            i = da.time.to_index()
            start_time = i[:1]
            end_time = i[-1:]

        if indexer:
            # Create a full synthetic time series and compare the number of days with the original series.
            t = date_range(
                start_time[0],
                end_time[-1],
                freq=src_timestep,
                calendar=get_calendar(da),
            )

            sda = xr.DataArray(data=np.ones(len(t)),
                               coords={"time": t},
                               dims=("time", ))
            st = generic.select_time(sda, **indexer)
            if freq:
                count = st.notnull().resample(time=freq).sum(dim="time")
            else:
                count = st.notnull().sum(dim="time")

        else:
            delta = end_time - start_time
            n = delta.astype(_np_timedelta64[src_timestep])

            if freq:
                count = xr.DataArray(n.values,
                                     coords={"time": c.time},
                                     dims="time")
            else:
                count = xr.DataArray(n.values[0] + 1)

        return null, count
Пример #12
0
def series(start, end, calendar):
    time = date_range(start, end, calendar=calendar)
    return xr.DataArray([1] * time.size, dims=("time",), coords={"time": time})
Пример #13
0
    def test_frequency(self):
        # generate test DataArray
        time_data = date_range(
            "1991-01-01", "1992-05-31", freq="D", calendar="standard"
        )
        data = xr.DataArray(
            np.ones((time_data.size, 2)),
            dims=("time", "lon"),
            coords={"time": time_data, "lon": [-70, -69]},
        )
        # generate test start and end dates
        start_v = [[70, 100], [200, 200], [270, 300], [35, 35], [80, 80]]
        end_v = [[130, 70], [200, np.nan], [330, 270], [35, np.nan], [150, 150]]
        end_m_v = [[20, 20], [40, 40], [80, 80], [100, 100], [130, 130]]
        start = xr.DataArray(
            start_v,
            dims=("time", "lon"),
            coords={
                "time": [
                    time_data[59],
                    time_data[151],
                    time_data[243],
                    time_data[334],
                    time_data[425],
                ],
                "lon": data.lon,
            },
            attrs={"calendar": "standard", "is_dayofyear": 1},
        )
        end = xr.DataArray(
            end_v,
            dims=("time", "lon"),
            coords={
                "time": [
                    time_data[59],
                    time_data[151],
                    time_data[243],
                    time_data[334],
                    time_data[425],
                ],
                "lon": data.lon,
            },
            attrs={"calendar": "standard", "is_dayofyear": 1},
        )
        end_m = xr.DataArray(
            end_m_v,
            dims=("time", "lon"),
            coords={
                "time": [
                    time_data[0],
                    time_data[31],
                    time_data[59],
                    time_data[90],
                    time_data[120],
                ],
                "lon": data.lon,
            },
            attrs={"calendar": "standard", "is_dayofyear": 1},
        )

        out = generic.aggregate_between_dates(data, start, end, op="sum", freq="QS-DEC")

        # expected output
        s = doy_to_days_since(start)
        e = doy_to_days_since(end)
        expected = e - s
        expected = xr.where(expected < 0, np.nan, expected)

        np.testing.assert_allclose(out[0], np.array([np.nan, np.nan]))
        np.testing.assert_allclose(out[1:6], expected)

        with pytest.raises(ValueError):
            generic.aggregate_between_dates(data, start, end_m)
Пример #14
0
def test_doy_to_days_since():
    # simple test
    time = date_range("2020-07-01", "2022-07-01", freq="AS-JUL")
    da = xr.DataArray(
        [190, 360, 3],
        dims=("time",),
        coords={"time": time},
        attrs={"is_dayofyear": 1, "calendar": "default"},
    )

    out = doy_to_days_since(da)
    np.testing.assert_array_equal(out, [7, 178, 186])

    assert out.attrs["units"] == "days after 07-01"
    assert "is_dayofyear" not in out.attrs

    da2 = days_since_to_doy(out)
    xr.testing.assert_identical(da, da2)

    out = doy_to_days_since(da, start="07-01")
    np.testing.assert_array_equal(out, [7, 178, 186])

    # other calendar
    out = doy_to_days_since(da, calendar="noleap")
    assert out.attrs["calendar"] == "noleap"
    np.testing.assert_array_equal(out, [8, 178, 186])

    da2 = days_since_to_doy(out)  # calendar read from attribute
    da2.attrs.pop("calendar")  # drop for identicality
    da.attrs.pop("calendar")  # drop for identicality
    xr.testing.assert_identical(da, da2)

    # with start
    time = date_range("2020-12-31", "2022-12-31", freq="Y")
    da = xr.DataArray(
        [190, 360, 3],
        dims=("time",),
        coords={"time": time},
        name="da",
        attrs={"is_dayofyear": 1, "calendar": "default"},
    )

    out = doy_to_days_since(da, start="01-02")
    np.testing.assert_array_equal(out, [188, 358, 1])

    da2 = days_since_to_doy(out)  # start read from attribute
    assert da2.name == da.name
    xr.testing.assert_identical(da, da2)

    # finer freq
    time = date_range("2020-01-01", "2020-03-01", freq="MS")
    da = xr.DataArray(
        [15, 33, 66],
        dims=("time",),
        coords={"time": time},
        name="da",
        attrs={"is_dayofyear": 1, "calendar": "default"},
    )

    out = doy_to_days_since(da)
    assert out.attrs["units"] == "days after time coordinate"
    np.testing.assert_array_equal(out, [14, 1, 5])

    da2 = days_since_to_doy(out)  # start read from attribute
    xr.testing.assert_identical(da, da2)
Пример #15
0
    )
    conv = interp_calendar(da_src, tgt)

    assert conv.size == tgt.size
    assert get_calendar(conv) == target

    np.testing.assert_almost_equal(conv.max(), 1, 2)
    assert conv.min() == 0


@pytest.mark.parametrize(
    "inp,calout",
    [
        (
            xr.DataArray(
                date_range("2004-01-01", "2004-01-10", freq="D"),
                dims=("time",),
                name="time",
            ),
            "standard",
        ),
        (date_range("2004-01-01", "2004-01-10", freq="D"), "standard"),
        (
            xr.DataArray(date_range("2004-01-01", "2004-01-10", freq="D")).values,
            "standard",
        ),
        (date_range("2004-01-01", "2004-01-10", freq="D").values, "standard"),
        (date_range("2004-01-01", "2004-01-10", freq="D", calendar="julian"), "julian"),
    ],
)
def test_ensure_cftime_array(inp, calout):
Пример #16
0
def potential_evapotranspiration(
    tasmin: Optional[xr.DataArray] = None,
    tasmax: Optional[xr.DataArray] = None,
    tas: Optional[xr.DataArray] = None,
    method: str = "BR65",
    peta: Optional[float] = 0.00516409319477,
    petb: Optional[float] = 0.0874972822289,
) -> xr.DataArray:
    """Potential evapotranspiration.

    The potential for water evaporation from soil and transpiration by plants if the water supply is
    sufficient, according to a given method.

    Parameters
    ----------
    tasmin : xarray.DataArray
      Minimum daily temperature.
    tasmax : xarray.DataArray
      Maximum daily temperature.
    tas : xarray.DataArray
      Mean daily temperature.
    method : {"baierrobertson65", "BR65", "hargreaves85", "HG85", "thornthwaite48", "TW48", "mcguinnessbordne05", "MB05"}
      Which method to use, see notes.
    peta : float
      Used only with method MB05 as :math:`a` for calculation of PET, see Notes section. Default value resulted from calibration of PET over the UK.
    petb : float
      Used only with method MB05 as :math:`b` for calculation of PET, see Notes section. Default value resulted from calibration of PET over the UK.

    Returns
    -------
    xarray.DataArray

    Notes
    -----
    Available methods are:

    - "baierrobertson65" or "BR65", based on [baierrobertson65]_. Requires tasmin and tasmax, daily [D] freq.
    - "hargreaves85" or "HG85", based on [hargreaves85]_. Requires tasmin and tasmax, daily [D] freq. (optional: tas can be given in addition of tasmin and tasmax).
    - "mcguinnessbordne05" or "MB05", based on [tanguy2018]_. Requires tas, daily [D] freq, with latitudes 'lat'.
    - "thornthwaite48" or "TW48", based on [thornthwaite48]_. Requires tasmin and tasmax, monthly [MS] or daily [D] freq. (optional: tas can be given instead of tasmin and tasmax).

    The McGuinness-Bordne [McGuinness1972]_ equation is:

    .. math::
        PET[mm day^{-1}] = a * \frac{S_0}{\\lambda}T_a + b *\frsc{S_0}{\\lambda}

    where :math:`a` and :math:`b` are empirical parameters; :math:`S_0` is the extraterrestrial radiation [MJ m-2 day-1]; :math:`\\lambda` is the latent heat of vaporisation [MJ kg-1] and :math:`T_a` is the air temperature [°C]. The equation was originally derived for the USA, with :math:`a=0.0147` and :math:`b=0.07353`. The default parameters used here are calibrated for the UK, using the method described in [Tanguy2018]_.

    References
    ----------
    .. [baierrobertson65] Baier, W., & Robertson, G. W. (1965). Estimation of latent evaporation from simple weather observations. Canadian journal of plant science, 45(3), 276-284.
    .. [hargreaves85] Hargreaves, G. H., & Samani, Z. A. (1985). Reference crop evapotranspiration from temperature. Applied engineering in agriculture, 1(2), 96-99.
    .. [tanguy2018] Tanguy, M., Prudhomme, C., Smith, K., & Hannaford, J. (2018). Historical gridded reconstruction of potential evapotranspiration for the UK. Earth System Science Data, 10(2), 951-968.
    .. [McGuinness1972] McGuinness, J. L., & Bordne, E. F. (1972). A comparison of lysimeter-derived potential evapotranspiration with computed values (No. 1452). US Department of Agriculture.
    .. [thornthwaite48] Thornthwaite, C. W. (1948). An approach toward a rational classification of climate. Geographical review, 38(1), 55-94.
    """

    if method in ["baierrobertson65", "BR65"]:
        tasmin = convert_units_to(tasmin, "degF")
        tasmax = convert_units_to(tasmax, "degF")

        latr = (tasmin.lat * np.pi) / 180
        gsc = 0.082  # MJ/m2/min

        # julian day fraction
        jd_frac = (datetime_to_decimal_year(tasmin.time) % 1) * 2 * np.pi

        ds = 0.409 * np.sin(jd_frac - 1.39)
        dr = 1 + 0.033 * np.cos(jd_frac)
        omega = np.arccos(-np.tan(latr) * np.tan(ds))
        re = ((24 * 60 / np.pi) * gsc * dr *
              (omega * np.sin(latr) * np.sin(ds) +
               np.cos(latr) * np.cos(ds) * np.sin(omega)))  # MJ/m2/day
        re = re / 4.1864e-2  # cal/cm2/day

        # Baier et Robertson(1965) formula
        out = 0.094 * (-87.03 + 0.928 * tasmax + 0.933 *
                       (tasmax - tasmin) + 0.0486 * re)
        out = out.clip(0)

    elif method in ["hargreaves85", "HG85"]:
        tasmin = convert_units_to(tasmin, "degC")
        tasmax = convert_units_to(tasmax, "degC")
        if tas is None:
            tas = (tasmin + tasmax) / 2
        else:
            tas = convert_units_to(tas, "degC")

        latr = (tasmin.lat * np.pi) / 180
        gsc = 0.082  # MJ/m2/min
        lv = 2.5  # MJ/kg

        # julian day fraction
        jd_frac = (datetime_to_decimal_year(tasmin.time) % 1) * 2 * np.pi

        ds = 0.409 * np.sin(jd_frac - 1.39)
        dr = 1 + 0.033 * np.cos(jd_frac)
        omega = np.arccos(-np.tan(latr) * np.tan(ds))
        ra = ((24 * 60 / np.pi) * gsc * dr *
              (omega * np.sin(latr) * np.sin(ds) +
               np.cos(latr) * np.cos(ds) * np.sin(omega)))  # MJ/m2/day

        # Hargreaves and Samani(1985) formula
        out = (0.0023 * ra * (tas + 17.8) * (tasmax - tasmin)**0.5) / lv
        out = out.clip(0)

    elif method in ["mcguinnessbordne05", "MB05"]:
        if tas is None:
            tasmin = convert_units_to(tasmin, "degC")
            tasmax = convert_units_to(tasmax, "degC")
            tas = (tasmin + tasmax) / 2
            tas.attrs["units"] = "degC"

        tas = convert_units_to(tas, "degC")
        tasK = convert_units_to(tas, "K")

        latr = (tas.lat * np.pi) / 180
        jd_frac = (datetime_to_decimal_year(tas.time) % 1) * 2 * np.pi

        S = 1367.0  # Set solar constant [W/m2]
        ds = 0.409 * np.sin(jd_frac - 1.39)  # solar declination ds [radians]
        omega = np.arccos(-np.tan(latr) *
                          np.tan(ds))  # sunset hour angle [radians]
        dr = 1.0 + 0.03344 * np.cos(
            jd_frac - 0.048869)  # Calculate relative distance to sun

        ext_rad = (S * 86400 / np.pi * dr *
                   (omega * np.sin(ds) * np.sin(latr) +
                    np.sin(omega) * np.cos(ds) * np.cos(latr)))
        latentH = 4185.5 * (751.78 - 0.5655 * tasK)

        radDIVlat = ext_rad / latentH

        # parameters from calibration provided by Dr Maliko Tanguy @ CEH
        # (calibrated for PET over the UK)
        a = peta
        b = petb

        out = radDIVlat * a * tas + radDIVlat * b

    elif method in ["thornthwaite48", "TW48"]:
        if tas is None:
            tasmin = convert_units_to(tasmin, "degC")
            tasmax = convert_units_to(tasmax, "degC")
            tas = (tasmin + tasmax) / 2
        else:
            tas = convert_units_to(tas, "degC")
        tas = tas.clip(0)
        tas = tas.resample(time="MS").mean(dim="time")

        latr = (tas.lat * np.pi) / 180  # rad

        start = "-".join([
            str(tas.time[0].dt.year.values),
            f"{tas.time[0].dt.month.values:02d}",
            "01",
        ])

        end = "-".join([
            str(tas.time[-1].dt.year.values),
            f"{tas.time[-1].dt.month.values:02d}",
            str(tas.time[-1].dt.daysinmonth.values),
        ])

        time_v = xr.DataArray(
            date_range(start, end, freq="D", calendar="standard"),
            dims="time",
            name="time",
        )

        # julian day fraction
        jd_frac = (datetime_to_decimal_year(time_v) % 1) * 2 * np.pi

        ds = 0.409 * np.sin(jd_frac - 1.39)
        omega = np.arccos(-np.tan(latr) * np.tan(ds)) * 180 / np.pi  # degrees

        # monthly-mean daytime length (multiples of 12 hours)
        dl = 2 * omega / (15 * 12)
        dl_m = dl.resample(time="MS").mean(dim="time")

        # annual heat index
        id_m = (tas / 5)**1.514
        id_y = id_m.resample(time="YS").sum(dim="time")

        tas_idy_a = []
        for base_time, indexes in tas.resample(time="YS").groups.items():
            tas_y = tas.isel(time=indexes)
            id_v = id_y.sel(time=base_time)
            a = 6.75e-7 * id_v**3 - 7.71e-5 * id_v**2 + 0.01791 * id_v + 0.49239

            frac = (10 * tas_y / id_v)**a
            tas_idy_a.append(frac)

        tas_idy_a = xr.concat(tas_idy_a, dim="time")

        # Thornthwaite(1948) formula
        out = 1.6 * dl_m * tas_idy_a  # cm/month
        out = 10 * out  # mm/month

    else:
        raise NotImplementedError(f"'{method}' method is not implemented.")

    out.attrs["units"] = "mm"
    return amount2rate(out, out_units="kg m-2 s-1")