diff --git a/examples/02_knmi_observations.ipynb b/examples/02_knmi_observations.ipynb index de33e34d..91a1630c 100644 --- a/examples/02_knmi_observations.ipynb +++ b/examples/02_knmi_observations.ipynb @@ -1477,11 +1477,10 @@ "oc = hpd.ObsCollection([precip1, precip2])\n", "gdf = oc.to_gdf()\n", "gdf = gdf.set_crs(28992)\n", - "gdf = gdf.to_crs(3857)\n", "gdf[\"name\"] = gdf.index\n", "ax = gdf.buffer(2000).plot(alpha=0, figsize=(8, 8))\n", "gdf.plot(\"name\", ax=ax, cmap=\"jet\", legend=True, markersize=100)\n", - "cx.add_basemap(ax)" + "cx.add_basemap(ax, crs=28992)" ] }, { @@ -3331,9 +3330,9 @@ ], "metadata": { "kernelspec": { - "display_name": "hpd_env", + "display_name": "dev", "language": "python", - "name": "python3" + "name": "dev" }, "language_info": { "codemirror_mode": { @@ -3345,7 +3344,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.10.12" + "version": "3.10.13" } }, "nbformat": 4, diff --git a/hydropandas/io/bro.py b/hydropandas/io/bro.py index 01f1ae4b..7b249d91 100644 --- a/hydropandas/io/bro.py +++ b/hydropandas/io/bro.py @@ -148,14 +148,12 @@ def get_bro_groundwater(bro_id, tube_nr=None, only_metadata=False, **kwargs): empty_df = pd.DataFrame() return empty_df, meta + dfl = [] for i, gld_id in enumerate(gld_ids): - if i == 0: - df, meta_new = measurements_from_gld(gld_id, **kwargs) - meta.update(meta_new) - else: - df_new, meta_new = measurements_from_gld(gld_id, **kwargs) - df = pd.concat([df, df_new], axis=1) - meta.update(meta_new) + df, meta_new = measurements_from_gld(gld_id, **kwargs) + meta.update(meta_new) + dfl.append(df) + df = pd.concat(dfl, axis=0) return df, meta @@ -305,7 +303,7 @@ def measurements_from_gld( # to dataframe df = pd.DataFrame( - index=pd.to_datetime(times), + index=pd.to_datetime(times, utc=True).tz_convert("CET"), data={"values": values, "qualifier": qualifiers}, ) @@ -313,7 +311,7 @@ def measurements_from_gld( if to_wintertime: # remove time zone information by transforming to dutch winter time df.index = pd.to_datetime(df.index, utc=True).tz_localize(None) + pd.Timedelta( - 1, unit="H" + 1, unit="h" ) # duplicates diff --git a/hydropandas/io/knmi.py b/hydropandas/io/knmi.py index 0e50537d..cbe4aede 100644 --- a/hydropandas/io/knmi.py +++ b/hydropandas/io/knmi.py @@ -108,7 +108,9 @@ def get_knmi_obs( ts, meta = get_knmi_timeseries_stn(stn, meteo_var, settings, start, end) elif fname is not None: logger.info(f"get KNMI data from file {fname} and meteo variable {meteo_var}") - ts, meta = get_knmi_timeseries_fname(fname, meteo_var, settings, start, end) + ts, meta = get_knmi_timeseries_fname( + str(fname), meteo_var, settings, start, end + ) elif xy is not None: logger.info( f"get KNMI data from station nearest to coordinates {xy} and meteo" diff --git a/hydropandas/io/lizard.py b/hydropandas/io/lizard.py index d93e6358..694e1b78 100644 --- a/hydropandas/io/lizard.py +++ b/hydropandas/io/lizard.py @@ -161,7 +161,7 @@ def _prepare_API_input(nr_pages, url_groundwater): urls = [] for page in range(nr_pages): true_page = page + 1 # The real page number is attached to the import thread - urls = [url_groundwater + "&page={}".format(true_page)] + urls += [url_groundwater + "&page={}".format(true_page)] return urls @@ -185,6 +185,32 @@ def _download(url, timeout=1800): return data +def _split_mw_tube_nr(code): + """get the tube number from a code that consists of the name and the tube number. + + Parameters + ---------- + code : str + name + tube_nr. e.g. 'BUWP014-11' or 'BUWP014012' + + Returns + ------- + monitoring well, tube_number (str, int) + + Notes + ----- + The format of the name + tube_nr is not very consistent and this function may need + further finetuning. + """ + + if code[-3:].isdigit(): + return code[:-3], int(code[-3:]) + else: + # assume there is a '-' to split name and filter number + tube_nr = code.split("-")[-1] + return code.strip(f"-{tube_nr}"), int(tube_nr) + + def get_metadata_tube(metadata_mw, tube_nr): """Extract the metadata for a specific tube from the monitoring well metadata. @@ -218,19 +244,54 @@ def get_metadata_tube(metadata_mw, tube_nr): "status": None, } + metadata_tube_list = [] for metadata_tube in metadata_mw["filters"]: - if metadata_tube["code"].endswith(str(tube_nr)): - break - else: + # check if name+filternr ends with three digits + code, tbnr = _split_mw_tube_nr(metadata_tube["code"]) + if tbnr == tube_nr: + metadata_tube_list.append(metadata_tube) + + if len(metadata_tube_list) == 0: raise ValueError(f"{metadata_mw['name']} doesn't have a tube number {tube_nr}") + elif len(metadata_tube_list) == 1: + mtd_tube = metadata_tube_list[0] + elif len(metadata_tube_list) > 1: + # tube has probably been replaced, multiple tubes with the same code and tube nr + # merge metadata from all tubes + logger.info( + f"there are {len(metadata_tube_list)} instances of {code} and tube " + f"{tube_nr}, trying to merge all in one observation object" + ) + mtd_tube = metadata_tube_list[0].copy() + relevant_keys = { + "top_level", + "filter_top_level", + "filter_bottom_level", + "timeseries", + } + for metadata_tube in metadata_tube_list: + for key in set(metadata_tube.keys()) & relevant_keys: + # check if properties are always the same for a tube number + val = metadata_tube[key] + if key in ["top_level", "filter_top_level", "filter_bottom_level"]: + if val != mtd_tube[key]: + logger.warning( + f"multiple {key} values found ({val} & {mtd_tube[key]})" + f" for {code} and tube {tube_nr}, using {mtd_tube[key]}" + ) + # merge time series from all tubes with the same code and tube number + elif key == "timeseries": + mtd_tube[key] += val + + mtd_tube["code"] = f"{code}{tube_nr}" metadata.update( { "tube_nr": tube_nr, - "name": metadata_tube["code"].replace("-", ""), - "tube_top": metadata_tube["top_level"], - "screen_top": metadata_tube["filter_top_level"], - "screen_bottom": metadata_tube["filter_bottom_level"], + "name": mtd_tube["code"].replace("-", ""), + "tube_top": mtd_tube["top_level"], + "screen_top": mtd_tube["filter_top_level"], + "screen_bottom": mtd_tube["filter_bottom_level"], } ) @@ -238,10 +299,10 @@ def get_metadata_tube(metadata_mw, tube_nr): transformer = Transformer.from_crs("WGS84", "EPSG:28992") metadata["x"], metadata["y"] = transformer.transform(lat, lon) - if not metadata_tube["timeseries"]: + if not mtd_tube["timeseries"]: metadata["timeseries_type"] = None else: - for series in metadata_tube["timeseries"]: + for series in mtd_tube["timeseries"]: series_info = requests.get(series).json() if series_info["name"] == "WNS9040.hand": metadata["uuid_hand"] = series_info["uuid"] @@ -382,8 +443,6 @@ def _combine_timeseries(hand_measurements, diver_measurements): measurements = measurements.loc[ :, ["value_hand", "value_diver", "flag_hand", "flag_diver"] ] - measurements.loc[:, "name"] = hand_measurements.loc[:, "name"][0] - measurements.loc[:, "filter_nr"] = hand_measurements.loc[:, "filter_nr"][0] return measurements @@ -413,6 +472,7 @@ def get_timeseries_tube(tube_metadata, tmin, tmax, type_timeseries): metadata_df : dict metadata of the monitoring well """ + if tube_metadata["timeseries_type"] is None: return pd.DataFrame(), tube_metadata @@ -559,18 +619,23 @@ class of the observations, e.g. GroundwaterObs obs_list = [] for code in codes: groundwaterstation_metadata = get_metadata_mw_from_code(code) + tubes = [] if tube_nr == "all": for metadata_tube in groundwaterstation_metadata["filters"]: - tube_nr = int(metadata_tube["code"][-3:]) - o = ObsClass.from_lizard( - code, - tube_nr, - tmin, - tmax, - type_timeseries, - only_metadata=only_metadata, - ) - obs_list.append(o) + tnr = _split_mw_tube_nr(metadata_tube["code"])[-1] + if tnr not in tubes: + logger.info(f"get {code}{tnr}") + o = ObsClass.from_lizard( + code, + tnr, + tmin, + tmax, + type_timeseries, + only_metadata=only_metadata, + ) + obs_list.append(o) + tubes.append(tnr) + else: o = ObsClass.from_lizard( code, tube_nr, tmin, tmax, type_timeseries, only_metadata=only_metadata diff --git a/hydropandas/io/solinst.py b/hydropandas/io/solinst.py new file mode 100644 index 00000000..1b145380 --- /dev/null +++ b/hydropandas/io/solinst.py @@ -0,0 +1,138 @@ +import logging +import os +import zipfile + +import numpy as np +import pandas as pd +from pyproj import Transformer + +logger = logging.getLogger(__name__) + + +def read_solinst_file( + path, + transform_coords=True, +): + """Read Solinst logger file (XLE) + + Parameters + ---------- + path : str + path to Solinst file (.xle) + transform_coords : boolean + convert coordinates from WGS84 to RD + + Returns + ------- + df : pandas.DataFrame + DataFrame containing file content + meta : dict, optional + dict containing meta + """ + + # open file + path = str(path) + name = os.path.splitext(os.path.basename(path))[0] + if path.endswith(".xle"): + f = path + elif path.endswith(".zip"): + zf = zipfile.ZipFile(path) + f = zf.open("{}.xle".format(name)) + else: + raise NotImplementedError( + "File type '{}' not supported!".format(os.path.splitext(path)[-1]) + ) + + logger.info("reading -> {}".format(f)) + + # read channel 1 data header + df_ch1_data_header = pd.read_xml(path, xpath="/Body_xle/Ch1_data_header") + series_ch1_data_header = df_ch1_data_header.T.iloc[:, 0] + colname_ch1 = ( + series_ch1_data_header.Identification.lower() + + "_" + + series_ch1_data_header.Unit.lower() + ) + + # read channel 2 data header + df_ch2_data_header = pd.read_xml(path, xpath="/Body_xle/Ch2_data_header") + series_ch2_data_header = df_ch2_data_header.T.iloc[:, 0] + colname_ch2 = ( + series_ch2_data_header.Identification.lower() + + "_" + + series_ch2_data_header.Unit.lower() + ) + + # read observations + df = pd.read_xml( + path, + xpath="/Body_xle/Data/Log", + ) + df.rename(columns={"ch1": colname_ch1, "ch2": colname_ch2}, inplace=True) + if "ms" in df.columns: + df["date_time"] = pd.to_datetime( + df["Date"] + " " + df["Time"] + ) + pd.to_timedelta(df["ms"], unit="ms") + drop_cols = ["id", "Date", "Time", "ms"] + else: + df["date_time"] = pd.to_datetime(df["Date"] + " " + df["Time"]) + drop_cols = ["id", "Date", "Time"] + df.set_index("date_time", inplace=True) + + df.drop(columns=drop_cols, inplace=True) + + # parse meta into dict, per group in XLE file + meta = {} + # read file info + df_file_info = pd.read_xml(path, xpath="/Body_xle/File_info") + dict_file_info = df_file_info.T.iloc[:, 0].to_dict() + + # read instrument info + df_instrument_info = pd.read_xml(path, xpath="/Body_xle/Instrument_info") + dict_instrument_info = df_instrument_info.T.iloc[:, 0].to_dict() + + # read instrument info + df_instrument_info_data_header = pd.read_xml( + path, xpath="/Body_xle/Instrument_info_data_header" + ) + dict_instrument_info_data_header = df_instrument_info_data_header.T.iloc[ + :, 0 + ].to_dict() + + meta = { + **dict_file_info, + **dict_instrument_info, + **dict_instrument_info_data_header, + } + + if transform_coords: + # lat and lon has 0,000 when location is not supplied + # replace comma with point first + if isinstance(meta["Latitude"], str): + meta["Latitude"] = float(meta["Latitude"].replace(",", ".")) + if isinstance(meta["Longtitude"], str): + meta["Longtitude"] = float(meta["Longtitude"].replace(",", ".")) + if (meta["Latitude"] != 0) & (meta["Longtitude"] != 0): + # NOTE: check EPSG:28992 definition and whether location is showing up in + # the right spot. + transformer = Transformer.from_crs("epsg:4326", "epsg:28992") + x, y = transformer.transform(meta["Latitude"], meta["Longtitude"]) + x = np.round(x, 2) + y = np.round(y, 2) + else: + logger.warning("file has no location included") + x = None + y = None + else: + x = meta["Latitude"] + y = meta["Longtitude"] + meta["x"] = x + meta["y"] = y + meta["filename"] = f + meta["source"] = meta["Created_by"] + meta["name"] = name + meta["monitoring_well"] = name + meta["unit"] = series_ch1_data_header.Unit.lower() + meta["metadata_available"] = True + + return df, meta diff --git a/hydropandas/io/waterinfo.py b/hydropandas/io/waterinfo.py index 5c2aeef6..7410d584 100644 --- a/hydropandas/io/waterinfo.py +++ b/hydropandas/io/waterinfo.py @@ -156,7 +156,9 @@ def read_waterinfo_obs(file_or_dir, ObsClass, progressbar=False, **kwargs): # get location and convert to m RD for stn in df[location_col].unique(): mask = df[location_col] == stn - x, y = transformer.transform(df.loc[mask, "X"][-1], df.loc[mask, "Y"][-1]) + x, y = transformer.transform( + df.loc[mask, "X"].iloc[-1], df.loc[mask, "Y"].iloc[-1] + ) metadata = { "name": stn, "x": x, diff --git a/hydropandas/obs_collection.py b/hydropandas/obs_collection.py index 70a2a6da..fbcd3cf9 100644 --- a/hydropandas/obs_collection.py +++ b/hydropandas/obs_collection.py @@ -2031,6 +2031,50 @@ def from_pastastore( } return cls(obs_df, name=pstore.name, meta=meta) + def get_obs(self, name=None, **kwargs): + """get an observation object from a collection + + Parameters + ---------- + name : str or None, optional + name of the observation you want to select, by default None + **kwargs : any metadata, value pair e.g. for a collection of GroundwaterObs: + tube_nr = 1 or source = 'BRO' + + Returns + ------- + hpd.Obs + Observation object from the collection. + + Raises + ------ + ValueError + If multiple observations in the collection match the given attribute values. + ValueError + If no observation in the collection match the given attribute values. + """ + + # select by name + if name is None: + selected_obs = self + else: + selected_obs = self.loc[[name]] + + # select by condition + for key, item in kwargs.items(): + condition = selected_obs[key] == item + selected_obs = selected_obs.loc[condition] + + # return an Obs objet + if len(selected_obs) == 1: + return selected_obs["obs"].values[0] + elif len(selected_obs) == 0: + raise ValueError("no observations for given conditions") + else: + raise ValueError( + f"multiple observations for given conditions {selected_obs.index}" + ) + def to_excel(self, path, meta_sheet_name="metadata"): """Write an ObsCollection to an excel, the first sheet in the excel contains the metadata, the other tabs are the timeseries of each observation. @@ -2073,14 +2117,14 @@ def to_excel(self, path, meta_sheet_name="metadata"): oc["obs"] = [type(o).__name__ for o in obseries] # write ObsCollection dataframe to first sheet - super(ObsCollection, oc).to_excel(writer, meta_sheet_name) + super(ObsCollection, oc).to_excel(writer, sheet_name=meta_sheet_name) # write each observation time series to next sheets for o in obseries: sheetname = o.name for ch in ["[", "]", ":", "*", "?", "/", "\\"]: sheetname = sheetname.replace(ch, "_") - o.to_excel(writer, sheetname) + o.to_excel(writer, sheet_name=sheetname) def to_pi_xml(self, fname, timezone="", version="1.24"): from .io import fews diff --git a/hydropandas/observation.py b/hydropandas/observation.py index deb57d20..e6efdf1c 100644 --- a/hydropandas/observation.py +++ b/hydropandas/observation.py @@ -837,6 +837,47 @@ def from_pastastore(cls, pstore, libname, name, metadata_mapping=None): return cls(data, meta=metadata, **kwargs) + @classmethod + def from_solinst( + cls, + path, + transform_coords=True, + screen_bottom=None, + screen_top=None, + ground_level=None, + tube_nr=None, + tube_top=None, + ): + """Read data from Solinst xle file. + + Parameters + ---------- + path : str + path to file (file can zip or xle) + + """ + from .io import solinst + + df, meta = solinst.read_solinst_file(path, transform_coords=transform_coords) + + return cls( + df, + meta=meta, + name=meta.pop("name"), + x=meta.pop("x"), + y=meta.pop("y"), + filename=meta.pop("filename"), + source=meta.pop("source"), + unit=meta.pop("unit"), + screen_bottom=screen_bottom, + screen_top=screen_top, + ground_level=ground_level, + metadata_available=meta.pop("metadata_available"), + monitoring_well=meta.pop("monitoring_well"), + tube_nr=tube_nr, + tube_top=tube_top, + ) + class WaterQualityObs(Obs): """Class for water quality ((grond)watersamenstelling) point observations. diff --git a/hydropandas/util.py b/hydropandas/util.py index 35e7da51..53a863cc 100644 --- a/hydropandas/util.py +++ b/hydropandas/util.py @@ -120,13 +120,13 @@ def get_files( raise ValueError("Please specify a different folder to unpack files!") # identify whether file_or_dir started as zip - if file_or_dir.endswith(".zip"): + if str(file_or_dir).endswith(".zip"): iszip = True else: iszip = False # unzip dir - if file_or_dir.endswith(".zip"): + if iszip: zipf = file_or_dir if unpackdir is None: file_or_dir = tempfile.TemporaryDirectory().name @@ -288,12 +288,13 @@ def oc_to_df(oc, col: Optional[str] = None) -> pd.DataFrame: _description_ """ df_list = [] - for obs in oc.obs.values: - if not obs.empty: + for o in oc.obs.values: + if not o.empty: if col is None: - vals = obs.loc[:, obs._get_first_numeric_col_name()] + vals = o.loc[:, o._get_first_numeric_col_name()] else: - vals = obs.loc[:, col] + vals = o.loc[:, col] + vals.name = o.name df_list.append(vals) return pd.concat(df_list, axis=1) diff --git a/hydropandas/version.py b/hydropandas/version.py index 61fb31ca..ae6db5f1 100644 --- a/hydropandas/version.py +++ b/hydropandas/version.py @@ -1 +1 @@ -__version__ = "0.10.0" +__version__ = "0.11.0" diff --git a/pyproject.toml b/pyproject.toml index 26b7f91d..cbd5ebb3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -18,7 +18,7 @@ maintainers = [ requires-python = ">=3.7" dependencies = [ "scipy", - "pandas<2.1.0", + "pandas", "matplotlib", "tqdm", "requests", diff --git a/tests/data/2024-solinst-test/WsNoo_dp366_BUB_20231222_slug1m.xle b/tests/data/2024-solinst-test/WsNoo_dp366_BUB_20231222_slug1m.xle new file mode 100644 index 00000000..4124b10e --- /dev/null +++ b/tests/data/2024-solinst-test/WsNoo_dp366_BUB_20231222_slug1m.xle @@ -0,0 +1,4164 @@ + + + + + +2023/12/22 + + +iOS App Version 3.5.1 +iOS App Version 3.5.1 +iOS App Version 3.5.1 +Desktop Reader + + +L5_LT +M5 +Stopped +2170536 +3.50600 +1378.03 +1970/01/01 00:00:00 +2 +1.006 + + +20231123 +slugtest +51.4253 +3.91670 +12 +0 + +Slate +2 +0.000000 + +2023/12/22 10:16:37 +2023/12/22 10:17:50 +587 + + +LEVEL +m + + + + + +TEMPERATURE +°C + + + + +2023/12/22 + +0 +10.2388 +15.462 + + +2023/12/22 + +125 +10.2392 +15.462 + + +2023/12/22 + +250 +10.2389 +15.463 + + +2023/12/22 + +375 +10.2389 +15.464 + + +2023/12/22 + +500 +10.2390 +15.464 + + +2023/12/22 + +625 +10.2391 +15.466 + + +2023/12/22 + +750 +10.2389 +15.464 + + +2023/12/22 + +875 +10.2389 +15.463 + + +2023/12/22 + +0 +10.2389 +15.466 + + +2023/12/22 + +125 +10.2389 +15.466 + + +2023/12/22 + +250 +10.2392 +15.466 + + +2023/12/22 + +375 +10.2387 +15.466 + + +2023/12/22 + +500 +10.2390 +15.468 + + +2023/12/22 + +625 +10.2389 +15.467 + + +2023/12/22 + +750 +10.2389 +15.468 + + +2023/12/22 + +875 +10.2386 +15.465 + + +2023/12/22 + +0 +10.2388 +15.466 + + +2023/12/22 + +125 +10.2391 +15.467 + + +2023/12/22 + +250 +10.2388 +15.468 + + +2023/12/22 + +375 +10.2388 +15.466 + + +2023/12/22 + +500 +10.2387 +15.468 + + +2023/12/22 + +625 +10.2390 +15.469 + + +2023/12/22 + +750 +10.2389 +15.467 + + +2023/12/22 + +875 +10.2390 +15.468 + + +2023/12/22 + +0 +10.2390 +15.467 + + +2023/12/22 + +125 +10.2391 +15.469 + + +2023/12/22 + +250 +10.2390 +15.470 + + +2023/12/22 + +375 +10.2392 +15.470 + + +2023/12/22 + +500 +10.2391 +15.471 + + +2023/12/22 + +625 +10.2391 +15.469 + + +2023/12/22 + +750 +10.2392 +15.471 + + +2023/12/22 + +875 +10.2393 +15.471 + + +2023/12/22 + +0 +10.2392 +15.471 + + +2023/12/22 + +125 +10.2392 +15.471 + + +2023/12/22 + +250 +10.2391 +15.469 + + +2023/12/22 + +375 +10.2393 +15.470 + + +2023/12/22 + +500 +10.2392 +15.472 + + +2023/12/22 + +625 +10.2392 +15.471 + + +2023/12/22 + +750 +10.2393 +15.474 + + +2023/12/22 + +875 +10.2393 +15.473 + + +2023/12/22 + +0 +10.2394 +15.473 + + +2023/12/22 + +125 +10.2393 +15.473 + + +2023/12/22 + +250 +10.2393 +15.473 + + +2023/12/22 + +375 +10.2392 +15.474 + + +2023/12/22 + +500 +10.2393 +15.474 + + +2023/12/22 + +625 +10.2393 +15.476 + + +2023/12/22 + +750 +10.2394 +15.476 + + +2023/12/22 + +875 +10.2394 +15.474 + + +2023/12/22 + +0 +10.2394 +15.475 + + +2023/12/22 + +125 +10.2395 +15.478 + + +2023/12/22 + +250 +10.2394 +15.475 + + +2023/12/22 + +375 +10.2395 +15.474 + + +2023/12/22 + +500 +10.2396 +15.476 + + +2023/12/22 + +625 +10.2395 +15.477 + + +2023/12/22 + +750 +10.2395 +15.477 + + +2023/12/22 + +875 +10.2393 +15.476 + + +2023/12/22 + +0 +10.2394 +15.477 + + +2023/12/22 + +125 +10.2394 +15.479 + + +2023/12/22 + +250 +10.2396 +15.478 + + +2023/12/22 + +375 +10.2394 +15.478 + + +2023/12/22 + +500 +10.2392 +15.478 + + +2023/12/22 + +625 +10.2393 +15.478 + + +2023/12/22 + +750 +10.2394 +15.481 + + +2023/12/22 + +875 +10.2396 +15.478 + + +2023/12/22 + +0 +10.2394 +15.479 + + +2023/12/22 + +125 +10.2393 +15.479 + + +2023/12/22 + +250 +10.2391 +15.481 + + +2023/12/22 + +375 +10.2391 +15.481 + + +2023/12/22 + +500 +10.2391 +15.479 + + +2023/12/22 + +625 +10.2391 +15.481 + + +2023/12/22 + +750 +10.2390 +15.479 + + +2023/12/22 + +875 +10.2390 +15.481 + + +2023/12/22 + +0 +10.2391 +15.483 + + +2023/12/22 + +125 +10.2389 +15.482 + + +2023/12/22 + +250 +10.2386 +15.482 + + +2023/12/22 + +375 +10.2388 +15.482 + + +2023/12/22 + +500 +10.2389 +15.483 + + +2023/12/22 + +625 +10.2389 +15.483 + + +2023/12/22 + +750 +10.2386 +15.483 + + +2023/12/22 + +875 +10.2384 +15.485 + + +2023/12/22 + +0 +10.2387 +15.485 + + +2023/12/22 + +125 +10.2385 +15.484 + + +2023/12/22 + +250 +10.2386 +15.485 + + +2023/12/22 + +375 +10.2387 +15.485 + + +2023/12/22 + +500 +10.2390 +15.485 + + +2023/12/22 + +625 +10.2388 +15.485 + + +2023/12/22 + +750 +10.2390 +15.487 + + +2023/12/22 + +875 +10.2388 +15.487 + + +2023/12/22 + +0 +10.2390 +15.488 + + +2023/12/22 + +125 +10.2392 +15.486 + + +2023/12/22 + +250 +10.2391 +15.486 + + +2023/12/22 + +375 +10.2390 +15.489 + + +2023/12/22 + +500 +10.2390 +15.488 + + +2023/12/22 + +625 +10.2389 +15.486 + + +2023/12/22 + +750 +10.2390 +15.488 + + +2023/12/22 + +875 +10.2393 +15.488 + + +2023/12/22 + +0 +10.2389 +15.491 + + +2023/12/22 + +125 +10.2391 +15.491 + + +2023/12/22 + +250 +10.2390 +15.490 + + +2023/12/22 + +375 +10.2392 +15.489 + + +2023/12/22 + +500 +10.2391 +15.489 + + +2023/12/22 + +625 +10.2392 +15.490 + + +2023/12/22 + +750 +10.2392 +15.492 + + +2023/12/22 + +875 +10.2391 +15.490 + + +2023/12/22 + +0 +10.2390 +15.491 + + +2023/12/22 + +125 +10.2392 +15.491 + + +2023/12/22 + +250 +10.2391 +15.491 + + +2023/12/22 + +375 +10.2391 +15.491 + + +2023/12/22 + +500 +10.2389 +15.493 + + +2023/12/22 + +625 +10.2391 +15.493 + + +2023/12/22 + +750 +10.2390 +15.492 + + +2023/12/22 + +875 +10.2388 +15.494 + + +2023/12/22 + +0 +10.2390 +15.494 + + +2023/12/22 + +125 +10.2390 +15.496 + + +2023/12/22 + +250 +10.2389 +15.495 + + +2023/12/22 + +375 +10.2390 +15.496 + + +2023/12/22 + +500 +10.2390 +15.495 + + +2023/12/22 + +625 +10.2389 +15.496 + + +2023/12/22 + +750 +10.2392 +15.495 + + +2023/12/22 + +875 +10.2391 +15.494 + + +2023/12/22 + +0 +10.2387 +15.497 + + +2023/12/22 + +125 +10.2386 +15.495 + + +2023/12/22 + +250 +10.2389 +15.497 + + +2023/12/22 + +375 +10.2390 +15.498 + + +2023/12/22 + +500 +10.2390 +15.497 + + +2023/12/22 + +625 +10.2391 +15.497 + + +2023/12/22 + +750 +10.2392 +15.497 + + +2023/12/22 + +875 +10.2391 +15.498 + + +2023/12/22 + +0 +10.2390 +15.498 + + +2023/12/22 + +125 +10.2391 +15.499 + + +2023/12/22 + +250 +10.2388 +15.499 + + +2023/12/22 + +375 +10.2390 +15.500 + + +2023/12/22 + +500 +10.2389 +15.498 + + +2023/12/22 + +625 +10.2395 +15.501 + + +2023/12/22 + +750 +10.2393 +15.501 + + +2023/12/22 + +875 +10.2391 +15.500 + + +2023/12/22 + +0 +10.2392 +15.502 + + +2023/12/22 + +125 +10.2391 +15.502 + + +2023/12/22 + +250 +10.2394 +15.503 + + +2023/12/22 + +375 +10.2390 +15.501 + + +2023/12/22 + +500 +10.2389 +15.504 + + +2023/12/22 + +625 +10.2389 +15.501 + + +2023/12/22 + +750 +10.2390 +15.502 + + +2023/12/22 + +875 +10.2389 +15.503 + + +2023/12/22 + +0 +10.2387 +15.504 + + +2023/12/22 + +125 +10.2390 +15.504 + + +2023/12/22 + +250 +10.2389 +15.504 + + +2023/12/22 + +375 +10.2389 +15.503 + + +2023/12/22 + +500 +10.2391 +15.504 + + +2023/12/22 + +625 +10.2389 +15.504 + + +2023/12/22 + +750 +10.2391 +15.503 + + +2023/12/22 + +875 +10.2391 +15.507 + + +2023/12/22 + +0 +10.2393 +15.508 + + +2023/12/22 + +125 +10.2390 +15.507 + + +2023/12/22 + +250 +10.2392 +15.506 + + +2023/12/22 + +375 +10.2392 +15.506 + + +2023/12/22 + +500 +10.2391 +15.509 + + +2023/12/22 + +625 +10.2393 +15.506 + + +2023/12/22 + +750 +10.2391 +15.507 + + +2023/12/22 + +875 +10.2392 +15.508 + + +2023/12/22 + +0 +10.2391 +15.509 + + +2023/12/22 + +125 +10.2392 +15.509 + + +2023/12/22 + +250 +10.2391 +15.510 + + +2023/12/22 + +375 +10.2392 +15.509 + + +2023/12/22 + +500 +10.2396 +15.511 + + +2023/12/22 + +625 +10.2392 +15.510 + + +2023/12/22 + +750 +10.2392 +15.509 + + +2023/12/22 + +875 +10.2392 +15.510 + + +2023/12/22 + +0 +10.2394 +15.511 + + +2023/12/22 + +125 +10.2394 +15.512 + + +2023/12/22 + +250 +10.2394 +15.513 + + +2023/12/22 + +375 +10.2395 +15.512 + + +2023/12/22 + +500 +10.2394 +15.511 + + +2023/12/22 + +625 +10.2393 +15.512 + + +2023/12/22 + +750 +10.2393 +15.511 + + +2023/12/22 + +875 +10.2395 +15.512 + + +2023/12/22 + +0 +10.2394 +15.514 + + +2023/12/22 + +125 +10.2394 +15.514 + + +2023/12/22 + +250 +10.2394 +15.513 + + +2023/12/22 + +375 +10.2394 +15.515 + + +2023/12/22 + +500 +10.2393 +15.514 + + +2023/12/22 + +625 +10.2394 +15.515 + + +2023/12/22 + +750 +10.2393 +15.514 + + +2023/12/22 + +875 +10.2392 +15.514 + + +2023/12/22 + +0 +10.2393 +15.517 + + +2023/12/22 + +125 +10.2393 +15.516 + + +2023/12/22 + +250 +10.2391 +15.518 + + +2023/12/22 + +375 +10.2393 +15.518 + + +2023/12/22 + +500 +10.2392 +15.517 + + +2023/12/22 + +625 +10.2391 +15.517 + + +2023/12/22 + +750 +10.2392 +15.517 + + +2023/12/22 + +875 +10.2393 +15.518 + + +2023/12/22 + +0 +10.2392 +15.517 + + +2023/12/22 + +125 +10.2394 +15.519 + + +2023/12/22 + +250 +10.2392 +15.520 + + +2023/12/22 + +375 +10.2392 +15.520 + + +2023/12/22 + +500 +10.2393 +15.521 + + +2023/12/22 + +625 +10.2392 +15.519 + + +2023/12/22 + +750 +10.2393 +15.522 + + +2023/12/22 + +875 +10.2394 +15.521 + + +2023/12/22 + +0 +10.2392 +15.520 + + +2023/12/22 + +125 +10.2393 +15.521 + + +2023/12/22 + +250 +10.2394 +15.521 + + +2023/12/22 + +375 +10.2394 +15.522 + + +2023/12/22 + +500 +10.2393 +15.521 + + +2023/12/22 + +625 +10.2391 +15.524 + + +2023/12/22 + +750 +10.2390 +15.522 + + +2023/12/22 + +875 +10.2392 +15.525 + + +2023/12/22 + +0 +10.2395 +15.526 + + +2023/12/22 + +125 +10.2396 +15.523 + + +2023/12/22 + +250 +10.2394 +15.524 + + +2023/12/22 + +375 +10.2394 +15.526 + + +2023/12/22 + +500 +10.2394 +15.524 + + +2023/12/22 + +625 +10.2394 +15.524 + + +2023/12/22 + +750 +10.2394 +15.525 + + +2023/12/22 + +875 +10.2394 +15.527 + + +2023/12/22 + +0 +10.2397 +15.525 + + +2023/12/22 + +125 +10.2391 +15.526 + + +2023/12/22 + +250 +10.2391 +15.527 + + +2023/12/22 + +375 +10.2390 +15.526 + + +2023/12/22 + +500 +10.2389 +15.526 + + +2023/12/22 + +625 +10.2389 +15.528 + + +2023/12/22 + +750 +10.2390 +15.527 + + +2023/12/22 + +875 +10.2390 +15.529 + + +2023/12/22 + +0 +10.2387 +15.528 + + +2023/12/22 + +125 +10.2389 +15.530 + + +2023/12/22 + +250 +10.2389 +15.530 + + +2023/12/22 + +375 +10.2389 +15.528 + + +2023/12/22 + +500 +10.2388 +15.530 + + +2023/12/22 + +625 +10.2389 +15.529 + + +2023/12/22 + +750 +10.2386 +15.531 + + +2023/12/22 + +875 +10.2386 +15.532 + + +2023/12/22 + +0 +10.2385 +15.532 + + +2023/12/22 + +125 +10.2392 +15.532 + + +2023/12/22 + +250 +10.2392 +15.532 + + +2023/12/22 + +375 +10.2392 +15.532 + + +2023/12/22 + +500 +10.2391 +15.534 + + +2023/12/22 + +625 +10.2391 +15.531 + + +2023/12/22 + +750 +10.2391 +15.533 + + +2023/12/22 + +875 +10.2395 +15.532 + + +2023/12/22 + +0 +10.2395 +15.534 + + +2023/12/22 + +125 +10.2395 +15.533 + + +2023/12/22 + +250 +10.2395 +15.535 + + +2023/12/22 + +375 +10.2395 +15.536 + + +2023/12/22 + +500 +10.2394 +15.536 + + +2023/12/22 + +625 +10.2395 +15.535 + + +2023/12/22 + +750 +10.2397 +15.536 + + +2023/12/22 + +875 +10.2398 +15.535 + + +2023/12/22 + +0 +10.2396 +15.537 + + +2023/12/22 + +125 +10.2392 +15.537 + + +2023/12/22 + +250 +10.2388 +15.536 + + +2023/12/22 + +375 +10.2393 +15.537 + + +2023/12/22 + +500 +10.2395 +15.537 + + +2023/12/22 + +625 +10.2395 +15.536 + + +2023/12/22 + +750 +10.2401 +15.538 + + +2023/12/22 + +875 +10.2403 +15.538 + + +2023/12/22 + +0 +10.2405 +15.538 + + +2023/12/22 + +125 +10.2400 +15.539 + + +2023/12/22 + +250 +10.2403 +15.540 + + +2023/12/22 + +375 +10.2402 +15.539 + + +2023/12/22 + +500 +10.2402 +15.540 + + +2023/12/22 + +625 +10.2400 +15.540 + + +2023/12/22 + +750 +10.2399 +15.541 + + +2023/12/22 + +875 +10.2401 +15.540 + + +2023/12/22 + +0 +10.2398 +15.541 + + +2023/12/22 + +125 +10.2401 +15.541 + + +2023/12/22 + +250 +10.2404 +15.542 + + +2023/12/22 + +375 +10.2401 +15.541 + + +2023/12/22 + +500 +10.2403 +15.542 + + +2023/12/22 + +625 +10.2399 +15.542 + + +2023/12/22 + +750 +10.2401 +15.542 + + +2023/12/22 + +875 +10.2400 +15.542 + + +2023/12/22 + +0 +10.2400 +15.544 + + +2023/12/22 + +125 +10.2400 +15.544 + + +2023/12/22 + +250 +10.2397 +15.544 + + +2023/12/22 + +375 +10.2399 +15.546 + + +2023/12/22 + +500 +10.2399 +15.545 + + +2023/12/22 + +625 +10.2398 +15.545 + + +2023/12/22 + +750 +10.2398 +15.546 + + +2023/12/22 + +875 +10.2397 +15.546 + + +2023/12/22 + +0 +10.2395 +15.546 + + +2023/12/22 + +125 +10.2396 +15.545 + + +2023/12/22 + +250 +10.2397 +15.547 + + +2023/12/22 + +375 +10.2397 +15.546 + + +2023/12/22 + +500 +10.2396 +15.548 + + +2023/12/22 + +625 +10.2397 +15.549 + + +2023/12/22 + +750 +10.2397 +15.547 + + +2023/12/22 + +875 +10.2397 +15.549 + + +2023/12/22 + +0 +10.2395 +15.548 + + +2023/12/22 + +125 +10.2395 +15.549 + + +2023/12/22 + +250 +10.2395 +15.548 + + +2023/12/22 + +375 +10.2398 +15.550 + + +2023/12/22 + +500 +10.2397 +15.550 + + +2023/12/22 + +625 +10.2397 +15.550 + + +2023/12/22 + +750 +10.2398 +15.551 + + +2023/12/22 + +875 +10.2396 +15.549 + + +2023/12/22 + +0 +10.2398 +15.552 + + +2023/12/22 + +125 +10.2398 +15.552 + + +2023/12/22 + +250 +10.2396 +15.552 + + +2023/12/22 + +375 +10.2396 +15.552 + + +2023/12/22 + +500 +10.2397 +15.552 + + +2023/12/22 + +625 +10.2397 +15.552 + + +2023/12/22 + +750 +10.2398 +15.552 + + +2023/12/22 + +875 +10.2397 +15.552 + + +2023/12/22 + +0 +10.2398 +15.553 + + +2023/12/22 + +125 +10.2399 +15.553 + + +2023/12/22 + +250 +10.2397 +15.554 + + +2023/12/22 + +375 +10.2397 +15.554 + + +2023/12/22 + +500 +10.2398 +15.555 + + +2023/12/22 + +625 +10.2398 +15.553 + + +2023/12/22 + +750 +10.2396 +15.556 + + +2023/12/22 + +875 +10.2396 +15.555 + + +2023/12/22 + +0 +10.2396 +15.554 + + +2023/12/22 + +125 +10.2398 +15.555 + + +2023/12/22 + +250 +10.2398 +15.556 + + +2023/12/22 + +375 +10.2396 +15.556 + + +2023/12/22 + +500 +10.2395 +15.554 + + +2023/12/22 + +625 +10.2402 +15.559 + + +2023/12/22 + +750 +10.2403 +15.559 + + +2023/12/22 + +875 +10.2400 +15.558 + + +2023/12/22 + +0 +10.2400 +15.557 + + +2023/12/22 + +125 +10.2398 +15.557 + + +2023/12/22 + +250 +10.2398 +15.557 + + +2023/12/22 + +375 +10.2399 +15.558 + + +2023/12/22 + +500 +10.2399 +15.558 + + +2023/12/22 + +625 +10.2399 +15.558 + + +2023/12/22 + +750 +10.2396 +15.560 + + +2023/12/22 + +875 +10.2398 +15.560 + + +2023/12/22 + +0 +10.2397 +15.559 + + +2023/12/22 + +125 +10.2399 +15.559 + + +2023/12/22 + +250 +10.2399 +15.561 + + +2023/12/22 + +375 +10.2398 +15.561 + + +2023/12/22 + +500 +10.2399 +15.562 + + +2023/12/22 + +625 +10.2398 +15.562 + + +2023/12/22 + +750 +10.2397 +15.561 + + +2023/12/22 + +875 +10.2398 +15.562 + + +2023/12/22 + +0 +10.2397 +15.563 + + +2023/12/22 + +125 +10.2399 +15.562 + + +2023/12/22 + +250 +10.2399 +15.563 + + +2023/12/22 + +375 +10.2399 +15.562 + + +2023/12/22 + +500 +10.2400 +15.562 + + +2023/12/22 + +625 +10.2397 +15.564 + + +2023/12/22 + +750 +10.2399 +15.563 + + +2023/12/22 + +875 +10.2399 +15.565 + + +2023/12/22 + +0 +10.2399 +15.562 + + +2023/12/22 + +125 +10.2400 +15.562 + + +2023/12/22 + +250 +10.2398 +15.565 + + +2023/12/22 + +375 +10.2398 +15.564 + + +2023/12/22 + +500 +10.2398 +15.564 + + +2023/12/22 + +625 +10.2398 +15.565 + + +2023/12/22 + +750 +10.2399 +15.565 + + +2023/12/22 + +875 +10.2400 +15.566 + + +2023/12/22 + +0 +10.2399 +15.565 + + +2023/12/22 + +125 +10.2399 +15.564 + + +2023/12/22 + +250 +10.2399 +15.564 + + +2023/12/22 + +375 +10.2397 +15.565 + + +2023/12/22 + +500 +10.2397 +15.566 + + +2023/12/22 + +625 +10.2397 +15.562 + + +2023/12/22 + +750 +10.2397 +15.566 + + +2023/12/22 + +875 +10.2398 +15.567 + + +2023/12/22 + +0 +10.2398 +15.566 + + +2023/12/22 + +125 +10.2399 +15.568 + + +2023/12/22 + +250 +10.2397 +15.567 + + +2023/12/22 + +375 +10.2396 +15.567 + + +2023/12/22 + +500 +10.2397 +15.568 + + +2023/12/22 + +625 +10.2398 +15.567 + + +2023/12/22 + +750 +10.2394 +15.567 + + +2023/12/22 + +875 +10.2396 +15.569 + + +2023/12/22 + +0 +10.2395 +15.568 + + +2023/12/22 + +125 +10.2395 +15.569 + + +2023/12/22 + +250 +10.2397 +15.567 + + +2023/12/22 + +375 +10.2396 +15.565 + + +2023/12/22 + +500 +10.2396 +15.568 + + +2023/12/22 + +625 +10.2395 +15.566 + + +2023/12/22 + +750 +10.2395 +15.570 + + +2023/12/22 + +875 +10.2395 +15.568 + + +2023/12/22 + +0 +10.2395 +15.568 + + +2023/12/22 + +125 +10.2396 +15.569 + + +2023/12/22 + +250 +10.2395 +15.568 + + +2023/12/22 + +375 +10.2395 +15.568 + + +2023/12/22 + +500 +10.2394 +15.570 + + +2023/12/22 + +625 +10.2395 +15.569 + + +2023/12/22 + +750 +10.2391 +15.572 + + +2023/12/22 + +875 +10.2394 +15.570 + + +2023/12/22 + +0 +10.2395 +15.568 + + +2023/12/22 + +125 +10.2394 +15.569 + + +2023/12/22 + +250 +10.2393 +15.571 + + +2023/12/22 + +375 +10.2393 +15.569 + + +2023/12/22 + +500 +10.2395 +15.569 + + +2023/12/22 + +625 +10.2397 +15.570 + + +2023/12/22 + +750 +10.2394 +15.569 + + +2023/12/22 + +875 +10.2396 +15.571 + + +2023/12/22 + +0 +10.2396 +15.571 + + +2023/12/22 + +125 +10.2396 +15.571 + + +2023/12/22 + +250 +10.2398 +15.573 + + +2023/12/22 + +375 +10.2395 +15.571 + + +2023/12/22 + +500 +10.2395 +15.571 + + +2023/12/22 + +625 +10.2394 +15.570 + + +2023/12/22 + +750 +10.2393 +15.571 + + +2023/12/22 + +875 +10.2393 +15.571 + + +2023/12/22 + +0 +10.2396 +15.571 + + +2023/12/22 + +125 +10.2395 +15.571 + + +2023/12/22 + +250 +10.2395 +15.571 + + +2023/12/22 + +375 +10.2394 +15.571 + + +2023/12/22 + +500 +10.2394 +15.572 + + +2023/12/22 + +625 +10.2396 +15.573 + + +2023/12/22 + +750 +10.2395 +15.573 + + +2023/12/22 + +875 +10.2395 +15.572 + + +2023/12/22 + +0 +10.2396 +15.572 + + +2023/12/22 + +125 +10.2396 +15.573 + + +2023/12/22 + +250 +10.2395 +15.572 + + +2023/12/22 + +375 +10.2397 +15.573 + + +2023/12/22 + +500 +10.2400 +15.571 + + +2023/12/22 + +625 +10.2400 +15.574 + + +2023/12/22 + +750 +10.2398 +15.571 + + +2023/12/22 + +875 +10.2397 +15.574 + + +2023/12/22 + +0 +10.2400 +15.573 + + +2023/12/22 + +125 +10.2396 +15.573 + + +2023/12/22 + +250 +10.2398 +15.573 + + +2023/12/22 + +375 +10.2398 +15.574 + + +2023/12/22 + +500 +10.2397 +15.574 + + +2023/12/22 + +625 +10.2397 +15.574 + + +2023/12/22 + +750 +10.2398 +15.574 + + +2023/12/22 + +875 +10.2397 +15.574 + + +2023/12/22 + +0 +10.2397 +15.573 + + +2023/12/22 + +125 +10.2398 +15.575 + + +2023/12/22 + +250 +10.2396 +15.573 + + +2023/12/22 + +375 +10.2397 +15.573 + + +2023/12/22 + +500 +10.2397 +15.575 + + +2023/12/22 + +625 +10.2399 +15.574 + + +2023/12/22 + +750 +10.2397 +15.573 + + +2023/12/22 + +875 +10.2397 +15.575 + + +2023/12/22 + +0 +10.2397 +15.575 + + +2023/12/22 + +125 +10.2396 +15.576 + + +2023/12/22 + +250 +10.2396 +15.576 + + +2023/12/22 + +375 +10.2400 +15.576 + + +2023/12/22 + +500 +10.2398 +15.576 + + +2023/12/22 + +625 +10.2398 +15.576 + + +2023/12/22 + +750 +10.2400 +15.576 + + +2023/12/22 + +875 +10.2400 +15.577 + + +2023/12/22 + +0 +10.2401 +15.576 + + +2023/12/22 + +125 +10.2400 +15.575 + + +2023/12/22 + +250 +10.2402 +15.575 + + +2023/12/22 + +375 +10.2402 +15.576 + + +2023/12/22 + +500 +10.2401 +15.577 + + +2023/12/22 + +625 +10.2401 +15.577 + + +2023/12/22 + +750 +10.2401 +15.577 + + +2023/12/22 + +875 +10.2402 +15.576 + + +2023/12/22 + +0 +10.2400 +15.577 + + +2023/12/22 + +125 +10.2401 +15.577 + + +2023/12/22 + +250 +10.2400 +15.576 + + +2023/12/22 + +375 +10.2398 +15.577 + + +2023/12/22 + +500 +10.2399 +15.577 + + +2023/12/22 + +625 +10.2400 +15.576 + + +2023/12/22 + +750 +10.2397 +15.578 + + +2023/12/22 + +875 +10.2394 +15.578 + + +2023/12/22 + +0 +10.2392 +15.578 + + +2023/12/22 + +125 +10.2394 +15.577 + + +2023/12/22 + +250 +10.2395 +15.578 + + +2023/12/22 + +375 +10.2393 +15.577 + + +2023/12/22 + +500 +10.2397 +15.578 + + +2023/12/22 + +625 +10.2400 +15.576 + + +2023/12/22 + +750 +10.2402 +15.579 + + +2023/12/22 + +875 +10.2400 +15.577 + + +2023/12/22 + +0 +10.2401 +15.576 + + +2023/12/22 + +125 +10.2400 +15.578 + + +2023/12/22 + +250 +10.2400 +15.578 + + +2023/12/22 + +375 +10.2397 +15.578 + + +2023/12/22 + +500 +10.2394 +15.579 + + +2023/12/22 + +625 +10.2394 +15.578 + + +2023/12/22 + +750 +10.2396 +15.581 + + +2023/12/22 + +875 +10.2398 +15.578 + + +2023/12/22 + +0 +10.2399 +15.578 + + +2023/12/22 + +125 +10.2398 +15.577 + + +2023/12/22 + +250 +10.2400 +15.580 + + +2023/12/22 + +375 +10.2401 +15.579 + + +2023/12/22 + +500 +10.2402 +15.579 + + +2023/12/22 + +625 +10.2401 +15.578 + + +2023/12/22 + +750 +10.2400 +15.577 + + +2023/12/22 + +875 +10.2399 +15.579 + + +2023/12/22 + +0 +10.2397 +15.579 + + +2023/12/22 + +125 +10.2396 +15.579 + + +2023/12/22 + +250 +10.2398 +15.581 + + +2023/12/22 + +375 +10.2397 +15.581 + + +2023/12/22 + +500 +10.2396 +15.578 + + +2023/12/22 + +625 +10.2397 +15.578 + + +2023/12/22 + +750 +10.2397 +15.579 + + +2023/12/22 + +875 +10.2396 +15.580 + + +2023/12/22 + +0 +10.2395 +15.579 + + +2023/12/22 + +125 +10.2399 +15.580 + + +2023/12/22 + +250 +10.2399 +15.581 + + +2023/12/22 + +375 +10.2400 +15.580 + + +2023/12/22 + +500 +10.2399 +15.581 + + +2023/12/22 + +625 +10.2401 +15.580 + + +2023/12/22 + +750 +10.2402 +15.580 + + +2023/12/22 + +875 +10.2404 +15.581 + + +2023/12/22 + +0 +10.2407 +15.581 + + +2023/12/22 + +125 +10.2405 +15.581 + + +2023/12/22 + +250 +10.2404 +15.581 + + +2023/12/22 + +375 +10.2404 +15.581 + + +2023/12/22 + +500 +10.2406 +15.580 + + +2023/12/22 + +625 +10.2410 +15.580 + + +2023/12/22 + +750 +10.2409 +15.579 + + +2023/12/22 + +875 +10.2409 +15.579 + + +2023/12/22 + +0 +10.2408 +15.580 + + +2023/12/22 + +125 +10.2414 +15.581 + + +2023/12/22 + +250 +10.2415 +15.579 + + +2023/12/22 + +375 +10.2415 +15.580 + + +2023/12/22 + +500 +10.2417 +15.579 + + +2023/12/22 + +625 +10.2418 +15.580 + + +2023/12/22 + +750 +10.2414 +15.579 + + +2023/12/22 + +875 +10.2415 +15.580 + + +2023/12/22 + +0 +10.2418 +15.579 + + +2023/12/22 + +125 +10.2416 +15.580 + + +2023/12/22 + +250 +10.2415 +15.580 + + +2023/12/22 + +375 +10.2418 +15.581 + + +2023/12/22 + +500 +10.2420 +15.580 + + +2023/12/22 + +625 +10.2419 +15.581 + + +2023/12/22 + +750 +10.2420 +15.580 + + +2023/12/22 + +875 +10.2417 +15.580 + + +2023/12/22 + +0 +10.2416 +15.581 + + +2023/12/22 + +125 +10.2413 +15.577 + + +2023/12/22 + +250 +10.2415 +15.582 + + +2023/12/22 + +375 +10.2415 +15.580 + + +2023/12/22 + +500 +10.2413 +15.580 + + +2023/12/22 + +625 +10.2414 +15.578 + + +2023/12/22 + +750 +10.2412 +15.581 + + +2023/12/22 + +875 +10.2411 +15.578 + + +2023/12/22 + +0 +10.2408 +15.580 + + +2023/12/22 + +125 +10.2407 +15.579 + + +2023/12/22 + +250 +10.2404 +15.578 + + +2023/12/22 + +375 +10.2404 +15.578 + + +2023/12/22 + +500 +10.2401 +15.578 + + +2023/12/22 + +625 +10.2402 +15.579 + + +2023/12/22 + +750 +10.2400 +15.578 + + +2023/12/22 + +875 +10.2402 +15.579 + + +2023/12/22 + +0 +10.2407 +15.581 + + +2023/12/22 + +125 +10.2448 +15.579 + + +2023/12/22 + +250 +10.2389 +15.579 + + +2023/12/22 + +375 +10.2394 +15.579 + + +2023/12/22 + +500 +10.2397 +15.578 + + +2023/12/22 + +625 +10.2398 +15.580 + + +2023/12/22 + +750 +10.2399 +15.578 + + +2023/12/22 + +875 +10.2397 +15.579 + + +2023/12/22 + +0 +10.2398 +15.578 + + +2023/12/22 + +125 +10.2398 +15.580 + + +2023/12/22 + +250 +10.2397 +15.578 + + +2023/12/22 + +375 +10.2396 +15.578 + + +2023/12/22 + +500 +10.2397 +15.579 + + +2023/12/22 + +625 +10.2396 +15.576 + + +2023/12/22 + +750 +10.2394 +15.579 + + +2023/12/22 + +875 +10.2395 +15.577 + + +2023/12/22 + +0 +10.2396 +15.579 + + +2023/12/22 + +125 +10.2394 +15.577 + + +2023/12/22 + +250 +10.2394 +15.576 + + +2023/12/22 + +375 +10.2395 +15.577 + + +2023/12/22 + +500 +10.2394 +15.578 + + +2023/12/22 + +625 +10.2392 +15.577 + + +2023/12/22 + +750 +10.2392 +15.578 + + +2023/12/22 + +875 +10.2390 +15.578 + + +2023/12/22 + +0 +10.2394 +15.578 + + +2023/12/22 + +125 +10.2392 +15.578 + + +2023/12/22 + +250 +10.2391 +15.578 + + +2023/12/22 + +375 +10.2391 +15.575 + + +2023/12/22 + +500 +10.2392 +15.579 + + +2023/12/22 + +625 +10.2392 +15.577 + + +2023/12/22 + +750 +10.2390 +15.577 + + +2023/12/22 + +875 +10.2390 +15.576 + + +2023/12/22 + +0 +10.2390 +15.575 + + +2023/12/22 + +125 +10.2391 +15.575 + + +2023/12/22 + +250 +10.2392 +15.577 + + +2023/12/22 + +375 +10.2393 +15.575 + + +2023/12/22 + +500 +10.2391 +15.578 + + +2023/12/22 + +625 +10.2393 +15.577 + + +2023/12/22 + +750 +10.2391 +15.576 + + +2023/12/22 + +875 +10.2392 +15.577 + + +2023/12/22 + +0 +10.2392 +15.577 + + +2023/12/22 + +125 +10.2391 +15.576 + + +2023/12/22 + +250 +10.2392 +15.576 + + +2023/12/22 + +375 +10.2391 +15.576 + + +2023/12/22 + +500 +10.2391 +15.577 + + +2023/12/22 + +625 +10.2391 +15.575 + + +2023/12/22 + +750 +10.2391 +15.576 + + +2023/12/22 + +875 +10.2392 +15.577 + + +2023/12/22 + +0 +10.2389 +15.578 + + +2023/12/22 + +125 +10.2387 +15.576 + + +2023/12/22 + +250 +10.2390 +15.578 + + + diff --git a/tests/data/2024-solinst-test/example-10min-interval-via-laptop.zip b/tests/data/2024-solinst-test/example-10min-interval-via-laptop.zip new file mode 100644 index 00000000..0788fb49 Binary files /dev/null and b/tests/data/2024-solinst-test/example-10min-interval-via-laptop.zip differ diff --git a/tests/test_002_obs_objects.py b/tests/test_002_obs_objects.py index ca972f66..5b48ace3 100644 --- a/tests/test_002_obs_objects.py +++ b/tests/test_002_obs_objects.py @@ -1,19 +1,11 @@ import numpy as np import pandas as pd +import pytest import hydropandas as hpd -# import sys -# sys.path.insert(1, "..") - -# TEST_DIR = os.path.dirname(os.path.abspath(__file__)) -# PROJECT_DIR = os.path.abspath(os.path.join(TEST_DIR, os.pardir)) -# sys.path.insert(0, PROJECT_DIR) -# os.chdir(TEST_DIR) - - -def test_groundwater_obs(name="groundwaterobs_001", tube_nr=2): +def _get_groundwater_obs(name="groundwaterobs_001", tube_nr=2): df = pd.DataFrame( index=pd.date_range("2020-1-1", "2020-1-10"), data={"values": np.random.rand(10)}, @@ -41,7 +33,7 @@ def test_groundwater_obs(name="groundwaterobs_001", tube_nr=2): return o -def test_waterlvl_obs(): +def _get_waterlvl_obs(): df = pd.DataFrame( index=pd.date_range("2020-1-1", "2020-1-10"), data={"values": np.random.rand(10)}, @@ -60,6 +52,16 @@ def test_waterlvl_obs(): return o +def _obscollection_from_list(): + o_list = [] + for i in range(10): + o_list.append(_get_groundwater_obs(name=f"groundwaterobs_00{i}", tube_nr=i)) + + oc = hpd.ObsCollection.from_list(o_list) + + return oc + + def test_groundwater_quality_obs(): df = pd.DataFrame( index=pd.date_range("2020-1-1", "2020-1-10"), data={"pH": np.random.rand(10)} @@ -75,25 +77,15 @@ def test_groundwater_quality_obs(): ) -def test_obscollection_from_list(): - o_list = [] - for i in range(10): - o_list.append(test_groundwater_obs(name=f"groundwaterobs_00{i}", tube_nr=i)) - - oc = hpd.ObsCollection.from_list(o_list) - - return oc - - def test_add_meta_to_df(): - oc = test_obscollection_from_list() + oc = _obscollection_from_list() oc.add_meta_to_df(key="all") assert "info" in oc.columns, "unexpected result for add_meta_to_df" def test_copy_obs(): - o = test_groundwater_obs(name="groundwaterobs_001", tube_nr=2) + o = _get_groundwater_obs(name="groundwaterobs_001", tube_nr=2) o2 = o.copy() o.meta["hello"] = "world" @@ -133,10 +125,10 @@ def test_convert_waterlvl_groundwater_obs(): def test_merge_observations_same_timeseries(): # base - o = test_groundwater_obs(name="groundwaterobs_010", tube_nr=10) + o = _get_groundwater_obs(name="groundwaterobs_010", tube_nr=10) # observation with different metadata, same time series - o2 = test_groundwater_obs(name="groundwaterobs_010", tube_nr=10) + o2 = _get_groundwater_obs(name="groundwaterobs_010", tube_nr=10) o2.iloc[:, 0] = o.iloc[:, 0] omerged = o.merge_observation(o2, merge_metadata=False) @@ -146,7 +138,7 @@ def test_merge_observations_same_timeseries(): def test_merge_observations_different_timeseries(): # base - o = test_groundwater_obs(name="groundwaterobs_010", tube_nr=10) + o = _get_groundwater_obs(name="groundwaterobs_010", tube_nr=10) # observation with different time series o2 = o.copy() @@ -163,7 +155,7 @@ def test_merge_observations_different_timeseries(): def test_merge_overlapping(): # base - o = test_groundwater_obs(name="groundwaterobs_010", tube_nr=10) + o = _get_groundwater_obs(name="groundwaterobs_010", tube_nr=10) # observation with partially overlapping time series and extra columns o2 = o.copy() @@ -182,29 +174,47 @@ def test_merge_overlapping(): def test_merge_errors(): # base - o = test_groundwater_obs(name="groundwaterobs_010", tube_nr=10) + o = _get_groundwater_obs(name="groundwaterobs_010", tube_nr=10) # observation with partially overlapping time series and extra columns - o2 = test_waterlvl_obs() + o2 = _get_waterlvl_obs() - try: + with pytest.raises(TypeError): o.merge_observation(o2) - except TypeError: - return - - raise RuntimeError("function should raise an error") def test_add_observation_to_oc(): - oc = test_obscollection_from_list() + oc = _obscollection_from_list() - o = test_groundwater_obs(name="groundwaterobs_010", tube_nr=10) + o = _get_groundwater_obs(name="groundwaterobs_010", tube_nr=10) oc.add_observation(o) def test_interpolate_obscollection(): - oc = test_obscollection_from_list() + oc = _obscollection_from_list() xy = [[500, 11000], [9000, 18000]] oc.interpolate(xy) + + +def test_get_obs(): + oc = _obscollection_from_list() + + # by name + o = oc.get_obs(name="groundwaterobs_001") + assert isinstance(o, hpd.GroundwaterObs) + assert o.name == "groundwaterobs_001" + + # by attributes + o = oc.get_obs(monitoring_well="groundwaterobs", tube_nr=2) + assert isinstance(o, hpd.GroundwaterObs) + assert o.tube_nr == 2 + + # multiple observations + with pytest.raises(ValueError): + oc.get_obs(monitoring_well="groundwaterobs") + + # no observations + with pytest.raises(ValueError): + oc.get_obs(monitoring_well="I do not exist") diff --git a/tests/test_013_lizard.py b/tests/test_013_lizard.py index 299666b8..d7d65e2e 100644 --- a/tests/test_013_lizard.py +++ b/tests/test_013_lizard.py @@ -14,5 +14,24 @@ def test_extent(): def test_codes(): - oc = hpd.read_lizard(codes="27BP0003") + oc = hpd.read_lizard( + codes=["39F-0735", "39F-0736", "39F-0737"], type_timeseries="merge" + ) assert not oc.empty + + +def test_many_tubed_well(): + + oc = hpd.read_lizard(codes="EEWP004", tube_nr="all") + assert not oc.empty + + +def test_complex_well(): + + oc = hpd.read_lizard(codes="BUWP014", tube_nr="all") + assert not oc.empty + + +def test_combine(): + + hpd.GroundwaterObs.from_lizard("39F-0736", tube_nr=1, type_timeseries="combine") diff --git a/tests/test_014_solinst.py b/tests/test_014_solinst.py new file mode 100644 index 00000000..43ea2526 --- /dev/null +++ b/tests/test_014_solinst.py @@ -0,0 +1,57 @@ +# %% +import logging + +from hydropandas import observation +from hydropandas.io import solinst + +logging.basicConfig(level=logging.DEBUG) + + +# %% test observations + + +def test_read_solinst_file_obs(): + # observation of slugtest, 8 observations per second + df, _ = solinst.read_solinst_file( + "./tests/data/2024-solinst-test/WsNoo_dp366_BUB_20231222_slug1m.xle", + ) + + assert len(df) == 587, "Dataframe should have 587 readings" + + +def test_read_solinst_file_meta_has_location(): + # observation of slugtest, created via iOs app, location included by app + _, meta = solinst.read_solinst_file( + "./tests/data/2024-solinst-test/WsNoo_dp366_BUB_20231222_slug1m.xle", + ) + + assert meta["x"] == 52730.58, "x coordinate should be 52730.58" + + +def test_read_solinst_file_meta_without_location(): + # example observation created via desktop, location not included + _, meta = solinst.read_solinst_file( + "./tests/data/2024-solinst-test/example-10min-interval-via-laptop.zip", + ) + + assert meta["x"] is None, "x coordinate not available" + + +def test_read_solinst_file_with_manual_meta(): + # manual metadata about levels provided + screen_bottom = -10 + screen_top = -5 + ground_level = -1 + tube_nr = 1 + tube_top = -0.5 + + oc = observation.GroundwaterObs.from_solinst( + "./tests/data/2024-solinst-test/WsNoo_dp366_BUB_20231222_slug1m.xle", + screen_bottom=screen_bottom, + screen_top=screen_top, + ground_level=ground_level, + tube_nr=tube_nr, + tube_top=tube_top, + ) + + assert oc.tube_top == tube_top, "tube_top should be in oc"