diff --git a/nlmod/dims/time.py b/nlmod/dims/time.py index 9f481d63..4214f2e9 100644 --- a/nlmod/dims/time.py +++ b/nlmod/dims/time.py @@ -1,9 +1,11 @@ +import cftime import datetime as dt import logging import warnings import numpy as np import pandas as pd +from pandas._libs.tslibs.np_datetime import OutOfBoundsDatetime, OutOfBoundsTimedelta import xarray as xr from xarray import IndexVariable @@ -152,14 +154,40 @@ def set_ds_time_deprecated( return ds +def _pd_timestamp_to_cftime(time_pd): + """convert a pandas timestamp into a cftime stamp + + Parameters + ---------- + time_pd : pd.Timestamp or list of pd.Timestamp + datetimes + + Returns + ------- + cftime.datetime or list of cftime.datetime + """ + + if hasattr(time_pd, "__iter__"): + return [_pd_timestamp_to_cftime(tpd) for tpd in time_pd] + else: + return cftime.datetime( + time_pd.year, + time_pd.month, + time_pd.day, + time_pd.hour, + time_pd.minute, + time_pd.second, + ) + + def set_ds_time( ds, start, time=None, + perlen=None, steady=False, steady_start=True, time_units="DAYS", - perlen=None, nstp=1, tsmult=1.0, ): @@ -169,15 +197,19 @@ def set_ds_time( ---------- ds : xarray.Dataset model dataset - start : int, float, str or pandas.Timestamp + start : int, float, str, pandas.Timestamp or cftime.datetime model start. When start is an integer or float it is interpreted as the number - of days of the first stress-period. When start is a string or pandas Timestamp - it is the start datetime of the simulation. + of days of the first stress-period. When start is a string, pandas Timestamp or + cftime datetime it is the start datetime of the simulation. Use cftime datetime + when you get an OutOfBounds error using pandas. time : float, int or array-like, optional float(s) (indicating elapsed time) or timestamp(s) corresponding to the end of each stress period in the model. When time is a single value, the model will have only one stress period. When time is None, the stress period lengths have to be supplied via perlen. The default is None. + perlen : float, int or array-like, optional + length of each stress-period. Only used when time is None. When perlen is a + single value, the model will have only one stress period. The default is None. steady : arraylike or bool, optional arraylike indicating which stress periods are steady-state, by default False, which sets all stress periods to transient with the first period determined by @@ -187,9 +219,6 @@ def set_ds_time( when steady is passed as single boolean. time_units : str, optional time units, by default "DAYS" - perlen : float, int or array-like, optional - length of each stress-period. Only used when time is None. When perlen is a - single value, the model will have only one stress period. The default is None. nstp : int or array-like, optional number of steps per stress period, stored in ds.attrs, default is 1 tsmult : float, optional @@ -214,27 +243,48 @@ def set_ds_time( if isinstance(time, str) or not hasattr(time, "__iter__"): time = [time] - # parse start - if isinstance(start, (int, np.integer, float)): + try: + # parse start + if isinstance(start, (int, np.integer, float)): + if isinstance(time[0], (int, np.integer, float, str)): + raise TypeError("Make sure 'start' or 'time' argument is a valid TimeStamp") + start = time[0] - pd.to_timedelta(start, "D") + elif isinstance(start, str): + start = pd.Timestamp(start) + elif isinstance(start, (pd.Timestamp, cftime.datetime)): + pass + elif isinstance(start, np.datetime64): + start = pd.Timestamp(start) + else: + raise TypeError("Cannot parse start datetime.") + + # parse time make sure 'time' and 'start' are same type (pd.Timestamps or cftime.datetime) if isinstance(time[0], (int, np.integer, float)): - raise (ValueError("Make sure start or time contains a valid TimeStamp")) - start = time[0] - pd.to_timedelta(start, "D") - elif isinstance(start, str): - start = pd.Timestamp(start) - elif isinstance(start, (pd.Timestamp, np.datetime64)): - pass - else: - raise TypeError("Cannot parse start datetime.") - - # convert time to Timestamps - if isinstance(time[0], (int, np.integer, float)): - time = pd.Timestamp(start) + pd.to_timedelta(time, time_units) - elif isinstance(time[0], str): - time = pd.to_datetime(time) - elif isinstance(time[0], (pd.Timestamp, np.datetime64, xr.core.variable.Variable)): - pass - else: - raise TypeError("Cannot process 'time' argument. Datatype not understood.") + if isinstance(start, cftime.datetime): + time = [start + dt.timedelta(days=int(td)) for td in time] + else: + time = start + pd.to_timedelta(time, time_units) + elif isinstance(time[0], str): + time = pd.to_datetime(time) + if isinstance(start, cftime.datetime): + time = _pd_timestamp_to_cftime(time) + elif isinstance(time[0], (pd.Timestamp)): + if isinstance(start, cftime.datetime): + time = _pd_timestamp_to_cftime(time) + elif isinstance(time[0], (np.datetime64, xr.core.variable.Variable)): + logger.info( + "time arguments with types np.datetime64, xr.core.variable.Variable not tested!" + ) + elif isinstance(time[0], cftime.datetime): + start = _pd_timestamp_to_cftime(start) + else: + msg = ( + f"Cannot process 'time' argument. Datatype -> {type(time)} not understood." + ) + raise TypeError(msg) + except (OutOfBoundsDatetime, OutOfBoundsTimedelta) as e: + msg = "cannot convert 'start' and 'time' to pandas datetime, use cftime types for 'start' and 'time'" + raise type(e)(msg) if time[0] <= start: msg = ( @@ -244,9 +294,138 @@ def set_ds_time( logger.error(msg) raise ValueError(msg) + # create time coordinates ds = ds.assign_coords(coords={"time": time}) ds.coords["time"].attrs = dim_attrs["time"] + # add steady, nstp and tsmult to dataset + ds = set_time_variables( + ds, start, time, steady, steady_start, time_units, nstp, tsmult + ) + + return ds + + +def set_ds_time_numeric( + ds, + start, + time=None, + perlen=None, + steady=False, + steady_start=True, + time_units="DAYS", + nstp=1, + tsmult=1.0, +): + """Set a numerical time discretisation for a model dataset. + + Parameters + ---------- + ds : xarray.Dataset + model dataset + start : int, float, str, pandas.Timestamp or cftime.datetime + model start. When start is an integer or float it is interpreted as the number + of days of the first stress-period. When start is a string, pandas Timestamp or + cftime datetime it is the start datetime of the simulation. Use cftime datetime + when you get an OutOfBounds error using pandas. + time : float, int or array-like, optional + float(s) (indicating elapsed time) corresponding to the end of each stress + period in the model. When time is a single value, the model will have only one + stress period. When time is None, the stress period lengths have to be supplied + via perlen. The default is None. + perlen : float, int or array-like, optional + length of each stress-period. Only used when time is None. When perlen is a + single value, the model will have only one stress period. The default is None. + steady : arraylike or bool, optional + arraylike indicating which stress periods are steady-state, by default False, + which sets all stress periods to transient with the first period determined by + value of `steady_start`. + steady_start : bool, optional + whether to set the first period to steady-state, default is True, only used + when steady is passed as single boolean. + time_units : str, optional + time units, by default "DAYS" + nstp : int or array-like, optional + number of steps per stress period, stored in ds.attrs, default is 1 + tsmult : float, optional + timestep multiplier within stress periods, stored in ds.attrs, default is 1.0 + + Returns + ------- + ds : xarray.Dataset + model dataset with added time coordinate + """ + + if time is None and perlen is None: + raise (ValueError("Please specify either time or perlen in set_ds_time")) + elif perlen is not None: + if time is not None: + msg = f"Cannot use both time and perlen. Ignoring perlen: {perlen}" + logger.warning(msg) + else: + if isinstance(perlen, (int, np.integer, float)): + perlen = [perlen] + time = np.cumsum(perlen) + + if isinstance(time, str) or not hasattr(time, "__iter__"): + time = [time] + + # check time + if isinstance(time[0], (str, pd.Timestamp, cftime.datetime)): + raise TypeError("'time' argument should be of a numerical type") + + time = np.asarray(time, dtype=float) + if (time <= 0.0).any(): + msg = "timesteps smaller or equal to 0 are not allowed" + logger.error(msg) + raise ValueError(msg) + + # create time coordinates + ds = ds.assign_coords(coords={"time": time}) + ds.coords["time"].attrs = dim_attrs["time"] + + # add steady, nstp and tsmult to dataset + ds = set_time_variables( + ds, start, time, steady, steady_start, time_units, nstp, tsmult + ) + + return ds + + +def set_time_variables(ds, start, time, steady, steady_start, time_units, nstp, tsmult): + """add data variables: steady, nstp and tsmult, set attributes: start, time_units + + Parameters + ---------- + ds : xarray.Dataset + model dataset + start : int, float, str, pandas.Timestamp or cftime.datetime + model start. When start is an integer or float it is interpreted as the number + of days of the first stress-period. When start is a string, pandas Timestamp or + cftime datetime it is the start datetime of the simulation. Use cftime datetime + when you get an OutOfBounds error using pandas. + time : array-like, optional + numerical (indicating elapsed time) or timestamps corresponding to the end + of each stress period in the model. + steady : arraylike or bool, optional + arraylike indicating which stress periods are steady-state, by default False, + which sets all stress periods to transient with the first period determined by + value of `steady_start`. + steady_start : bool, optional + whether to set the first period to steady-state, default is True, only used + when steady is passed as single boolean. + time_units : str, optional + time units, by default "DAYS" + nstp : int or array-like, optional + number of steps per stress period, stored in ds.attrs, default is 1 + tsmult : float, optional + timestep multiplier within stress periods, stored in ds.attrs, default is 1.0 + + Returns + ------- + ds : xarray.Dataset + model dataset with attributes added to the time coordinates + """ # add steady, nstp and tsmult to dataset if isinstance(steady, bool): steady = int(steady) * np.ones(len(time), dtype=int) @@ -311,6 +490,7 @@ def ds_time_idx_from_tdis_settings(start, perlen, nstp=1, tsmult=1.0, time_units deltlist.append(delt) dt_arr = np.cumsum(np.concatenate(deltlist)) + return ds_time_idx(dt_arr, start_datetime=start, time_units=time_units) @@ -476,7 +656,7 @@ def ds_time_idx_from_modeltime(modeltime): ) -def ds_time_idx(t, start_datetime=None, time_units="D"): +def ds_time_idx(t, start_datetime=None, time_units="D", dtype="datetime"): """Get time index variable from elapsed time array. Parameters @@ -487,18 +667,26 @@ def ds_time_idx(t, start_datetime=None, time_units="D"): starting datetime time_units : str, optional time units, default is days + dtype : str, optional + dtype of time index. Can be 'datetime' or 'float'. If 'datetime' try to create + a pandas datetime index if that fails use cftime lib. Default is 'datetime'. Returns ------- IndexVariable time coordinate for xarray data-array or dataset """ - if start_datetime is not None: - dt = pd.to_timedelta(t, time_units) - times = pd.Timestamp(start_datetime) + dt - - else: + if (start_datetime is None) or (dtype in ["int", "float"]): times = t + else: + try: + dtarr = pd.to_timedelta(t, time_units) + times = pd.Timestamp(start_datetime) + dtarr + except (OutOfBoundsDatetime, OutOfBoundsTimedelta) as e: + msg = f"using cftime time index because of {e}" + logger.debug(msg) + start = _pd_timestamp_to_cftime(pd.Timestamp(start_datetime)) + times = [start + dt.timedelta(days=int(td)) for td in t] time = IndexVariable(["time"], times) time.attrs["time_units"] = time_units @@ -518,6 +706,9 @@ def dataframe_to_flopy_timeseries( append=False, ): assert not df.isna().any(axis=None) + assert ( + ds.time.dtype.kind == "M" + ), "get recharge requires a datetime64[ns] time index" if ds is not None: # set index to days after the start of the simulation df = df.copy() @@ -560,6 +751,12 @@ def ds_time_to_pandas_index(ds, include_start=True): pandas datetime index """ if include_start: - return ds.time.to_index().insert(0, pd.Timestamp(ds.time.start)) + if ds.time.dtype.kind == "M": # "M" is a numpy datetime-type + return ds.time.to_index().insert(0, pd.Timestamp(ds.time.start)) + elif ds.time.dtype.kind == "O": + start = _pd_timestamp_to_cftime(pd.Timestamp(ds.time.start)) + return ds.time.to_index().insert(0, start) + elif ds.time.dtype.kind in ["i", "f"]: + return ds.time.to_index().insert(0, 0) else: return ds.time.to_index() diff --git a/nlmod/gwf/surface_water.py b/nlmod/gwf/surface_water.py index 4d7acd4e..72e024f2 100644 --- a/nlmod/gwf/surface_water.py +++ b/nlmod/gwf/surface_water.py @@ -1111,6 +1111,8 @@ def add_season_timeseries( summer_name : str, optional The name of the time-series with ones in summer. The default is "summer". """ + if ds.time.dtype.kind != "M": + raise TypeError("add_season_timeseries requires a datetime64[ns] time index") tmin = pd.to_datetime(ds.time.start) if tmin.month in summer_months: ts_data = [(0.0, 0.0, 1.0)] diff --git a/nlmod/mfoutput/mfoutput.py b/nlmod/mfoutput/mfoutput.py index f1d06eb9..0e98c874 100644 --- a/nlmod/mfoutput/mfoutput.py +++ b/nlmod/mfoutput/mfoutput.py @@ -75,6 +75,7 @@ def _get_time_index(fobj, ds=None, gwf_or_gwt=None): fobj.get_times(), start_datetime=(ds.time.attrs["start"] if "time" in ds else None), time_units=(ds.time.attrs["time_units"] if "time" in ds else None), + dtype="float" if ds.time.dtype.kind in ["i", "f"] else "datetime", ) return tindex diff --git a/nlmod/plot/dcs.py b/nlmod/plot/dcs.py index 67a02ded..c9c55399 100644 --- a/nlmod/plot/dcs.py +++ b/nlmod/plot/dcs.py @@ -566,11 +566,15 @@ def animate( elif "units" in da.attrs: cbar.set_label(da.units) - t = pd.Timestamp(da.time.values[iper]) + if da.time.dtype.kind in ["M", "O"]: + t = pd.Timestamp(da.time.values[iper]).strftime(date_fmt) + else: + t = f"{da.time.values[iper]} {da.time.time_units}" + if plot_title is None: title = None else: - title = self.ax.set_title(f"{plot_title}, t = {t.strftime(date_fmt)}") + title = self.ax.set_title(f"{plot_title}, t = {t}") # update func def update(iper, pc, title): @@ -578,9 +582,13 @@ def update(iper, pc, title): pc.set_array(array) # update title - t = pd.Timestamp(da.time.values[iper]) + if da.time.dtype.kind in ["M", "O"]: + t = pd.Timestamp(da.time.values[iper]).strftime(date_fmt) + else: + t = f"{da.time.values[iper]} {da.time.time_units}" + if title is not None: - title.set_text(f"{plot_title}, t = {t.strftime(date_fmt)}") + title.set_text(f"{plot_title}, t = {t}") return pc, title diff --git a/nlmod/read/bro.py b/nlmod/read/bro.py index 34f54ea0..9facbee3 100644 --- a/nlmod/read/bro.py +++ b/nlmod/read/bro.py @@ -38,6 +38,9 @@ def add_modelled_head(oc, ml=None, ds=None, method="linear"): # this function requires a flopy model object, see # https://github.com/ArtesiaWater/hydropandas/issues/146 + if ds.time.dtype.kind != "M": + raise TypeError("add modelled head requires a datetime64[ns] time index") + oc_modflow = hpd.read_modflow(oc, ml, heads.values, ds.time.values, method=method) if ds.gridtype == "vertex": diff --git a/nlmod/read/knmi.py b/nlmod/read/knmi.py index 5b8d48a7..2329e779 100644 --- a/nlmod/read/knmi.py +++ b/nlmod/read/knmi.py @@ -57,6 +57,9 @@ def get_recharge(ds, method="linear", most_common_station=False): "Please run nlmod.time.set_ds_time()" ) ) + if ds.time.dtype.kind != "M": + raise TypeError("get recharge requires a datetime64[ns] time index") + start = pd.Timestamp(ds.time.attrs["start"]) end = pd.Timestamp(ds.time.data[-1]) diff --git a/nlmod/sim/sim.py b/nlmod/sim/sim.py index 4d5e2438..a8818c1f 100644 --- a/nlmod/sim/sim.py +++ b/nlmod/sim/sim.py @@ -9,6 +9,7 @@ import pandas as pd from .. import util +from ..dims.time import _pd_timestamp_to_cftime logger = logging.getLogger(__name__) @@ -89,12 +90,24 @@ def get_tdis_perioddata(ds, nstp="nstp", tsmult="tsmult"): 1}{tsmult^{nstp}-1}`. """ deltat = pd.to_timedelta(1, ds.time.time_units) - perlen = [ - (pd.to_datetime(ds["time"].data[0]) - pd.to_datetime(ds.time.start)) / deltat - ] - - if len(ds["time"]) > 1: - perlen.extend(np.diff(ds["time"]) / deltat) + if ds.time.dtype.kind == "M": + # dtype is pandas timestamps + perlen = [ + (pd.to_datetime(ds["time"].data[0]) - pd.to_datetime(ds.time.start)) + / deltat + ] + if len(ds["time"]) > 1: + perlen.extend(np.diff(ds["time"]) / deltat) + elif ds.time.dtype.kind == "O": + perlen = [ + (ds["time"].data[0] - _pd_timestamp_to_cftime(pd.Timestamp(ds.time.start))) + / deltat + ] + if len(ds["time"]) > 1: + perlen.extend(np.diff(ds["time"]) / deltat) + elif ds.time.dtype.kind in ["i", "f"]: + perlen = [ds['time'][0]] + perlen.extent(np.diff(ds["time"].values)) nstp = util._get_value_from_ds_datavar(ds, "nstp", nstp, return_da=False) diff --git a/pyproject.toml b/pyproject.toml index 4867067e..16e929cb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -63,7 +63,7 @@ grib = ["cfgrib", "ecmwflibs"] test = ["pytest>=7", "pytest-cov", "pytest-dependency"] nbtest = ["nbformat", "nbconvert>6.4.5"] lint = ["flake8", "isort", "black[jupyter]"] -ci = ["nlmod[full,lint,test,nbtest]", "netCDF4<1.7.0", "pandas<2.1.0"] +ci = ["nlmod[full,lint,test,nbtest]", "numpy==1.26.4"] rtd = [ "nlmod[full]", "ipython", diff --git a/tests/test_016_time.py b/tests/test_016_time.py index 056f7a75..9436ddce 100644 --- a/tests/test_016_time.py +++ b/tests/test_016_time.py @@ -1,6 +1,13 @@ import numpy as np +import xarray as xr +import datetime as dt +import pandas as pd +import cftime import nlmod +import pytest + +from pandas._libs.tslibs.np_datetime import OutOfBoundsDatetime, OutOfBoundsTimedelta def test_estimate_nstp(): @@ -32,3 +39,160 @@ def test_ds_time_from_tdis_settings(): def test_get_time_step_length(): assert (nlmod.time.get_time_step_length(100, 2, 1.5) == np.array([40, 60])).all() + + +def test_time_options(): + """Attempt to list all the variations of start, time and perlen + caling the nlmod.dims.set_ds_time functions + """ + + ds = nlmod.get_ds([0, 1000, 2000, 3000]) + + # start_time str and time int + _ = nlmod.dims.set_ds_time(ds, start="2000-1-1", time=10) + + # start_time str and time list of int + _ = nlmod.dims.set_ds_time(ds, start="2000-1-1", time=[10, 40, 50]) + + # start_time str and time list of timestamps + _ = nlmod.dims.set_ds_time( + ds, start="2000-1-1", time=pd.to_datetime(["2000-2-1", "2000-3-1"]) + ) + + # start_time str and time list of str + _ = nlmod.dims.set_ds_time(ds, start="2000-1-1", time=["2000-2-1", "2000-3-1"]) + + # start_time int and time timestamp + _ = nlmod.dims.set_ds_time(ds, start=5, time=pd.Timestamp("2000-1-1")) + + # start_time timestamp and time timestamp + _ = nlmod.dims.set_ds_time( + ds, start=pd.Timestamp("2000-1-1"), time=pd.Timestamp("2000-2-1") + ) + + # start_time timestamp and time int + _ = nlmod.dims.set_ds_time(ds, start=pd.Timestamp("2000-1-1"), time=10) + + # start_time timestamp and time str + _ = nlmod.dims.set_ds_time(ds, start=pd.Timestamp("2000-1-1"), time="2000-2-1") + + # start_time timestamp and time list of timestamp + _ = nlmod.dims.set_ds_time( + ds, + start=pd.Timestamp("2000-1-1"), + time=pd.to_datetime(["2000-2-1", "2000-3-1"]), + ) + + # start_time str and perlen int + _ = nlmod.dims.set_ds_time(ds, start="2000-1-1", perlen=10) + + # start_time str and perlen list of int + _ = nlmod.dims.set_ds_time(ds, start="2000-1-1", perlen=[10, 30, 50]) + + # start_time timestamp and perlen list of int + _ = nlmod.dims.set_ds_time(ds, start=pd.Timestamp("2000-1-1"), perlen=[10, 30, 50]) + + +def test_time_out_of_bounds(): + """related to this issue: https://github.com/gwmod/nlmod/issues/374 + + pandas timestamps can only do computations with dates between the years 1678 and 2262. + """ + + ds = nlmod.get_ds([0, 1000, 2000, 3000]) + + cftime_ind = xr.date_range("1000-01-02", "9999-01-01", freq="100YS") + start_model = cftime.datetime(1000, 1, 1) + + # start cf.datetime and time CFTimeIndex + _ = nlmod.dims.set_ds_time(ds, start=start_model, time=cftime_ind) + + # start cf.datetime and time list of int + _ = nlmod.dims.set_ds_time(ds, start=start_model, time=[10, 20, 21, 55]) + + # start cf.datetime and time list of int + _ = nlmod.dims.set_ds_time( + ds, start=start_model, time=pd.to_datetime(["2000-2-1", "2000-3-1"]) + ) + + # start cf.datetime and time list of str (no general method to convert str to cftime) + with pytest.raises(OutOfBoundsDatetime): + nlmod.dims.set_ds_time(ds, start=start_model, time=["1000-01-02", "1000-01-03"]) + + # start cf.datetime and perlen int + _ = nlmod.dims.set_ds_time(ds, start=start_model, perlen=365) + + # start str and time CFTimeIndex + _ = nlmod.dims.set_ds_time(ds, start="1000-01-01", time=cftime_ind) + + # start str and time int + with pytest.raises(OutOfBoundsDatetime): + nlmod.dims.set_ds_time(ds, start="1000-01-01", time=1) + + # start str and time list of int + with pytest.raises(OutOfBoundsDatetime): + nlmod.dims.set_ds_time(ds, start="1000-01-01", time=[10, 20, 21, 55]) + + # start str and time list of timestamp + _ = nlmod.dims.set_ds_time( + ds, start="1000-01-01", time=pd.to_datetime(["2000-2-1", "2000-3-1"]) + ) + + # start str and time list of str (no general method to convert str to cftime) + with pytest.raises(OutOfBoundsDatetime): + nlmod.dims.set_ds_time(ds, start="1000-01-01", time=["1000-2-1", "1000-3-1"]) + + # start str and perlen int + with pytest.raises(OutOfBoundsTimedelta): + nlmod.dims.set_ds_time(ds, start="1000-01-01", perlen=365000) + + # start numpy datetime and perlen list of int + with pytest.raises(OutOfBoundsDatetime): + nlmod.dims.set_ds_time( + ds, start=np.datetime64("1000-01-01"), perlen=[10, 100, 24] + ) + + # start numpy datetime and time list of timestamps + _ = nlmod.dims.set_ds_time( + ds, + start=np.datetime64("1000-01-01"), + time=pd.to_datetime(["2000-2-1", "2000-3-1"]), + ) + + # start numpy datetime and time list of str + with pytest.raises(OutOfBoundsDatetime): + nlmod.dims.set_ds_time( + ds, start=np.datetime64("1000-01-01"), time=["1000-2-1", "1000-3-1"] + ) + + # start timestamp and perlen list of int + with pytest.raises(OutOfBoundsDatetime): + nlmod.dims.set_ds_time( + ds, start=pd.Timestamp("1000-01-01"), perlen=[10, 100, 24] + ) + + # start timestamp and time CFTimeIndex + _ = nlmod.dims.set_ds_time(ds, start=pd.Timestamp("1000-01-01"), time=cftime_ind) + + # start int and time CFTimeIndex + _ = nlmod.dims.set_ds_time(ds, start=96500, time=cftime_ind) + + # start int and time timestamp + with pytest.raises(OutOfBoundsDatetime): + nlmod.dims.set_ds_time(ds, start=96500, time=pd.Timestamp("1000-01-01")) + + # start int and time str + with pytest.raises(TypeError): + nlmod.dims.set_ds_time(ds, start=96500, time="1000-01-01") + + +def test_numerical_time_index(): + ds = nlmod.get_ds([0, 1000, 2000, 3000]) + + # start str and time floats + _ = nlmod.dims.set_ds_time_numeric(ds, start="2000-1-1", time=[10.0, 20.0, 30.1]) + + # start timestamp and time ints + _ = nlmod.dims.set_ds_time_numeric( + ds, start=pd.Timestamp("1000-01-01"), time=[10, 20, 30] + )