diff --git a/.gitignore b/.gitignore index ee11d40..97e5249 100644 --- a/.gitignore +++ b/.gitignore @@ -109,3 +109,7 @@ venv.bak/ *.hdf5 *.nc *.tif + +*.log +notebooks/logs +notebooks/results diff --git a/environment.yml b/environment.yml index e69de29..555ab7f 100644 --- a/environment.yml +++ b/environment.yml @@ -0,0 +1,23 @@ +name: h5cloud +channels: + - conda-forge +dependencies: + - jupyterlab + - boto3 + - tqdm + - matplotlib-base + - pandas + - numpy + - s3fs + - xarray + - dask + - distributed + - geopandas + - h5py>=3.10 + - zarr + - kerchunk + - h5netcdf + - pip + - pip: + - git+https://github.com/betolink/filesystem_spec.git + - git+https://github.com/ICESat2-SlideRule/h5coro.git diff --git a/h5tests/h5coro_arr_mean.py b/h5tests/h5coro_arr_mean.py index 58b3f76..6969c4a 100644 --- a/h5tests/h5coro_arr_mean.py +++ b/h5tests/h5coro_arr_mean.py @@ -1,27 +1,40 @@ -from .h5test import H5Test, timer_decorator -import numpy as np import subprocess +import numpy as np +from h5test import H5Test, timer_decorator + try: import h5coro except: - completed_process = subprocess.run([ - 'mamba', 'install', '-c', 'conda-forge', 'h5coro', '--yes' - ]) + completed_process = subprocess.run( + ["pip", "install", "git+https://github.com/ICESat2-SlideRule/h5coro.git@main"] + ) import h5coro -from h5coro import h5coro, s3driver, filedriver -h5coro.config(errorChecking=True, verbose=False, enableAttributes=False) - +from h5coro import h5coro, s3driver + +driver = s3driver.S3Driver + + class H5CoroArrMean(H5Test): @timer_decorator - def run(self): - group = '/gt1l/heights' - variable = 'h_ph' + def run(self, dataset="/gt1l/heights", variable="h_ph"): + group = dataset + variable = variable final_h5coro_array = [] + for file in self.files: - h5obj = h5coro.H5Coro(file.replace("s3://", ""), s3driver.S3Driver) - output = h5obj.readDatasets(datasets=[f'{group}/{variable}'], block=True) - data = h5obj[f'{group}/{variable}'].values - final_h5coro_array = np.insert(final_h5coro_array, len(final_h5coro_array), data, axis=None) + if link.startswith("s3://nasa-cryo-persistent/"): + h5obj = h5coro.H5Coro(link.replace("s3://", ""), s3driver.S3Driver) + else: + h5obj = h5coro.H5Coro( + link.replace("s3://", ""), + s3driver.S3Driver, + credentials={"annon": True}, + ) + ds = h5obj.readDatasets(datasets=[f"{group}/{variable}"], block=True) + data = ds[f"{group}/{variable}"][:] + final_h5coro_array = np.insert( + final_h5coro_array, len(final_h5coro_array), data, axis=None + ) return np.mean(final_h5coro_array) diff --git a/h5tests/h5py_arr_mean.py b/h5tests/h5py_arr_mean.py index 7c35407..8e059cf 100644 --- a/h5tests/h5py_arr_mean.py +++ b/h5tests/h5py_arr_mean.py @@ -1,21 +1,26 @@ -from .h5test import H5Test, timer_decorator import h5py import numpy as np +from h5test import H5Test, fsspec_logging_decorator, timer_decorator + class H5pyArrMean(H5Test): @timer_decorator - def run(self): - final_h5py_array = [] - # TODO: Do we need to make this configurable or consistent? - group = '/gt1l/heights' - variable = 'h_ph' + @fsspec_logging_decorator + def run(self, io_params={}, dataset="/gt1l/heights", variable="h_ph"): + final_h5py_array = [] + fsspec_params = {} + h5py_params = {} + if "fsspec_params" in io_params: + fsspec_params = io_params["fsspec_params"] + if "h5py_params" in io_params: + h5py_params = io_params["h5py_params"] + self.file_sizes = [self.s3_fs.info(file)["size"] for file in self.files] for file in self.files: - with h5py.File(self.s3_fs.open(file, 'rb')) as f: - data = f[f'{group}/{variable}'][:] - # Need to test if using concatenate is faster - final_h5py_array = np.insert( - final_h5py_array, - len(final_h5py_array), - data, axis=None - ) + with self.s3_fs.open(file, mode="rb", **fsspec_params) as fo: + print("h5py params: ", h5py_params) + with h5py.File(fo, **h5py_params) as f: + data = f[f"{dataset}/{variable}"][:] + final_h5py_array = np.insert( + final_h5py_array, len(final_h5py_array), data, axis=None + ) return np.mean(final_h5py_array) diff --git a/h5tests/h5py_arr_subset_mean.py b/h5tests/h5py_arr_subset_mean.py index e8ceeea..f2c2629 100644 --- a/h5tests/h5py_arr_subset_mean.py +++ b/h5tests/h5py_arr_subset_mean.py @@ -1,16 +1,16 @@ import os import sys -from .h5test import H5Test, timer_decorator import h5py import numpy as np +from h5test import H5Test, fsspec_logging_decorator, timer_decorator -current = os.path.abspath('..') +current = os.path.abspath("..") sys.path.append(current) -from helpers.geospatial import get_subset_region, get_subset_indices +from helpers.geospatial import get_subset_indices, get_subset_region + class H5pyArrSubsetMean(H5Test): - def __init__(self, data_format, geometry=None): """ geometry : path to geojson file containing geometry @@ -18,32 +18,34 @@ def __init__(self, data_format, geometry=None): """ super().__init__(data_format) self.bounds = get_subset_region(geometry) - + @timer_decorator - def run(self): - final_h5py_array = [] + @fsspec_logging_decorator + def run(self, io_params={}, dataset="/gt1l/heights", variable="h_ph"): + final_h5py_array = [] # TODO: Do we need to make this configurable or consistent? - group = '/gt1l/heights' - variable = 'h_ph' + if "fsspec_params" in io_params: + fsspec_params = io_params["fsspec_params"] + if "h5py_params" in io_params: + h5py_params = io_params["h5py_params"] for file in self.files: - with h5py.File(self.s3_fs.open(file, 'rb')) as f: - - lat = f[f'{group}/lat_ph'][:] - lon = f[f'{group}/lon_ph'][:] - + with h5py.File( + self.s3_fs.open(file, "rb", **fsspec_params), **h5py_params + ) as f: + lat = f[f"{dataset}/lat_ph"][:] + lon = f[f"{dataset}/lon_ph"][:] + idx_start, idx_end = get_subset_indices(lat, lon, self.bounds) - + # Leaving this code here so that we can create a DataFrame or - # Dataset at a later date. Suggest creating dict which can be + # Dataset at a later date. Suggest creating dict which can be # passsed to xarray or (geo)pandas # lat[idx_start:idx_end]) # lon[idx_start:idx_end]) - data = f[f'{group}/{variable}'][idx_start:idx_end] + data = f[f"{dataset}/{variable}"][idx_start:idx_end] # Need to test if using concatenate is faster final_h5py_array = np.insert( - final_h5py_array, - len(final_h5py_array), - data, axis=None + final_h5py_array, len(final_h5py_array), data, axis=None ) - return np.mean(final_h5py_array) \ No newline at end of file + return np.mean(final_h5py_array) diff --git a/h5tests/h5test.py b/h5tests/h5test.py index 74bb4de..b77e81c 100644 --- a/h5tests/h5test.py +++ b/h5tests/h5test.py @@ -1,74 +1,217 @@ -import boto3 import csv -from io import StringIO +import logging +import os +import pathlib +import sys import time from datetime import datetime -import os +from io import StringIO + +import boto3 import s3fs -import sys -current = os.path.abspath('..') +current = os.path.abspath("..") sys.path.append(current) -from helpers.links import S3Links -def generate_timestamp(): - return datetime.now().strftime('%Y-%m-%d-%H%M%S') + +def fsspec_logging_decorator(func): + """ + It will store the fsspec logs inside ./logs and will get some stats from file access + Will pass values to timer_decorator + """ + + def __setup_logging(self): + pathlib.Path(f"./logs").mkdir(exist_ok=True) + logger = logging.getLogger("fsspec") + logger.setLevel(logging.DEBUG) + self._file_handler = logging.FileHandler(self.log_filename) + self._file_handler.setLevel(logging.DEBUG) + logging.getLogger("fsspec").addHandler(self._file_handler) + + def __turnoff_logging(self): + [ + logging.getLogger("fsspec").debug(f"FileSize: {size}") + for size in self.file_sizes + ] + logging.getLogger("fsspec").removeHandler(self._file_handler) + self._file_handler.close() + + def fsspec_stats(log_file): + stats = None + with open(log_file, "r") as input_file: + num_requests = 0 + total_requested_bytes = 0 + for line in input_file: + try: + read_range = line.split("read:")[1].split(" - ") + request_size = int(read_range[1]) - int(read_range[0]) + total_requested_bytes += request_size + num_requests += 1 + except Exception: + pass + if total_requested_bytes > 0: + stats = { + "total_reqs": num_requests, + "total_reqs_bytes": total_requested_bytes, + "avg_req_size": int(round(total_requested_bytes / num_requests, 2)), + } + return stats + + def wrapper(self, *args, **kwargs): + tstamp = datetime.now().strftime("%Y-%m-%d-%H%M%S") + self.log_filename = f"logs/{self.data_format}-{tstamp}.log" + + __setup_logging(self) + result = func(self, *args, **kwargs) + __turnoff_logging(self) + + self.io_stats = fsspec_stats(self.log_filename) + return result, {"logs": self.log_filename, "io_stats": self.io_stats} + + return wrapper + def timer_decorator(func): """ A decorator to measure the execution time of the wrapped function. """ + def wrapper(self, *args, **kwargs): + tstamp = datetime.now().strftime("%Y-%m-%d-%H%M%S") + start_time = time.time() result = func(self, *args, **kwargs) end_time = time.time() execution_time = end_time - start_time + if "io_params" in kwargs: + self.runtime_params = kwargs["io_params"] + if len(args) > 0: + self.runtime_params = args[0] + # Call the store method here if self.store_results: - results_key = f"{generate_timestamp()}_{self.name}_{self.data_format}_results.csv" - s3_key = f"{self.results_directory}/{results_key}" - self.store(run_time=execution_time, result=result, bucket=self.bucket, s3_key=s3_key) - return result, execution_time + if type(result) in [list, dict, tuple]: + # unpack + func_result, _ = result + else: + func_result = result + results_key = f"{tstamp}_{self.name}_{self.data_format}_results.csv" + self.store( + run_time=execution_time, result=func_result, file_name=results_key + ) + return result, {"execution_time": execution_time} + return wrapper + class H5Test: - def __init__(self, data_format: str, files=None, store_results=True): + def __init__( + self, + data_format: str, + files=[], + store_results=True, + ): self.name = self.__class__.__name__ + self.io_stats = None + self.runtime_params = None + self.log_filename = "" self.data_format = data_format - if files: + if len(files) > 0: self.files = files else: - self.files = S3Links().get_links_by_format(data_format) - self.s3_client = boto3.client('s3') # Ensure AWS credentials are configured - self.s3_fs = s3fs.S3FileSystem(anon=False) + raise ValueError("We need at least 1 ATL03 granule URL hosted in S3") + self.store_results = store_results - self.bucket = "nasa-cryo-scratch" - self.results_directory = "h5cloud/benchmark_results" + + if files[0].startswith("s3://nasa-cryo-persistent"): + self.s3_client = boto3.client("s3") # + self.annon_access = False + self.results_bucket = "s3://nasa-cryo-persistent/" + self.results_directory = "h5cloud/benchmark_results" + self.results_store_type = "S3" + else: + self.annon_access = True + self.results_path = "results" + pathlib.Path(f"./{self.results_path}").mkdir(exist_ok=True) + self.results_store_type = "Local" + + self.s3_fs = s3fs.S3FileSystem(anon=self.annon_access) + self.file_sizes = [self.s3_fs.info(file)["size"] for file in self.files] @timer_decorator - def run(self): + def run(self, io_params, dataset, variable): raise NotImplementedError("The run method has not been implemented") - def store(self, run_time: float, result: str, bucket: str, s3_key: str): + def store(self, run_time: float, result: str, file_name: str): """ Store test results to an S3 bucket as a CSV file. - :param run_time: The runtime of the test :param result: The result of the test - :param bucket: The name of the S3 bucket where the CSV will be uploaded - :param s3_key: The S3 key (filename) where the CSV will be stored + :param file_name: file to store the results """ # Create a CSV in-memory csv_buffer = StringIO() csv_writer = csv.writer(csv_buffer) - csv_writer.writerow(['Name', 'Data Format', 'Run Time', 'Result']) # Headers - csv_writer.writerow([self.name, self.data_format, run_time, result]) + if self.io_stats: # if we are using the fsspec logger decorator + csv_writer.writerow( + [ + "Name", + "Data Format", + "Run Time", + "Result", + "Runtime Params", + "Access Log", + "Total Bytes Tranferred", + "Total Requests", + "Average Request Size", + ] + ) # Headers + csv_writer.writerow( + [ + self.name, + self.data_format, + run_time, + result, + self.runtime_params, + self.log_filename, + self.io_stats["total_reqs_bytes"], + self.io_stats["total_reqs"], + self.io_stats["avg_req_size"], + ] + ) + else: + csv_writer.writerow( + [ + "Name", + "Data Format", + "Run Time", + "Result", + ] + ) # Headers + csv_writer.writerow( + [ + self.name, + self.data_format, + run_time, + result, + ] + ) # Reset the buffer's position to the beginning csv_buffer.seek(0) # Upload the CSV to S3 - self.s3_client.put_object(Bucket=bucket, Key=s3_key, Body=csv_buffer.getvalue()) + if self.results_store_type == "S3": + # assumes s3 can write to bucket + self.s3_client.put_object( + Bucket=self.results_bucket, + Key=f"{self.results_directory}/{file_name}", + Body=csv_buffer.getvalue(), + ) + else: + with open(f"{self.results_path}/{file_name}", "w", newline="") as csv_file: + csv_file.write(csv_buffer.getvalue()) + ## Example subclass # class SampleTest(H5Test): diff --git a/h5tests/single-test.ipynb b/h5tests/single-test.ipynb index 1ea5439..cd7a3e0 100644 --- a/h5tests/single-test.ipynb +++ b/h5tests/single-test.ipynb @@ -1,22 +1,27 @@ { "cells": [ + { + "cell_type": "markdown", + "id": "a1039b23-f008-4740-adbd-bafb8eaccfd2", + "metadata": { + "tags": [], + "user_expressions": [] + }, + "source": [ + "## Testing access time on ICESat-2 ATL03 HDF5 files in AWS S3.\n", + "\n", + "This notebook runs a single test from the different access patterns and stores the results in `results/` and `logs/`\n", + "If we use files in the CryoCloud the results will be send to the S3 bucket `s3://nasa-cryo-persistent/h5cloud/benchmark_results/`\n" + ] + }, { "cell_type": "code", - "execution_count": 73, + "execution_count": 1, "id": "48daa283-8e1e-46e3-b4ce-1a0271b86d37", "metadata": { "tags": [] }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "The autoreload extension is already loaded. To reload it, use:\n", - " %reload_ext autoreload\n" - ] - } - ], + "outputs": [], "source": [ "%load_ext autoreload\n", "%autoreload \n", @@ -25,25 +30,54 @@ "import os\n", "current = os.path.abspath('..')\n", "sys.path.append(current)\n", - "from h5tests.xarray_arr_len import XarrayArrLen\n", - "from helpers.links import S3Links" + "from xarray_arr_mean import XarrayArrMean\n", + "import pandas as pd\n", + "\n", + "benchmarks = []" ] }, { "cell_type": "code", - "execution_count": 74, + "execution_count": 2, "id": "d6ce77fd-f9cd-48b1-94cd-1fe57f52e11f", "metadata": { "tags": [] }, "outputs": [], "source": [ - "test = XarrayArrLen('kerchunk-repacked', store_results=False)" + "granules = [\n", + " \"s3://its-live-data/cloud-experiments/h5cloud/atl03/average/original/ATL03_20200922221235_13680801_006_02.h5\",\n", + " # \"s3://its-live-data/cloud-experiments/h5cloud/atl03/average/original/ATL03_20191225111315_13680501_006_01.h5\",\n", + "]\n", + "\n", + "# We create the test cases for each kind of granule.\n", + "xarray_test = XarrayArrMean('atl03-xarray-original', files=granules, store_results=True)" + ] + }, + { + "cell_type": "markdown", + "id": "33dcde98-df71-4e49-b051-f67c865981c6", + "metadata": { + "tags": [], + "user_expressions": [] + }, + "source": [ + "### Benchmarking access patterns \n", + "\n", + "```python\n", + "io_params ={\n", + " \"fsspec_params\": {}, # if we use fsspec we can pass io params here\n", + " \"h5py_params\" : {} # if we use h5py we can pass io params here\n", + "}\n", + "```\n", + "\n", + "Accesing ATL03 with Xarray takes considerably longer than using h5py directly, this is mainly due the decoding and metadata that Xarray uses to represent the data.\n", + "Using Xarray it takes approx ~10 minutes per granule out of region (6+ GB granules) and ~2 minutes per granule in-region (6+ GB granules) when we access the non optimized granules.\n" ] }, { "cell_type": "code", - "execution_count": 75, + "execution_count": 3, "id": "60eeeb1b-9531-4fec-a847-3ca5304c4685", "metadata": { "tags": [] @@ -51,17 +85,163 @@ "outputs": [ { "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
libraryformatmeantimetotal_requested_bytestotal_requestsavg_req_size
0xarrayoriginal18.12813670.1682850852992382813284
\n", + "
" + ], "text/plain": [ - "(338294671, 69.48884391784668)" + " library format mean time total_requested_bytes \\\n", + "0 xarray original 18.128136 70.16828 50852992 \n", + "\n", + " total_requests avg_req_size \n", + "0 3828 13284 " ] }, - "execution_count": 75, + "execution_count": 3, "metadata": {}, "output_type": "execute_result" } ], "source": [ - "test.run()" + "# we don't need this when using the original granules.\n", + "io_params ={\n", + " \"fsspec_params\": {\n", + " # \"cache_type\": \"blockcache\",\n", + " # \"block_size\": 8*1024*1024\n", + " },\n", + " \"h5py_params\" : {\n", + "# \"driver_kwds\": {\n", + "# \"page_buf_size\": 64*1024*1024,\n", + "# \"rdcc_nbytes\": 8*1024*1024\n", + "# }\n", + "\n", + " }\n", + "}\n", + "\n", + "# this info gets stored in logs and csv files as usual but we want to plot them here too.\n", + "execution_info, execution_time = xarray_test.run(io_params)\n", + "\n", + "io_stats = execution_info[1][\"io_stats\"]\n", + "\n", + "benchmarks.append({\"library\": \"xarray\",\n", + " \"format\": \"original\",\n", + " \"mean\": execution_info[0],\n", + " \"time\": execution_time[\"execution_time\"],\n", + " \"total_requested_bytes\": io_stats[\"total_reqs_bytes\"],\n", + " \"total_requests\": io_stats[\"total_reqs\"],\n", + " \"avg_req_size\": io_stats[\"avg_req_size\"]})\n", + "\n", + "df = pd.DataFrame.from_dict(benchmarks)\n", + "df" + ] + }, + { + "cell_type": "markdown", + "id": "3d41bada-8735-4973-8536-bd9050b2e31f", + "metadata": { + "tags": [], + "user_expressions": [] + }, + "source": [ + "## Plotting the resuls\n" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "3ff4c22f-7f77-4c69-a84c-f13b0fbba1f2", + "metadata": { + "tags": [] + }, + "outputs": [ + { + "data": { + "image/png": "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okQJ+fj45PoeDBQ5BkChGDx4sJFkqlSpYt566y2zdOlS079/fyPJ9OnTx1StWtV8+OGHZunSpeaJJ54wksy3335r2//s2bOmZs2apnjx4mb06NHmxx9/NGPHjjWBgYHmvvvuM1ar1db3hRdeMOPHjzeLFy82y5cvNx988IEpXry4eeKJJ+xqatiwoQkJCTGRkZFmwoQJZunSpaZXr15GkpkyZYqtX3p6uvH29jb9+/fP89zWr19vJJn58+df8jVITU010dHRxtfX14waNcrExsaaQYMGGTc3N9OqVSu7vpJMRESEqVOnjpk9e7ZZtGiRiYmJMW5ubmbv3r2XPM6KFSuMJFO2bFnTrl078/3335tp06aZypUrm4CAALv9u3XrZjw8PEy1atXMqFGjzI8//mjefPNNY7FYzNChQwtc+9q1a423t7dp1aqVWbt2rVm7dq3ZsWOHMcaYP//80/j7+5tKlSqZqVOnmoULF5qOHTsaSebdd9/NVX9ERITp3LmzWbhwoZk5c6YpV66ciYyMNFlZWZc8/x9//NG4urqau+++28ydO9fMmTPH3HnnnaZcuXLG8dt6fj5XduzYYRo0aGDCwsJs57R27Vrb9scff9xMnDjRLF261CxdutS89dZbxtvb2+71y8uOHTvMG2+8YSSZSZMmmbVr15o9e/YYY4wZPny4kWQ6duxoFi5caKZOnWoqVqxoAgMDze7du+0+fu7u7iYiIsKMGDHCLFu2zCxZsuSix8z5Oixfvrx59dVXzdKlS828efOMMee/Hho2bJhrn27dupny5cvbnu/fv9/28WnRooWZN2+emTdvnomKijLFihUzp0+ftvWtUqWKqVy5svnqq6/MqlWrzLfffmteeukls2LFiku+NtOnTzeSTLNmzcy8efPMrFmzzB133GE8PDzMzz//nOt8qlSpYt58802zdOlSM3r0aOPp6Znr6/1iVq9ebSSZV155xRhjzGOPPWYsFovZt29fnv0bNmxoqlevnue2nM/dOXPmXPR4EyZMMJJsXxcXql27tqlXr16u9uzsbJOZmWni4+PNuHHjjJubm5kwYUJ+Ti+XnI/fpEmTbG07duwwgYGBJioqykydOtXExsaal156ybi4uJghQ4bY+o0YMcK4urqawYMHm2XLlpnFixebMWPG2PVp3ry5KVGihPnss8/MypUrzbx588yb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6C/VF1qxYoVcXFxUsWLFf12Pj4+PGjVqpC1btig6OjrP7695/XElKChIDz30kHr37q1Tp07l+Uax5cqVU58+fdS0adNLfswvx2Kx6LbbbtMHH3ygoKCgfI2VkpKi+fPn27XNmDFDLi4uuvfeeyWd/7mSkJCg7OzsPM/7wiBzse8BF5u9rFKliiIjI/Xbb7/lOXbt2rV5zzRc95hRAoqIfv366dtvv9W9996rF154QdHR0bJarTp06JBiY2P10ksvqW7dumratKk8PDzUsWNH9e/fX2lpaRo/fnyuS3UKokyZMqpYsaLWrVunvn375tqec/1+o0aNLjlO165dNW7cOHXr1k0HDhxQVFSUfvnlFw0fPlytWrVSkyZNrrjGvMTHx+uBBx5Qjx49lJSUpMGDB8vLy0sDBgwo8FgFqT0qKkorV67U999/r1KlSsnf319VqlTR4MGDtWDBAjVq1EhvvvmmgoODNX36dC1cuFAjR45UYGBgoZz3W2+9pRYtWqhp06Z66aWXlJ2drXfffVe+vr52s4IF+VyJiorS3LlzNX78eN1xxx1ycXFR7dq11bp1a40ePVqdOnXS008/rYSEBI0aNcoufBVUUFCQBg0apNdff11du3ZVx44dlZCQoKFDh8rLy0uDBw++4rEvpUuXLvr000/12GOPqUePHkpISNDIkSMLfFlfjm3btqlPnz7q0KGDIiMj5eHhoeXLl2vbtm2XnA1wcXHRyJEj1blzZ7Vp00Y9e/ZUenq63nvvPZ0+fVrvvPPOlZ6inZzZpP79++f5HlYpKSlatmyZpk2bpueff75AYzsuk56jYcOGKlGihEaOHKkuXbqoZ8+e6tixo/766y/1799fTZs2VYsWLWz9n376aQUEBKhOnToKDQ3VyZMnNWfOHM2aNUuvvPKK3WzS5MmT9cQTT2jSpEm57m+7nLFjx+ruu+/WPffco2effVYRERFKSUnRnj179P3332v58uWSpLZt26pGjRqqXbu2SpQooYMHD2rMmDEqX768IiMjlZSUpEaNGqlTp06qWrWq/P39tXHjRi1evDhfAfBCCxYs0CeffKL7779fFStWlDFGc+fO1enTp9W0adPL7h8SEqJnn31Whw4d0i233KJFixbp888/17PPPqty5cpJkh599FFNnz5drVq10vPPP686derI3d1dR44c0YoVK9SuXTs9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nycA4Mq5OS+iAQCuF7t27dKGDRtssyRubm565JFH9OWXX6pJkyaSpK1bt6pHjx557r9161a5urqqYcOGeW7ftGmTVqxYYZthutDevXvVrFkzNW7cWFFRUWrevLmaNWumhx56SMWKFVNwcLAef/xxNW/eXE2bNlWTJk308MMPq1SpUpc8p44dO8rV1VWpqakKDAzUqFGj1LJlS0nShAkT9MUXX+jgwYNKTU1VRkaGatasmd+Xy+bEiRM6fPiwnnzySbvXJisrS4GBgXZ9L5x9y1G9enW5urranpcqVUrbt28vcB0AgIIjKAEALmvixInKyspS6dKlbW3GGLm7uysxMVHFihWTt7f3Rfe/1DZJslqtatu2rd59991c20qVKiVXV1ctXbpUa9asUWxsrD766CMNHDhQ69evV4UKFTRp0iT17dtXixcv1qxZs/TGG29o6dKluuuuuy56zA8++EBNmjRRQECASpYsaWufPXu2XnjhBb3//vuqV6+e/P399d5772n9+vWXPIeLnZd0/vK7unXr2m27MABJkq+vb6793d3d7Z5bLBbbmACAq4t7lAAAl5SVlaWpU6fq/fff19atW22P3377TeXLl9f06dMlSdHR0Vq2bFmeY0RFRclqtWrVqlV5br/99tu1Y8cORUREqHLlynaPnABhsVjUoEEDDR06VFu2bJGHh4e+++472xi1atXSgAEDtGbNGtWoUUMzZsy45HmFhYWpcuXKdiFJkn7++WfVr19fvXr1Uq1atVS5cmXt3bvXro+Hh4eys7NztUmyaw8NDVXp0qW1b9++XOeVs/gDAKBoIigBAC5pwYIFSkxM1JNPPqkaNWrYPR566CHbynWDBw/WzJkzNXjwYO3cuVPbt2/XyJEjJUkRERHq1q2bunfvrnnz5mn//v1auXKlbYGE3r1769SpU+rYsaM2bNigffv2KTY2Vt27d1d2drbWr1+v4cOH69dff9WhQ4c0d+5cnThxQtWqVdP+/fs1YMAArV27VgcPHlRsbKx2796tatWqXdH5Vq5cWb/++quWLFmi3bt3a9CgQdq4caNdn4iICG3btk27du3SyZMnlZmZqZIlS8rb29u2EEVSUpKk86vkjRgxQmPHjtXu3bu1fft2TZo0SaNHj77SDwkA4BogKAEALmnixIlq0qRJrntqJOnBBx/U1q1btXnzZsXExGjOnDmaP3++atasqfvuu8/ucrXx48froYceUq9evVS1alX16NHDtrx3eHi4Vq9erezsbDVv3lw1atTQ888/r8DAQLm4uCggIEA//fSTWrVqpVtuuUVvvPGG3n//fbVs2VI+Pj76888/9eCDD+qWW27R008/rT59+qhnz55XdL7PPPOM2rdvr0ceeUR169ZVQkKCevXqZdenR48eqlKlimrXrq0SJUpo9erVcnNz04cffqhPP/1U4eHhateunSTpqaee0hdffKHJkycrKipKDRs21OTJk5lRAoAizmLMBWu7AgAAAACYUQIAAAAARwQlAAAAAHBAUAIAAAAABwQlAAAAAHBAUAIAAAAABwQlAAAAAHBAUAIAAAAABwQlAAAAAHBAUAIAAAAABwQlAAAAAHBAUAIAAAAABwQlAAAAAHDwf6wjbd4st0WyAAAAAElFTkSuQmCC\n", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "import matplotlib.pyplot as plt\n", + "\n", + "fig, ax = plt.subplots(figsize=(10, 6))\n", + "\n", + "for name, group in df.groupby(['library', 'format']):\n", + " library, format = name\n", + " x = f'{library}, {format}'\n", + " y = group['time'].mean()\n", + " ax.bar(f'{library}, {format}', group['time'].mean(), label=f'{library}, {format}', align='center')\n", + " ax.text(x, y + 0.05, f'{group[\"time\"].mean():.2f}', ha='center', va='bottom', color='black', fontsize=12)\n", + " ax.text(x, y - (y/2), f'Total Requests: {group[\"total_requests\"].mean()}', ha='center', va='bottom', color='black', fontsize=8)\n", + " ax.text(x, y - (y/2.7), f'Total Req Bytes (MB): {round(group[\"total_requested_bytes\"].mean() / (1024*1024) , 2)}', ha='center', va='bottom', color='black', fontsize=8)\n", + "\n", + "# Set labels and title\n", + "ax.set_xlabel('Access Pattern')\n", + "ax.set_ylabel('Time in Seconds')\n", + "ax.set_title(f'mean() on photon data for runs on ATL03, less is better ')\n", + "\n", + "# Rotate x-axis labels for better readability\n", + "plt.xticks(rotation=45, ha='right')\n", + "\n", + "# # Show legend\n", + "# ax.legend()\n", + "\n", + "# Show the plot\n", + "with plt.xkcd():\n", + " # This figure will be in XKCD-style\n", + " fig1 = plt.figure()" ] } ], @@ -81,7 +261,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.10.12" + "version": "3.10.13" } }, "nbformat": 4, diff --git a/h5tests/xarray_arr_mean.py b/h5tests/xarray_arr_mean.py index 0c1fc92..6287a39 100644 --- a/h5tests/xarray_arr_mean.py +++ b/h5tests/xarray_arr_mean.py @@ -1,31 +1,46 @@ -from .h5test import H5Test, timer_decorator import fsspec -import xarray as xr import numpy as np +import xarray as xr +from h5test import H5Test, fsspec_logging_decorator + class XarrayArrMean(H5Test): - def open_reference_ds(self, file): + def open_reference_ds(self, file: str, dataset: str): fs = fsspec.filesystem( - 'reference', - fo=file, - remote_protocol='s3', - remote_options=dict(anon=False), - skip_instance_cache=True + "reference", + fo=file, + remote_protocol="s3", + remote_options=dict(anon=self.anon_access), + skip_instance_cache=True, + ) + return xr.open_dataset( + fs.get_mapper(""), engine="zarr", consolidated=False, group=dataset ) - return xr.open_dataset(fs.get_mapper(""), engine='zarr', consolidated=False, group='gt1l/heights') @timer_decorator - def run(self): - group = '/gt1l/heights' - variable = 'h_ph' - if 'kerchunk' in self.data_format: - datasets = [self.open_reference_ds(file) for file in self.files] + @fsspec_logging_decorator + def run(self, io_params={}, dataset="/gt1l/heights", variable="h_ph"): + if "kerchunk" in self.data_format: + datasets_ref = [ + self.open_reference_ds(file, dataset) for file in self.files + ] h_ph_values = [] - for dataset in datasets: - h_ph_values = np.append(h_ph_values, dataset['h_ph'].values) + for ds in datasets_ref: + h_ph_values = np.append(h_ph_values, ds[variable].values) return np.mean(h_ph_values) else: - s3_fileset = [self.s3_fs.open(file) for file in self.files] - xrds = xr.open_mfdataset(s3_fileset, group=group, combine='by_coords', engine='h5netcdf') - h_ph_values = xrds['h_ph'] + if "fsspec_params" in io_params: + fsspec_params = io_params["fsspec_params"] + if "h5py_params" in io_params: + h5py_params = io_params["h5py_params"] + + s3_fileset = [self.s3_fs.open(file, **fsspec_params) for file in self.files] + xrds = xr.open_mfdataset( + s3_fileset, + group=dataset, + combine="by_coords", + engine="h5netcdf", + **h5py_params + ) + h_ph_values = xrds[variable] return float(np.mean(h_ph_values).values) diff --git a/helpers/links-old.json b/helpers/links-old.json new file mode 100644 index 0000000..1f0b836 --- /dev/null +++ b/helpers/links-old.json @@ -0,0 +1,53 @@ +{ + "flatgeobuf": { + "ATL03_20181120182818_08110112_006_02.fgb": "s3://nasa-cryo-scratch/h5cloud/flatgeobuf/ATL03_20181120182818_08110112_006_02.fgb", + "ATL03_20190219140808_08110212_006_02.fgb": "s3://nasa-cryo-scratch/h5cloud/flatgeobuf/ATL03_20190219140808_08110212_006_02.fgb", + "ATL03_20200217204710_08110612_006_01.fgb": "s3://nasa-cryo-scratch/h5cloud/flatgeobuf/ATL03_20200217204710_08110612_006_01.fgb", + "ATL03_20211114142614_08111312_006_01.fgb": "s3://nasa-cryo-scratch/h5cloud/flatgeobuf/ATL03_20211114142614_08111312_006_01.fgb", + "ATL03_20230211164520_08111812_006_01.fgb": "s3://nasa-cryo-scratch/h5cloud/flatgeobuf/ATL03_20230211164520_08111812_006_01.fgb" + }, + "flatgeobuf_no_sindex": { + "ATL03_20181120182818_08110112_006_02_no_sindex.fgb": "s3://nasa-cryo-scratch/h5cloud/flatgeobuf_no_sindex/ATL03_20181120182818_08110112_006_02_no_sindex.fgb", + "ATL03_20190219140808_08110212_006_02_no_sindex.fgb": "s3://nasa-cryo-scratch/h5cloud/flatgeobuf_no_sindex/ATL03_20190219140808_08110212_006_02_no_sindex.fgb", + "ATL03_20200217204710_08110612_006_01_no_sindex.fgb": "s3://nasa-cryo-scratch/h5cloud/flatgeobuf_no_sindex/ATL03_20200217204710_08110612_006_01_no_sindex.fgb", + "ATL03_20211114142614_08111312_006_01_no_sindex.fgb": "s3://nasa-cryo-scratch/h5cloud/flatgeobuf_no_sindex/ATL03_20211114142614_08111312_006_01_no_sindex.fgb", + "ATL03_20230211164520_08111812_006_01_no_sindex.fgb": "s3://nasa-cryo-scratch/h5cloud/flatgeobuf_no_sindex/ATL03_20230211164520_08111812_006_01_no_sindex.fgb" + }, + "geoparquet": { + "ATL03_20181120182818_08110112_006_02.h5.gpq": "s3://nasa-cryo-scratch/h5cloud/geoparquet/ATL03_20181120182818_08110112_006_02.h5.gpq", + "ATL03_20190219140808_08110212_006_02.h5.gpq": "s3://nasa-cryo-scratch/h5cloud/geoparquet/ATL03_20190219140808_08110212_006_02.h5.gpq", + "ATL03_20200217204710_08110612_006_01.h5.gpq": "s3://nasa-cryo-scratch/h5cloud/geoparquet/ATL03_20200217204710_08110612_006_01.h5.gpq", + "ATL03_20211114142614_08111312_006_01.h5.gpq": "s3://nasa-cryo-scratch/h5cloud/geoparquet/ATL03_20211114142614_08111312_006_01.h5.gpq", + "ATL03_20230211164520_08111812_006_01.h5.gpq": "s3://nasa-cryo-scratch/h5cloud/geoparquet/ATL03_20230211164520_08111812_006_01.h5.gpq", + "['ATL03_20200217204710_08110612_006_01.h5'].gpq": "s3://nasa-cryo-scratch/h5cloud/geoparquet/['ATL03_20200217204710_08110612_006_01.h5'].gpq" + }, + + "h5repack": { + "ATL03_20181120182818_08110112_006_02_repacked.h5": "s3://nasa-cryo-scratch/h5cloud/h5repack/ATL03_20181120182818_08110112_006_02_repacked.h5", + "ATL03_20190219140808_08110212_006_02_repacked.h5": "s3://nasa-cryo-scratch/h5cloud/h5repack/ATL03_20190219140808_08110212_006_02_repacked.h5", + "ATL03_20200217204710_08110612_006_01_repacked.h5": "s3://nasa-cryo-scratch/h5cloud/h5repack/ATL03_20200217204710_08110612_006_01_repacked.h5", + "ATL03_20211114142614_08111312_006_01_repacked.h5": "s3://nasa-cryo-scratch/h5cloud/h5repack/ATL03_20211114142614_08111312_006_01_repacked.h5", + "ATL03_20230211164520_08111812_006_01_repacked.h5": "s3://nasa-cryo-scratch/h5cloud/h5repack/ATL03_20230211164520_08111812_006_01_repacked.h5" + }, + "kerchunk-original": { + "original_ATL03_20181120182818_08110112_006_02.json": "s3://nasa-cryo-scratch/h5cloud/kerchunk-original/original_ATL03_20181120182818_08110112_006_02.json", + "original_ATL03_20190219140808_08110212_006_02.json": "s3://nasa-cryo-scratch/h5cloud/kerchunk-original/original_ATL03_20190219140808_08110212_006_02.json", + "original_ATL03_20200217204710_08110612_006_01.json": "s3://nasa-cryo-scratch/h5cloud/kerchunk-original/original_ATL03_20200217204710_08110612_006_01.json", + "original_ATL03_20211114142614_08111312_006_01.json": "s3://nasa-cryo-scratch/h5cloud/kerchunk-original/original_ATL03_20211114142614_08111312_006_01.json", + "original_ATL03_20230211164520_08111812_006_01.json": "s3://nasa-cryo-scratch/h5cloud/kerchunk-original/original_ATL03_20230211164520_08111812_006_01.json" + }, + "kerchunk-repacked": { + "h5repack_ATL03_20181120182818_08110112_006_02_repacked.json": "s3://nasa-cryo-scratch/h5cloud/kerchunk-repacked/h5repack_ATL03_20181120182818_08110112_006_02_repacked.json", + "h5repack_ATL03_20190219140808_08110212_006_02_repacked.json": "s3://nasa-cryo-scratch/h5cloud/kerchunk-repacked/h5repack_ATL03_20190219140808_08110212_006_02_repacked.json", + "h5repack_ATL03_20200217204710_08110612_006_01_repacked.json": "s3://nasa-cryo-scratch/h5cloud/kerchunk-repacked/h5repack_ATL03_20200217204710_08110612_006_01_repacked.json", + "h5repack_ATL03_20211114142614_08111312_006_01_repacked.json": "s3://nasa-cryo-scratch/h5cloud/kerchunk-repacked/h5repack_ATL03_20211114142614_08111312_006_01_repacked.json", + "h5repack_ATL03_20230211164520_08111812_006_01_repacked.json": "s3://nasa-cryo-scratch/h5cloud/kerchunk-repacked/h5repack_ATL03_20230211164520_08111812_006_01_repacked.json" + }, + "original": { + "ATL03_20181120182818_08110112_006_02.h5": "s3://nasa-cryo-permanent/h5cloud/original/ATL03_20181120182818_08110112_006_02.h5", + "ATL03_20190219140808_08110212_006_02.h5": "s3://nasa-cryo-permanent/h5cloud/original/ATL03_20190219140808_08110212_006_02.h5", + "ATL03_20200217204710_08110612_006_01.h5": "s3://nasa-cryo-permanent/h5cloud/original/ATL03_20200217204710_08110612_006_01.h5", + "ATL03_20211114142614_08111312_006_01.h5": "s3://nasa-cryo-permanent/h5cloud/original/ATL03_20211114142614_08111312_006_01.h5", + "ATL03_20230211164520_08111812_006_01.h5": "s3://nasa-cryo-permanent/h5cloud/original/ATL03_20230211164520_08111812_006_01.h5" + } +} \ No newline at end of file diff --git a/helpers/links.py b/helpers/links.py index 5590042..e212ef0 100644 --- a/helpers/links.py +++ b/helpers/links.py @@ -4,8 +4,7 @@ import s3fs -S3LINK = "s3://nasa-cryo-scratch/h5cloud/" -S3FILELINKS = Path("../helpers/s3filelinks.json") +S3LINK = "s3://nasa-cryo-permanent/h5cloud/" class S3Links: @@ -41,9 +40,11 @@ class S3Links: 'h5cloud/original/ATL03_20181120182818_08110112_006_02.h5' """ - def __init__(self): - self.json_file = S3FILELINKS - self.table = load_s3testfile(S3FILELINKS) + def __init__(self, file="../helpers/s3filelinks.json"): + self.S3FILELINKS = Path(file) + + self.json_file = self.S3FILELINKS + self.table = load_s3testfile(self.S3FILELINKS) self.formats = list(self.table.keys()) def get_links_by_format(self, file_format): @@ -86,9 +87,9 @@ def update_links(self, write_to_file=True): print("Differences between self.table and S3 buckets: updating self.table") self.table = filelinks self.formats = list(self.table.keys()) - response = input(f"Update {S3FILELINKS} (y or n)?") + response = input(f"Update {self.S3FILELINKS} (y or n)?") if response.lower() == "y": - print(f"Updating {S3FILELINKS}") + print(f"Updating {self.S3FILELINKS}") write_s3links(filelinks) diff --git a/helpers/s3filelinks.json b/helpers/s3filelinks.json index 2a4ab87..2818b4b 100644 --- a/helpers/s3filelinks.json +++ b/helpers/s3filelinks.json @@ -1,52 +1,67 @@ { "flatgeobuf": { - "ATL03_20181120182818_08110112_006_02.fgb": "s3://nasa-cryo-scratch/h5cloud/flatgeobuf/ATL03_20181120182818_08110112_006_02.fgb", - "ATL03_20190219140808_08110212_006_02.fgb": "s3://nasa-cryo-scratch/h5cloud/flatgeobuf/ATL03_20190219140808_08110212_006_02.fgb", - "ATL03_20200217204710_08110612_006_01.fgb": "s3://nasa-cryo-scratch/h5cloud/flatgeobuf/ATL03_20200217204710_08110612_006_01.fgb", - "ATL03_20211114142614_08111312_006_01.fgb": "s3://nasa-cryo-scratch/h5cloud/flatgeobuf/ATL03_20211114142614_08111312_006_01.fgb", - "ATL03_20230211164520_08111812_006_01.fgb": "s3://nasa-cryo-scratch/h5cloud/flatgeobuf/ATL03_20230211164520_08111812_006_01.fgb" + }, "flatgeobuf_no_sindex": { - "ATL03_20181120182818_08110112_006_02_no_sindex.fgb": "s3://nasa-cryo-scratch/h5cloud/flatgeobuf_no_sindex/ATL03_20181120182818_08110112_006_02_no_sindex.fgb", - "ATL03_20190219140808_08110212_006_02_no_sindex.fgb": "s3://nasa-cryo-scratch/h5cloud/flatgeobuf_no_sindex/ATL03_20190219140808_08110212_006_02_no_sindex.fgb", - "ATL03_20200217204710_08110612_006_01_no_sindex.fgb": "s3://nasa-cryo-scratch/h5cloud/flatgeobuf_no_sindex/ATL03_20200217204710_08110612_006_01_no_sindex.fgb", - "ATL03_20211114142614_08111312_006_01_no_sindex.fgb": "s3://nasa-cryo-scratch/h5cloud/flatgeobuf_no_sindex/ATL03_20211114142614_08111312_006_01_no_sindex.fgb", - "ATL03_20230211164520_08111812_006_01_no_sindex.fgb": "s3://nasa-cryo-scratch/h5cloud/flatgeobuf_no_sindex/ATL03_20230211164520_08111812_006_01_no_sindex.fgb" + }, "geoparquet": { - "ATL03_20181120182818_08110112_006_02.h5.gpq": "s3://nasa-cryo-scratch/h5cloud/geoparquet/ATL03_20181120182818_08110112_006_02.h5.gpq", - "ATL03_20190219140808_08110212_006_02.h5.gpq": "s3://nasa-cryo-scratch/h5cloud/geoparquet/ATL03_20190219140808_08110212_006_02.h5.gpq", - "ATL03_20200217204710_08110612_006_01.h5.gpq": "s3://nasa-cryo-scratch/h5cloud/geoparquet/ATL03_20200217204710_08110612_006_01.h5.gpq", - "ATL03_20211114142614_08111312_006_01.h5.gpq": "s3://nasa-cryo-scratch/h5cloud/geoparquet/ATL03_20211114142614_08111312_006_01.h5.gpq", - "ATL03_20230211164520_08111812_006_01.h5.gpq": "s3://nasa-cryo-scratch/h5cloud/geoparquet/ATL03_20230211164520_08111812_006_01.h5.gpq", - "['ATL03_20200217204710_08110612_006_01.h5'].gpq": "s3://nasa-cryo-scratch/h5cloud/geoparquet/['ATL03_20200217204710_08110612_006_01.h5'].gpq" - }, - "h5repack": { - "ATL03_20181120182818_08110112_006_02_repacked.h5": "s3://nasa-cryo-scratch/h5cloud/h5repack/ATL03_20181120182818_08110112_006_02_repacked.h5", - "ATL03_20190219140808_08110212_006_02_repacked.h5": "s3://nasa-cryo-scratch/h5cloud/h5repack/ATL03_20190219140808_08110212_006_02_repacked.h5", - "ATL03_20200217204710_08110612_006_01_repacked.h5": "s3://nasa-cryo-scratch/h5cloud/h5repack/ATL03_20200217204710_08110612_006_01_repacked.h5", - "ATL03_20211114142614_08111312_006_01_repacked.h5": "s3://nasa-cryo-scratch/h5cloud/h5repack/ATL03_20211114142614_08111312_006_01_repacked.h5", - "ATL03_20230211164520_08111812_006_01_repacked.h5": "s3://nasa-cryo-scratch/h5cloud/h5repack/ATL03_20230211164520_08111812_006_01_repacked.h5" - }, - "kerchunk-original": { - "original_ATL03_20181120182818_08110112_006_02.json": "s3://nasa-cryo-scratch/h5cloud/kerchunk-original/original_ATL03_20181120182818_08110112_006_02.json", - "original_ATL03_20190219140808_08110212_006_02.json": "s3://nasa-cryo-scratch/h5cloud/kerchunk-original/original_ATL03_20190219140808_08110212_006_02.json", - "original_ATL03_20200217204710_08110612_006_01.json": "s3://nasa-cryo-scratch/h5cloud/kerchunk-original/original_ATL03_20200217204710_08110612_006_01.json", - "original_ATL03_20211114142614_08111312_006_01.json": "s3://nasa-cryo-scratch/h5cloud/kerchunk-original/original_ATL03_20211114142614_08111312_006_01.json", - "original_ATL03_20230211164520_08111812_006_01.json": "s3://nasa-cryo-scratch/h5cloud/kerchunk-original/original_ATL03_20230211164520_08111812_006_01.json" - }, - "kerchunk-repacked": { - "h5repack_ATL03_20181120182818_08110112_006_02_repacked.json": "s3://nasa-cryo-scratch/h5cloud/kerchunk-repacked/h5repack_ATL03_20181120182818_08110112_006_02_repacked.json", - "h5repack_ATL03_20190219140808_08110212_006_02_repacked.json": "s3://nasa-cryo-scratch/h5cloud/kerchunk-repacked/h5repack_ATL03_20190219140808_08110212_006_02_repacked.json", - "h5repack_ATL03_20200217204710_08110612_006_01_repacked.json": "s3://nasa-cryo-scratch/h5cloud/kerchunk-repacked/h5repack_ATL03_20200217204710_08110612_006_01_repacked.json", - "h5repack_ATL03_20211114142614_08111312_006_01_repacked.json": "s3://nasa-cryo-scratch/h5cloud/kerchunk-repacked/h5repack_ATL03_20211114142614_08111312_006_01_repacked.json", - "h5repack_ATL03_20230211164520_08111812_006_01_repacked.json": "s3://nasa-cryo-scratch/h5cloud/kerchunk-repacked/h5repack_ATL03_20230211164520_08111812_006_01_repacked.json" - }, - "original": { - "ATL03_20181120182818_08110112_006_02.h5": "s3://nasa-cryo-scratch/h5cloud/original/ATL03_20181120182818_08110112_006_02.h5", - "ATL03_20190219140808_08110212_006_02.h5": "s3://nasa-cryo-scratch/h5cloud/original/ATL03_20190219140808_08110212_006_02.h5", - "ATL03_20200217204710_08110612_006_01.h5": "s3://nasa-cryo-scratch/h5cloud/original/ATL03_20200217204710_08110612_006_01.h5", - "ATL03_20211114142614_08111312_006_01.h5": "s3://nasa-cryo-scratch/h5cloud/original/ATL03_20211114142614_08111312_006_01.h5", - "ATL03_20230211164520_08111812_006_01.h5": "s3://nasa-cryo-scratch/h5cloud/original/ATL03_20230211164520_08111812_006_01.h5" + + }, + "atl03-bigsize-original": { + "ATL03_20181120182818_08110112_006_02.h5": "s3://nasa-cryo-persistent/h5cloud/atl03/big/original/ATL03_20181120182818_08110112_006_02.h5", + "ATL03_20190219140808_08110212_006_02.h5": "s3://nasa-cryo-persistent/h5cloud/atl03/big/original/ATL03_20190219140808_08110212_006_02.h5", + "ATL03_20200217204710_08110612_006_01.h5": "s3://nasa-cryo-persistent/h5cloud/atl03/big/original/ATL03_20200217204710_08110612_006_01.h5", + "ATL03_20211114142614_08111312_006_01.h5": "s3://nasa-cryo-persistent/h5cloud/atl03/big/original/ATL03_20211114142614_08111312_006_01.h5", + "ATL03_20230211164520_08111812_006_01.h5": "s3://nasa-cryo-persistent/h5cloud/atl03/big/original/ATL03_20230211164520_08111812_006_01.h5" + }, + "atl03-bigsize-h5repack": { + "ATL03_20181120182818_08110112_006_02_repacked.h5": "s3://nasa-cryo-persistent/h5cloud/atl03/big/repacked/ATL03_20181120182818_08110112_006_02_repacked.h5", + "ATL03_20190219140808_08110212_006_02_repacked.h5": "s3://nasa-cryo-persistent/h5cloud/atl03/big/repacked/ATL03_20190219140808_08110212_006_02_repacked.h5", + "ATL03_20200217204710_08110612_006_01_repacked.h5": "s3://nasa-cryo-persistent/h5cloud/atl03/big/repacked/ATL03_20200217204710_08110612_006_01_repacked.h5", + "ATL03_20211114142614_08111312_006_01_repacked.h5": "s3://nasa-cryo-persistent/h5cloud/atl03/big/repacked/ATL03_20211114142614_08111312_006_01_repacked.h5", + "ATL03_20230211164520_08111812_006_01_repacked.h5": "s3://nasa-cryo-persistent/h5cloud/atl03/big/repacked/ATL03_20230211164520_08111812_006_01_repacked.h5" + }, + "atl03-kerchunk-bigsize-original": { + "atl03_ATL03_20190219140808_08110212_006_02.json": "s3://nasa-cryo-persistent/h5cloud/atl03/big/original/kerchunk/atl03_ATL03_20190219140808_08110212_006_02.json", + "atl03_ATL03_20230211164520_08111812_006_01.json": "s3://nasa-cryo-persistent/h5cloud/atl03/big/original/kerchunk/atl03_ATL03_20230211164520_08111812_006_01.json", + "atl03_ATL03_20200217204710_08110612_006_01.json": "s3://nasa-cryo-persistent/h5cloud/atl03/big/original/kerchunk/atl03_ATL03_20200217204710_08110612_006_01.json", + "atl03_ATL03_20181120182818_08110112_006_02.json": "s3://nasa-cryo-persistent/h5cloud/atl03/big/original/kerchunk/atl03_ATL03_20181120182818_08110112_006_02.json", + "atl03_ATL03_20211114142614_08111312_006_01.json": "s3://nasa-cryo-persistent/h5cloud/atl03/big/original/kerchunk/atl03_ATL03_20211114142614_08111312_006_01.json" + }, + "atl03-kerchunk-bigsize-repacked": { + "atl03_ATL03_20181120182818_08110112_006_02_repacked.json": "s3://nasa-cryo-persistent/h5cloud/atl03/big/repacked/kerchunk/atl03_ATL03_20181120182818_08110112_006_02_repacked.json", + "atl03_ATL03_20190219140808_08110212_006_02_repacked.json": "s3://nasa-cryo-persistent/h5cloud/atl03/big/repacked/kerchunk/atl03_ATL03_20190219140808_08110212_006_02_repacked.json", + "atl03_ATL03_20211114142614_08111312_006_01_repacked.json": "s3://nasa-cryo-persistent/h5cloud/atl03/big/repacked/kerchunk/atl03_ATL03_20211114142614_08111312_006_01_repacked.json", + "atl03_ATL03_20200217204710_08110612_006_01_repacked.json": "s3://nasa-cryo-persistent/h5cloud/atl03/big/repacked/kerchunk/atl03_ATL03_20200217204710_08110612_006_01_repacked.json", + "atl03_ATL03_20230211164520_08111812_006_01_repacked.json": "s3://nasa-cryo-persistent/h5cloud/atl03/big/repacked/kerchunk/atl03_ATL03_20230211164520_08111812_006_01_repacked.json" + }, + "atl03-midsize-original": { + "ATL03_20191225111315_13680501_006_01.h5": "s3://nasa-cryo-persistent/h5cloud/atl03/average/original/ATL03_20191225111315_13680501_006_01.h5", + "ATL03_20200922221235_13680801_006_02.h5": "s3://nasa-cryo-persistent/h5cloud/atl03/average/original/ATL03_20200922221235_13680801_006_02.h5", + "ATL03_20220620155150_13681501_006_01.h5": "s3://nasa-cryo-persistent/h5cloud/atl03/average/original/ATL03_20220620155150_13681501_006_01.h5", + "ATL03_20220919113142_13681601_006_01.h5": "s3://nasa-cryo-persistent/h5cloud/atl03/average/original/ATL03_20220919113142_13681601_006_01.h5", + "ATL03_20230618223036_13681901_006_01.h5": "s3://nasa-cryo-persistent/h5cloud/atl03/average/original/ATL03_20230618223036_13681901_006_01.h5" + }, + "atl03-midsize-h5repack":{ + "ATL03_20191225111315_13680501_006_01.h5": "s3://nasa-cryo-persistent/h5cloud/atl03/average/repacked/ATL03_20191225111315_13680501_006_01.h5", + "ATL03_20200922221235_13680801_006_02.h5": "s3://nasa-cryo-persistent/h5cloud/atl03/average/repacked/ATL03_20200922221235_13680801_006_02.h5", + "ATL03_20220620155150_13681501_006_01.h5": "s3://nasa-cryo-persistent/h5cloud/atl03/average/repacked/ATL03_20220620155150_13681501_006_01.h5", + "ATL03_20220919113142_13681601_006_01.h5": "s3://nasa-cryo-persistent/h5cloud/atl03/average/repacked/ATL03_20220919113142_13681601_006_01.h5", + "ATL03_20230618223036_13681901_006_01.h5": "s3://nasa-cryo-persistent/h5cloud/atl03/average/repacked/ATL03_20230618223036_13681901_006_01.h5" + }, + "atl03-kerchunk-midsize-original": { + "atl03_ATL03_20220919113142_13681601_006_01.json": "s3://nasa-cryo-persistent/h5cloud/atl03/average/original/kerchunk/atl03_ATL03_20220919113142_13681601_006_01.json", + "atl03_ATL03_20191225111315_13680501_006_01.json": "s3://nasa-cryo-persistent/h5cloud/atl03/average/original/kerchunk/atl03_ATL03_20191225111315_13680501_006_01.json", + "atl03_ATL03_20220620155150_13681501_006_01.json": "s3://nasa-cryo-persistent/h5cloud/atl03/average/original/kerchunk/atl03_ATL03_20220620155150_13681501_006_01.json", + "atl03_ATL03_20200922221235_13680801_006_02.json": "s3://nasa-cryo-persistent/h5cloud/atl03/average/original/kerchunk/atl03_ATL03_20200922221235_13680801_006_02.json", + "atl03_ATL03_20230618223036_13681901_006_01.json": "s3://nasa-cryo-persistent/h5cloud/atl03/average/original/kerchunk/atl03_ATL03_20230618223036_13681901_006_01.json" + }, + "atl03-kerchunk-midsize-repacked": { + "atl03_ATL03_20220620155150_13681501_006_01.json": "s3://nasa-cryo-persistent/h5cloud/atl03/average/repacked/kerchunk/atl03_ATL03_20220620155150_13681501_006_01.json", + "atl03_ATL03_20191225111315_13680501_006_01.json": "s3://nasa-cryo-persistent/h5cloud/atl03/average/repacked/kerchunk/atl03_ATL03_20191225111315_13680501_006_01.json", + "atl03_ATL03_20220919113142_13681601_006_01.json": "s3://nasa-cryo-persistent/h5cloud/atl03/average/repacked/kerchunk/atl03_ATL03_20220919113142_13681601_006_01.json", + "atl03_ATL03_20200922221235_13680801_006_02.json": "s3://nasa-cryo-persistent/h5cloud/atl03/average/repacked/kerchunk/atl03_ATL03_20200922221235_13680801_006_02.json", + "atl03_ATL03_20230618223036_13681901_006_01.json": "s3://nasa-cryo-persistent/h5cloud/atl03/average/repacked/kerchunk/atl03_ATL03_20230618223036_13681901_006_01.json" } } \ No newline at end of file diff --git a/helpers/s3itslive.json b/helpers/s3itslive.json new file mode 100644 index 0000000..4f0a16d --- /dev/null +++ b/helpers/s3itslive.json @@ -0,0 +1,50 @@ +{ + "flatgeobuf": { + + }, + "flatgeobuf_no_sindex": { + + }, + "geoparquet": { + + }, + "atl03-bigsize-original": { + "ATL03_20181120182818_08110112_006_02.h5": "s3://its-live-data/cloud-experiments/h5cloud/atl03/big/original/ATL03_20181120182818_08110112_006_02.h5", + "ATL03_20190219140808_08110212_006_02.h5": "s3://its-live-data/cloud-experiments/h5cloud/atl03/big/original/ATL03_20190219140808_08110212_006_02.h5", + "ATL03_20200217204710_08110612_006_01.h5": "s3://its-live-data/cloud-experiments/h5cloud/atl03/big/original/ATL03_20200217204710_08110612_006_01.h5", + "ATL03_20211114142614_08111312_006_01.h5": "s3://its-live-data/cloud-experiments/h5cloud/atl03/big/original/ATL03_20211114142614_08111312_006_01.h5", + "ATL03_20230211164520_08111812_006_01.h5": "s3://its-live-data/cloud-experiments/h5cloud/atl03/big/original/ATL03_20230211164520_08111812_006_01.h5" + }, + "atl03-bigsize-h5repack": { + "ATL03_20181120182818_08110112_006_02_repacked.h5": "s3://its-live-data/cloud-experiments/h5cloud/atl03/big/repacked/ATL03_20181120182818_08110112_006_02_repacked.h5", + "ATL03_20190219140808_08110212_006_02_repacked.h5": "s3://its-live-data/cloud-experiments/h5cloud/atl03/big/repacked/ATL03_20190219140808_08110212_006_02_repacked.h5", + "ATL03_20200217204710_08110612_006_01_repacked.h5": "s3://its-live-data/cloud-experiments/h5cloud/atl03/big/repacked/ATL03_20200217204710_08110612_006_01_repacked.h5", + "ATL03_20211114142614_08111312_006_01_repacked.h5": "s3://its-live-data/cloud-experiments/h5cloud/atl03/big/repacked/ATL03_20211114142614_08111312_006_01_repacked.h5", + "ATL03_20230211164520_08111812_006_01_repacked.h5": "s3://its-live-data/cloud-experiments/h5cloud/atl03/big/repacked/ATL03_20230211164520_08111812_006_01_repacked.h5" + }, + "atl03-kerchunk-bigsize-original": { + + }, + "atl03-kerchunk-bigsize-repacked": { + }, + "atl03-midsize-original": { + "ATL03_20191225111315_13680501_006_01.h5": "s3://its-live-data/cloud-experiments/h5cloud/atl03/average/original/ATL03_20191225111315_13680501_006_01.h5", + "ATL03_20200922221235_13680801_006_02.h5": "s3://its-live-data/cloud-experiments/h5cloud/atl03/average/original/ATL03_20200922221235_13680801_006_02.h5", + "ATL03_20220620155150_13681501_006_01.h5": "s3://its-live-data/cloud-experiments/h5cloud/atl03/average/original/ATL03_20220620155150_13681501_006_01.h5", + "ATL03_20220919113142_13681601_006_01.h5": "s3://its-live-data/cloud-experiments/h5cloud/atl03/average/original/ATL03_20220919113142_13681601_006_01.h5", + "ATL03_20230618223036_13681901_006_01.h5": "s3://its-live-data/cloud-experiments/h5cloud/atl03/average/original/ATL03_20230618223036_13681901_006_01.h5" + }, + "atl03-midsize-h5repack":{ + "ATL03_20191225111315_13680501_006_01.h5": "s3://its-live-data/cloud-experiments/h5cloud/atl03/average/repacked/ATL03_20191225111315_13680501_006_01.h5", + "ATL03_20200922221235_13680801_006_02.h5": "s3://its-live-data/cloud-experiments/h5cloud/atl03/average/repacked/ATL03_20200922221235_13680801_006_02.h5", + "ATL03_20220620155150_13681501_006_01.h5": "s3://its-live-data/cloud-experiments/h5cloud/atl03/average/repacked/ATL03_20220620155150_13681501_006_01.h5", + "ATL03_20220919113142_13681601_006_01.h5": "s3://its-live-data/cloud-experiments/h5cloud/atl03/average/repacked/ATL03_20220919113142_13681601_006_01.h5", + "ATL03_20230618223036_13681901_006_01.h5": "s3://its-live-data/cloud-experiments/h5cloud/atl03/average/repacked/ATL03_20230618223036_13681901_006_01.h5" + }, + "atl03-kerchunk-midsize-original": { + + }, + "atl03-kerchunk-midsize-repacked": { + + } +} \ No newline at end of file diff --git a/notebooks/access_time.summary.png b/notebooks/access_time.summary.png new file mode 100644 index 0000000..b8c3e4e Binary files /dev/null and b/notebooks/access_time.summary.png differ diff --git a/notebooks/benchmarks.csv b/notebooks/benchmarks.csv new file mode 100644 index 0000000..bda4468 --- /dev/null +++ b/notebooks/benchmarks.csv @@ -0,0 +1,47 @@ +,tool,dataset,cloud-aware,format,file,time,mean,size,product +0,h5py,ATL03-1GB,no,original,s3://nasa-cryo-persistent/h5cloud/atl03/average/original/ATL03_20230618223036_13681901_006_01.h5,2.843794107437134,386.06738,1GB,ATL03 +1,h5py,ATL03-1GB,no,optimized,s3://nasa-cryo-persistent/h5cloud/atl03/average/repacked/ATL03_20230618223036_13681901_006_01.h5,4.157144546508789,386.06738,1GB,ATL03 +2,h5py,ATL03-7GB,no,original,s3://nasa-cryo-persistent/h5cloud/atl03/big/original/ATL03_20181120182818_08110112_006_02.h5,6.9494102001190186,1035.1631,7GB,ATL03 +3,h5py,ATL03-7GB,no,optimized,s3://nasa-cryo-persistent/h5cloud/atl03/big/repacked/ATL03_20181120182818_08110112_006_02_repacked.h5,13.6586012840271,1035.1631,7GB,ATL03 +4,h5py,ATL03-2GB,no,original,s3://nasa-cryo-persistent/h5cloud/atl03/big/original/ATL03_20210402143840_01341107_006_02.h5,1.4053022861480713,2049.7554,2GB,ATL03 +5,h5py,ATL03-2GB,no,optimized,s3://nasa-cryo-persistent/h5cloud/atl03/big/repacked/ATL03_20210402143840_01341107_006_02_repacked.h5,1.0851728916168213,2049.7554,2GB,ATL03 +6,kerchunk,ATL03-7GB-kerchunk,no,original,s3://nasa-cryo-persistent/h5cloud/atl03/big/original/kerchunk/atl03_ATL03_20181120182818_08110112_006_02.json,10.746918678283691," +array(1035.1631, dtype=float32)",7GB,ATL03 +7,kerchunk,ATL03-7GB-kerchunk,no,optimized,s3://nasa-cryo-persistent/h5cloud/atl03/big/repacked/kerchunk/atl03_ATL03_20181120182818_08110112_006_02_repacked.json,8.8134024143219," +array(1035.1631, dtype=float32)",7GB,ATL03 +8,xarray,ATL03-1GB,no,original,s3://nasa-cryo-persistent/h5cloud/atl03/average/original/ATL03_20230618223036_13681901_006_01.h5,46.50308704376221," +array(386.06738, dtype=float32)",1GB,ATL03 +9,xarray,ATL03-1GB,no,optimized,s3://nasa-cryo-persistent/h5cloud/atl03/average/repacked/ATL03_20230618223036_13681901_006_01.h5,10.25867509841919," +array(386.06738, dtype=float32)",1GB,ATL03 +10,xarray,ATL03-7GB,no,original,s3://nasa-cryo-persistent/h5cloud/atl03/big/original/ATL03_20181120182818_08110112_006_02.h5,62.89623713493347," +array(1035.1631, dtype=float32)",7GB,ATL03 +11,xarray,ATL03-7GB,no,optimized,s3://nasa-cryo-persistent/h5cloud/atl03/big/repacked/ATL03_20181120182818_08110112_006_02_repacked.h5,81.67518210411072," +array(1035.1631, dtype=float32)",7GB,ATL03 +12,xarray,ATL03-2GB,no,original,s3://nasa-cryo-persistent/h5cloud/atl03/big/original/ATL03_20210402143840_01341107_006_02.h5,47.506706953048706," +array(2049.7554, dtype=float32)",2GB,ATL03 +13,xarray,ATL03-2GB,no,optimized,s3://nasa-cryo-persistent/h5cloud/atl03/big/repacked/ATL03_20210402143840_01341107_006_02_repacked.h5,18.109654188156128," +array(2049.7554, dtype=float32)",2GB,ATL03 +14,h5coro,ATL03-1GB,no,original,s3://nasa-cryo-persistent/h5cloud/atl03/average/original/ATL03_20230618223036_13681901_006_01.h5,4.562052011489868,386.06738,1GB,ATL03 +15,h5coro,ATL03-1GB,no,optimized,s3://nasa-cryo-persistent/h5cloud/atl03/average/repacked/ATL03_20230618223036_13681901_006_01.h5,4.286046743392944,386.06738,1GB,ATL03 +16,h5coro,ATL03-7GB,no,original,s3://nasa-cryo-persistent/h5cloud/atl03/big/original/ATL03_20181120182818_08110112_006_02.h5,14.072925567626953,1035.1631,7GB,ATL03 +17,h5coro,ATL03-7GB,no,optimized,s3://nasa-cryo-persistent/h5cloud/atl03/big/repacked/ATL03_20181120182818_08110112_006_02_repacked.h5,11.79448390007019,1035.1631,7GB,ATL03 +18,h5coro,ATL03-2GB,no,original,s3://nasa-cryo-persistent/h5cloud/atl03/big/original/ATL03_20210402143840_01341107_006_02.h5,3.1101267337799072,2049.7554,2GB,ATL03 +19,h5coro,ATL03-2GB,no,optimized,s3://nasa-cryo-persistent/h5cloud/atl03/big/repacked/ATL03_20210402143840_01341107_006_02_repacked.h5,1.8120653629302979,2049.7554,2GB,ATL03 +20,h5py,ATL03-1GB,yes,original,s3://nasa-cryo-persistent/h5cloud/atl03/average/original/ATL03_20230618223036_13681901_006_01.h5,1.8618409633636475,386.06738,1GB,ATL03 +21,h5py,ATL03-1GB,yes,optimized,s3://nasa-cryo-persistent/h5cloud/atl03/average/repacked/ATL03_20230618223036_13681901_006_01.h5,1.9302234649658203,386.06738,1GB,ATL03 +22,h5py,ATL03-7GB,yes,original,s3://nasa-cryo-persistent/h5cloud/atl03/big/original/ATL03_20181120182818_08110112_006_02.h5,6.602761507034302,1035.1631,7GB,ATL03 +23,h5py,ATL03-7GB,yes,optimized,s3://nasa-cryo-persistent/h5cloud/atl03/big/repacked/ATL03_20181120182818_08110112_006_02_repacked.h5,5.758350849151611,1035.1631,7GB,ATL03 +24,h5py,ATL03-2GB,yes,original,s3://nasa-cryo-persistent/h5cloud/atl03/big/original/ATL03_20210402143840_01341107_006_02.h5,1.2604756355285645,2049.7554,2GB,ATL03 +25,h5py,ATL03-2GB,yes,optimized,s3://nasa-cryo-persistent/h5cloud/atl03/big/repacked/ATL03_20210402143840_01341107_006_02_repacked.h5,0.8633284568786621,2049.7554,2GB,ATL03 +26,xarray,ATL03-1GB,yes,original,s3://nasa-cryo-persistent/h5cloud/atl03/average/original/ATL03_20230618223036_13681901_006_01.h5,42.18248891830444," +array(386.06738, dtype=float32)",1GB,ATL03 +27,xarray,ATL03-1GB,yes,optimized,s3://nasa-cryo-persistent/h5cloud/atl03/average/repacked/ATL03_20230618223036_13681901_006_01.h5,2.5429904460906982," +array(386.06738, dtype=float32)",1GB,ATL03 +28,xarray,ATL03-7GB,yes,original,s3://nasa-cryo-persistent/h5cloud/atl03/big/original/ATL03_20181120182818_08110112_006_02.h5,48.71459078788757," +array(1035.1631, dtype=float32)",7GB,ATL03 +29,xarray,ATL03-7GB,yes,optimized,s3://nasa-cryo-persistent/h5cloud/atl03/big/repacked/ATL03_20181120182818_08110112_006_02_repacked.h5,6.6719231605529785," +array(1035.1631, dtype=float32)",7GB,ATL03 +30,xarray,ATL03-2GB,yes,original,s3://nasa-cryo-persistent/h5cloud/atl03/big/original/ATL03_20210402143840_01341107_006_02.h5,40.31614112854004," +array(2049.7554, dtype=float32)",2GB,ATL03 +31,xarray,ATL03-2GB,yes,optimized,s3://nasa-cryo-persistent/h5cloud/atl03/big/repacked/ATL03_20210402143840_01341107_006_02_repacked.h5,2.156572103500366," +array(2049.7554, dtype=float32)",2GB,ATL03 diff --git a/notebooks/01_data-selection.ipynb b/notebooks/data-wrangling/01_data-selection.ipynb similarity index 100% rename from notebooks/01_data-selection.ipynb rename to notebooks/data-wrangling/01_data-selection.ipynb diff --git a/notebooks/arr_mean_bar_plot.png b/notebooks/data-wrangling/arr_mean_bar_plot.png similarity index 100% rename from notebooks/arr_mean_bar_plot.png rename to notebooks/data-wrangling/arr_mean_bar_plot.png diff --git a/notebooks/benchmark-h5repack.ipynb b/notebooks/data-wrangling/benchmark-h5repack.ipynb similarity index 100% rename from notebooks/benchmark-h5repack.ipynb rename to notebooks/data-wrangling/benchmark-h5repack.ipynb diff --git a/notebooks/benchmark-small-file-h5repack.ipynb b/notebooks/data-wrangling/benchmark-small-file-h5repack.ipynb similarity index 100% rename from notebooks/benchmark-small-file-h5repack.ipynb rename to notebooks/data-wrangling/benchmark-small-file-h5repack.ipynb diff --git a/notebooks/benchmarks-outline.ipynb b/notebooks/data-wrangling/benchmarks-outline.ipynb similarity index 100% rename from notebooks/benchmarks-outline.ipynb rename to notebooks/data-wrangling/benchmarks-outline.ipynb diff --git a/notebooks/data-wrangling/cloud-optimized-hdf5.ipynb b/notebooks/data-wrangling/cloud-optimized-hdf5.ipynb new file mode 100644 index 0000000..98eaa91 --- /dev/null +++ b/notebooks/data-wrangling/cloud-optimized-hdf5.ipynb @@ -0,0 +1,1062 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "11f9a9cb-c049-461e-8578-7090a644508e", + "metadata": {}, + "source": [ + "# Cloud Optimized HDF: or How I Learned to Stop Worrying and Love the Format\n", + "\n", + "\n", + "\n" + ] + }, + { + "cell_type": "markdown", + "id": "6332a484-8fd6-4448-827f-aa48e6322f8f", + "metadata": {}, + "source": [ + "" + ] + }, + { + "cell_type": "markdown", + "id": "2d37475f-42b0-4105-b34c-529f627d9066", + "metadata": {}, + "source": [ + "## The big ol list of \"ifs\"\n", + "\n", + "* We use the most recent versions of h5py, xarray and fsspec\n", + "* We create the HDF5 files with [cloud optimized flags](https://www.youtube.com/watch?v=rcS5vt-mKok)\n", + " * if the files are out there we can repack them, consolidating the metadata and perhaps incresing the chunk sizes\n", + "* We know how to \"tweak the nobs\" (or a fair understanding of what the I/O libraries are doing)." + ] + }, + { + "cell_type": "code", + "execution_count": 17, + "id": "736bb5fb-c5cd-42bf-be4e-6b81ae6eb865", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "xarray v2024.1.1\n", + "h5py v3.10.0\n", + "s3fs v2023.12.2\n" + ] + } + ], + "source": [ + "import xarray as xr\n", + "import h5py\n", + "import s3fs\n", + "\n", + "fs = s3fs.S3FileSystem(anon=True)\n", + "\n", + "for library in (xr, h5py, s3fs):\n", + " print(f'{library.__name__} v{library.__version__}')" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "id": "78d6697b-9f84-4edf-b426-fde27560bc68", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "{'ETag': '\"237bbd5828745b9e1a1e0ba88486e43c-835\"',\n", + " 'LastModified': datetime.datetime(2024, 1, 29, 4, 48, 24, tzinfo=tzutc()),\n", + " 'size': 6997123664,\n", + " 'name': 'its-live-data/cloud-experiments/h5cloud/atl03/big/original/ATL03_20190219140808_08110212_006_02.h5',\n", + " 'type': 'file',\n", + " 'StorageClass': 'INTELLIGENT_TIERING',\n", + " 'VersionId': None,\n", + " 'ContentType': 'application/x-hdf5'}" + ] + }, + "execution_count": 12, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# a \"big\" ATL03 file from the ICESat-2 mission\n", + "original_granule = \"s3://its-live-data/cloud-experiments/h5cloud/atl03/big/original/ATL03_20190219140808_08110212_006_02.h5\"\n", + "# the same \"big\" ATL03 file from the ICESat-2 mission, metadata consolidated in 8MB-size pages.\n", + "cloud_optimized = \"s3://its-live-data/cloud-experiments/h5cloud/atl03/big/repacked/ATL03_20190219140808_08110212_006_02_repacked.h5\"\n", + "\n", + "fs.info(original_granule)" + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "id": "e94bb01e-a325-4ab3-8f6a-ac5799d14f02", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "{'ETag': '\"08af0688f787f10eee1ccfb13f7eb66d-836\"',\n", + " 'LastModified': datetime.datetime(2024, 1, 29, 4, 52, 44, tzinfo=tzutc()),\n", + " 'size': 7008000000,\n", + " 'name': 'its-live-data/cloud-experiments/h5cloud/atl03/big/repacked/ATL03_20190219140808_08110212_006_02_repacked.h5',\n", + " 'type': 'file',\n", + " 'StorageClass': 'INTELLIGENT_TIERING',\n", + " 'VersionId': None,\n", + " 'ContentType': 'application/x-hdf5'}" + ] + }, + "execution_count": 13, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "fs.info(cloud_optimized)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "ec2bce8f-bcf4-4982-8556-d3a71209af74", + "metadata": {}, + "outputs": [], + "source": [ + "# don't even try this out of region (us-west-2) will take forever, forever >= 30 minutes\n", + "ds = xr.open_dataset(fs.open(original_granule),\n", + " group=\"/gt1l/heights\",\n", + " engine=\"h5netcdf\")\n", + "ds" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "f9b5701b-6a8b-41ac-a56a-34a4f42125e1", + "metadata": {}, + "outputs": [], + "source": [ + "# again... don't even try this out of region (us-west-2) will take forever, forever >= 30 minutes\n", + "ds = xr.open_dataset(fs.open(cloud_optimized),\n", + " group=\"/gt1l/heights\",\n", + " engine=\"h5netcdf\")\n", + "ds" + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "id": "0def8b43-7616-4e01-a502-3f44811ae47e", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "CPU times: user 4.16 s, sys: 3.04 s, total: 7.2 s\n", + "Wall time: 20.6 s\n" + ] + }, + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "
<xarray.Dataset>\n",
+       "Dimensions:         (delta_time: 73765028, ds_surf_type: 5)\n",
+       "Coordinates:\n",
+       "  * delta_time      (delta_time) datetime64[ns] 2019-02-19T14:08:08.557345384...\n",
+       "    lat_ph          (delta_time) float64 ...\n",
+       "    lon_ph          (delta_time) float64 ...\n",
+       "Dimensions without coordinates: ds_surf_type\n",
+       "Data variables:\n",
+       "    dist_ph_across  (delta_time) float32 ...\n",
+       "    dist_ph_along   (delta_time) float32 ...\n",
+       "    h_ph            (delta_time) float32 ...\n",
+       "    pce_mframe_cnt  (delta_time) uint32 ...\n",
+       "    ph_id_channel   (delta_time) uint8 ...\n",
+       "    ph_id_count     (delta_time) uint8 ...\n",
+       "    ph_id_pulse     (delta_time) uint8 ...\n",
+       "    quality_ph      (delta_time) int8 ...\n",
+       "    signal_conf_ph  (delta_time, ds_surf_type) int8 ...\n",
+       "    weight_ph       (delta_time) uint8 ...\n",
+       "Attributes:\n",
+       "    Description:  Contains arrays of the parameters for each received photon.\n",
+       "    data_rate:    Data are stored at the photon detection rate.
" + ], + "text/plain": [ + "\n", + "Dimensions: (delta_time: 73765028, ds_surf_type: 5)\n", + "Coordinates:\n", + " * delta_time (delta_time) datetime64[ns] 2019-02-19T14:08:08.557345384...\n", + " lat_ph (delta_time) float64 ...\n", + " lon_ph (delta_time) float64 ...\n", + "Dimensions without coordinates: ds_surf_type\n", + "Data variables:\n", + " dist_ph_across (delta_time) float32 ...\n", + " dist_ph_along (delta_time) float32 ...\n", + " h_ph (delta_time) float32 ...\n", + " pce_mframe_cnt (delta_time) uint32 ...\n", + " ph_id_channel (delta_time) uint8 ...\n", + " ph_id_count (delta_time) uint8 ...\n", + " ph_id_pulse (delta_time) uint8 ...\n", + " quality_ph (delta_time) int8 ...\n", + " signal_conf_ph (delta_time, ds_surf_type) int8 ...\n", + " weight_ph (delta_time) uint8 ...\n", + "Attributes:\n", + " Description: Contains arrays of the parameters for each received photon.\n", + " data_rate: Data are stored at the photon detection rate." + ] + }, + "execution_count": 15, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "%%time\n", + "\n", + "# this one is different! you can try this at home (cloud otpmized HDF5!)\n", + "\n", + "io_params ={\n", + " \"fsspec_params\": {\n", + " # \"skip_instance_cache\": True\n", + " \"cache_type\": \"blockcache\", # or \"first\" with enough space\n", + " \"block_size\": 8*1024*1024 # could be bigger\n", + " },\n", + " \"h5py_params\" : {\n", + " \"driver_kwds\": { # only recent versions of xarray and h5netcdf allow this correctly\n", + " \"page_buf_size\": 32*1024*1024, # this one only works in repacked files\n", + " \"rdcc_nbytes\": 8*1024*1024 # this one is to read the chunks \n", + " }\n", + "\n", + " }\n", + "}\n", + "ds = xr.open_dataset(fs.open(cloud_optimized, **io_params[\"fsspec_params\"]),\n", + " group=\"/gt1l/heights\",\n", + " engine=\"h5netcdf\",\n", + " **io_params[\"h5py_params\"])\n", + "ds" + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "id": "da959721-2f9d-4151-b361-6f9f38fa5b8c", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "CPU times: user 11 s, sys: 2.02 s, total: 13 s\n", + "Wall time: 1min 25s\n" + ] + }, + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "
<xarray.DataArray 'h_ph' ()>\n",
+       "array(1031.6101, dtype=float32)
" + ], + "text/plain": [ + "\n", + "array(1031.6101, dtype=float32)" + ] + }, + "execution_count": 16, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "%%time\n", + "\n", + "# takes about ~2 minutes\n", + "ds.h_ph.mean()" + ] + }, + { + "cell_type": "markdown", + "id": "35caf411-afe7-44f7-9264-5e7b892456d0", + "metadata": {}, + "source": [ + "
" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.12.1" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/notebooks/convert_h5dataframe2flatgeobuf.ipynb b/notebooks/data-wrangling/convert_h5dataframe2flatgeobuf.ipynb similarity index 100% rename from notebooks/convert_h5dataframe2flatgeobuf.ipynb rename to notebooks/data-wrangling/convert_h5dataframe2flatgeobuf.ipynb diff --git a/notebooks/example-list-test-files.ipynb b/notebooks/data-wrangling/example-list-test-files.ipynb similarity index 100% rename from notebooks/example-list-test-files.ipynb rename to notebooks/data-wrangling/example-list-test-files.ipynb diff --git a/notebooks/format-preprocessing-times.ipynb b/notebooks/data-wrangling/format-preprocessing-times.ipynb similarity index 100% rename from notebooks/format-preprocessing-times.ipynb rename to notebooks/data-wrangling/format-preprocessing-times.ipynb diff --git a/notebooks/fsspec-logs.ipynb b/notebooks/data-wrangling/fsspec-logs.ipynb similarity index 99% rename from notebooks/fsspec-logs.ipynb rename to notebooks/data-wrangling/fsspec-logs.ipynb index 96bfe63..87d7f05 100644 --- a/notebooks/fsspec-logs.ipynb +++ b/notebooks/data-wrangling/fsspec-logs.ipynb @@ -317,7 +317,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.10.13" + "version": "3.12.1" } }, "nbformat": 4, diff --git a/notebooks/h5coro_benchmarks.ipynb b/notebooks/data-wrangling/h5coro_benchmarks.ipynb similarity index 100% rename from notebooks/h5coro_benchmarks.ipynb rename to notebooks/data-wrangling/h5coro_benchmarks.ipynb diff --git a/notebooks/h5py_testing_original_repacked_with_subsetting.ipynb b/notebooks/data-wrangling/h5py_testing_original_repacked_with_subsetting.ipynb similarity index 100% rename from notebooks/h5py_testing_original_repacked_with_subsetting.ipynb rename to notebooks/data-wrangling/h5py_testing_original_repacked_with_subsetting.ipynb diff --git a/notebooks/kerchunker.ipynb b/notebooks/data-wrangling/kerchunker.ipynb similarity index 100% rename from notebooks/kerchunker.ipynb rename to notebooks/data-wrangling/kerchunker.ipynb diff --git a/notebooks/read-results.ipynb b/notebooks/data-wrangling/read-results.ipynb similarity index 100% rename from notebooks/read-results.ipynb rename to notebooks/data-wrangling/read-results.ipynb diff --git a/notebooks/data-wrangling/ros3vfd-log-info.ipynb b/notebooks/data-wrangling/ros3vfd-log-info.ipynb new file mode 100644 index 0000000..faa57d1 --- /dev/null +++ b/notebooks/data-wrangling/ros3vfd-log-info.ipynb @@ -0,0 +1,344 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "c1d4cad4-c84c-4104-981c-9eb0a20f75fd", + "metadata": {}, + "source": [ + "# ROS3 VFD Log Analysis Dashboard" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "f53c6f1c-e624-4952-a3e8-69302152da81", + "metadata": {}, + "outputs": [], + "source": [ + "from dataclasses import dataclass\n", + "import io\n", + "import re\n", + "import numpy as np\n", + "from bokeh.models import HoverTool\n", + "import holoviews as hv\n", + "import panel as pn\n", + "hv.extension('bokeh')\n", + "pn.extension()" + ] + }, + { + "cell_type": "markdown", + "id": "bc1a4585-45e7-48c7-a806-d7096f0f82bc", + "metadata": {}, + "source": [ + "## Log Parser\n", + "\n", + "The class that represents information of one HTTP range GET request:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "2cef30c0-71b2-405f-83d4-a440df748a52", + "metadata": {}, + "outputs": [], + "source": [ + "@dataclass(frozen=True)\n", + "class ByteRange:\n", + " start: int\n", + " end: int\n", + " filesize: int\n", + "\n", + " def __post_init__(self):\n", + " if self.start < 0 or self.end <= 0 or self.filesize <= 0:\n", + " raise ValueError('Start, end, and file size values must be positive integers')\n", + " elif self.end > self.filesize:\n", + " raise ValueError('End value must be smaller or equal to file size')\n", + " elif self.start > self.end:\n", + " raise ValueError('Start value must be smaller or equal to end value')\n", + "\n", + " @property\n", + " def size(self):\n", + " return self.end - self.start + 1\n", + "\n", + " def __len__(self):\n", + " return self.size" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "b205fe75-e4a3-4bb3-86ac-a24b630580f8", + "metadata": {}, + "outputs": [], + "source": [ + "def parse_fsspec_log(content: bytes) -> list[ByteRange]:\n", + " head_line = re.compile('read: 0 - ')\n", + " fsize_line = re.compile('FileSize: ([0-9]+)')\n", + " range_line = re.compile('\\s*(read: \\d+ - \\d+)')\n", + "\n", + " ranges = list()\n", + " with io.TextIOWrapper(io.BytesIO(content)) as logtxt:\n", + " for line in logtxt:\n", + " if head_line.match(line):\n", + " break\n", + " else:\n", + " raise RuntimeError('HEAD line not found in the log file')\n", + "\n", + " for line in logtxt:\n", + " match = fsize_line.match(line)\n", + " if match:\n", + " fsize = int(match.group(1))\n", + " break\n", + " else:\n", + " raise RuntimeError('FILESIZE line not found in the log file')\n", + "\n", + " for line in logtxt:\n", + " match = range_line.search(line)\n", + " if match:\n", + " range = ByteRange(start=int(match.group('start')), \n", + " end=int(match.group('end')),\n", + " filesize=fsize)\n", + " if range.size != int(match.group('size')):\n", + " raise ValueError(f'Reported size different for {match.group()}')\n", + " ranges.append(range)\n", + " \n", + " return ranges" + ] + }, + { + "cell_type": "markdown", + "id": "a5954181-f1d7-40f3-8594-3afe62bcd2aa", + "metadata": {}, + "source": [ + "Log file parser:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "a4e4e020-c058-4a6c-b2dd-ed12962ae785", + "metadata": {}, + "outputs": [], + "source": [ + "def parse_ros3vfd_log(content: bytes) -> list[ByteRange]:\n", + " head_line = re.compile('HEAD: Bytes 0 - ')\n", + " fsize_line = re.compile('FILESIZE: ([0-9]+)')\n", + " range_line = re.compile('GET: Bytes (?P[0-9]+) - (?P[0-9]+), Request Size: (?P[0-9]+)')\n", + "\n", + " ranges = list()\n", + " with io.TextIOWrapper(io.BytesIO(content)) as logtxt:\n", + " for line in logtxt:\n", + " if head_line.match(line):\n", + " break\n", + " else:\n", + " raise RuntimeError('HEAD line not found in the log file')\n", + "\n", + " for line in logtxt:\n", + " match = fsize_line.match(line)\n", + " if match:\n", + " fsize = int(match.group(1))\n", + " break\n", + " else:\n", + " raise RuntimeError('FILESIZE line not found in the log file')\n", + "\n", + " for line in logtxt:\n", + " match = range_line.search(line)\n", + " if match:\n", + " range = ByteRange(start=int(match.group('start')), \n", + " end=int(match.group('end')),\n", + " filesize=fsize)\n", + " if range.size != int(match.group('size')):\n", + " raise ValueError(f'Reported size different for {match.group()}')\n", + " ranges.append(range)\n", + " \n", + " return ranges" + ] + }, + { + "cell_type": "markdown", + "id": "502ba537-da68-4bc2-95bc-ccf73b1f3322", + "metadata": {}, + "source": [ + "## Dashboard\n", + "\n", + "Function for generating log stats and plots:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "8738d59c-3ff6-4d83-9ef2-4eeeb93ee851", + "metadata": {}, + "outputs": [], + "source": [ + "def plot_ros3vfd_log(from_file):\n", + " if from_file is None:\n", + " return\n", + " elif len(from_file) == 0:\n", + " return [pn.pane.Alert('ros3vfd log file empty.', alert_type='danger')]\n", + " try:\n", + " ranges = parse_ros3vfd_log(from_file)\n", + " except Exception as e:\n", + " return [pn.pane.Alert(f'Error: {str(e)}', alert_type='danger')]\n", + " if len(ranges) == 0:\n", + " return [pn.pane.Alert('No range `GET` info found.', alert_type='info')]\n", + " start = np.fromiter([r.start for r in ranges], dtype=np.uint64)\n", + " end = np.fromiter([r.end for r in ranges], dtype=np.uint64)\n", + " req_no = np.arange(len(ranges)) + 1\n", + " sizes = np.fromiter([r.size for r in ranges], np.uint64)\n", + " info = pn.pane.Markdown(f\"\"\"\n", + "# ros3vfd Log Information\n", + "\n", + "Log size: {len(from_file):,} bytes\n", + "\n", + "HDF5 file size: {ranges[0].filesize:,} bytes\n", + "\n", + "Number of range _GET_ requests: {len(ranges):,}\n", + "\n", + "Overall range _GET_ requests stats:\n", + "\n", + "* Smallest: {np.min(sizes):,} bytes
\n", + "* Median: {int(np.median(sizes)):,} bytes
\n", + "* Largest: {np.max(sizes):,} bytes\n", + "\n", + "Maximum file byte read: {end.max():,}\n", + "\n", + "Total of file content read: {sizes.sum():,} bytes\n", + "\n", + "Percentage of content read to file size: {100 * (sizes.sum() / ranges[0].filesize) :.2f} %\n", + "\"\"\")\n", + " data = dict(start=start, end=end, start_event=req_no, end_event=req_no)\n", + " max_offset_range = min(16_000_000, np.max(end))\n", + " req_range = np.where(end <= max_offset_range)[0]\n", + " if req_range.size == 0:\n", + " max_req_range = req_no[-1]\n", + " else:\n", + " max_req_range = req_no[np.where(end <= max_offset_range)[0][-1]] + 1\n", + " ros3plt = hv.Segments(\n", + " data, \n", + " [\n", + " hv.Dimension('start', label='File offset', range=(0, max_offset_range)),\n", + " hv.Dimension('start_event', label='Req. No.', range=(0, max_req_range)), \n", + " 'end', \n", + " 'end_event'\n", + " ]\n", + " )\n", + " hvrtip = HoverTool(\n", + " tooltips = [\n", + " ('req no', '@start_event'),\n", + " ('start byte', '@start'),\n", + " ('end byte', '@end')\n", + " ]\n", + " )\n", + " ros3plt.opts(width=700, height=600, invert_axes=True, color='blue', \n", + " line_width=3, tools=[hvrtip])\n", + " size_hist = hv.Histogram(np.histogram(sizes, bins=512))\n", + " size_hist.opts(color='blue', line_color=None, tools=['hover'],\n", + " xlabel='Size (bytes)', ylabel='Number of requests')\n", + " \n", + " return [pn.Row(info, size_hist), ros3plt]" + ] + }, + { + "cell_type": "markdown", + "id": "d1578b9a-1a8f-43cb-9902-96e21c83cf3a", + "metadata": {}, + "source": [ + "### Dashboard Components" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "553c70f5-6f78-4f28-9d6b-7998cbbc7ec2", + "metadata": {}, + "outputs": [], + "source": [ + "log_file = pn.widgets.FileInput()\n", + "upld_form = pn.Row(\n", + " pn.pane.Markdown('Please select a ros3vfd log file (limit 10MB):'),\n", + " log_file\n", + ")\n", + "res = pn.Column()\n", + "app = pn.WidgetBox(upld_form, res)" + ] + }, + { + "cell_type": "markdown", + "id": "15126686-d315-4ae4-8666-054dd6127ba5", + "metadata": {}, + "source": [ + "Callback function for interactive log processing invocation:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "322e3372-be21-4d65-b97e-9db5a552aaf0", + "metadata": {}, + "outputs": [], + "source": [ + "def callback(value):\n", + " res.objects = plot_ros3vfd_log(value)" + ] + }, + { + "cell_type": "markdown", + "id": "dd2b2c83-1d2e-4db4-a23f-db9ad5d6f84d", + "metadata": {}, + "source": [ + "Register callback with the appropriate dashboard object:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "580279df-8b79-426f-bb22-002f117f200a", + "metadata": {}, + "outputs": [], + "source": [ + "log_file.param.watch_values(callback, ['value']);" + ] + }, + { + "cell_type": "markdown", + "id": "a3f35ce7-0698-4b02-a95e-75107138a29a", + "metadata": {}, + "source": [ + "Run the dashboard:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "56c63583-922a-4b67-8cb0-8f3751ed7c28", + "metadata": {}, + "outputs": [], + "source": [ + "app.servable()" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.12.1" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/notebooks/run-tests.ipynb b/notebooks/data-wrangling/run-tests.ipynb similarity index 100% rename from notebooks/run-tests.ipynb rename to notebooks/data-wrangling/run-tests.ipynb diff --git a/notebooks/sliderule2geoparquet.ipynb b/notebooks/data-wrangling/sliderule2geoparquet.ipynb similarity index 100% rename from notebooks/sliderule2geoparquet.ipynb rename to notebooks/data-wrangling/sliderule2geoparquet.ipynb diff --git a/notebooks/xarray-h5coro-backend.ipynb b/notebooks/data-wrangling/xarray-h5coro-backend.ipynb similarity index 100% rename from notebooks/xarray-h5coro-backend.ipynb rename to notebooks/data-wrangling/xarray-h5coro-backend.ipynb diff --git a/notebooks/plot_benchmark_results.ipynb b/notebooks/plot_benchmark_results.ipynb new file mode 100644 index 0000000..a2195b3 --- /dev/null +++ b/notebooks/plot_benchmark_results.ipynb @@ -0,0 +1,449 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "e2d85d97-bd23-4af1-b302-1ab55921a30b", + "metadata": { + "user_expressions": [] + }, + "source": [ + "# Plot Benchmarking Results\n", + "\n", + "Plots the results in `benchmarks.csv`" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "id": "a7c05ac8-7256-42f7-a351-4b498da62ffc", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "import re\n", + "\n", + "import pandas as pd\n", + "import numpy as np\n", + "\n", + "import matplotlib.pyplot as plt\n", + "import seaborn as sns" + ] + }, + { + "cell_type": "markdown", + "id": "7804d7bc-a01e-46bb-807b-fb5081616d8f", + "metadata": { + "user_expressions": [] + }, + "source": [ + "## Read `benchmarks.csv`\n", + "\n", + "This file is generated using [portable-full-comparison.ipynb](https://hub.cryointhecloud.com/hub/user-redirect/lab/tree/h5cloud/notebooks/portable-full-comparison.ipynb)" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "1fa7d33b-ef5f-419d-a0a6-8213e075ede5", + "metadata": { + "tags": [] + }, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
tooldatasetcloud-awareformatfiletimemeansizeproduct
0h5pyATL03-1GBnooriginals3://nasa-cryo-persistent/h5cloud/atl03/averag...2.843794386.067381GBATL03
1h5pyATL03-1GBnooptimizeds3://nasa-cryo-persistent/h5cloud/atl03/averag...4.157145386.067381GBATL03
2h5pyATL03-7GBnooriginals3://nasa-cryo-persistent/h5cloud/atl03/big/or...6.9494101035.16317GBATL03
3h5pyATL03-7GBnooptimizeds3://nasa-cryo-persistent/h5cloud/atl03/big/re...13.6586011035.16317GBATL03
4h5pyATL03-2GBnooriginals3://nasa-cryo-persistent/h5cloud/atl03/big/or...1.4053022049.75542GBATL03
\n", + "
" + ], + "text/plain": [ + " tool dataset cloud-aware format \\\n", + "0 h5py ATL03-1GB no original \n", + "1 h5py ATL03-1GB no optimized \n", + "2 h5py ATL03-7GB no original \n", + "3 h5py ATL03-7GB no optimized \n", + "4 h5py ATL03-2GB no original \n", + "\n", + " file time mean \\\n", + "0 s3://nasa-cryo-persistent/h5cloud/atl03/averag... 2.843794 386.06738 \n", + "1 s3://nasa-cryo-persistent/h5cloud/atl03/averag... 4.157145 386.06738 \n", + "2 s3://nasa-cryo-persistent/h5cloud/atl03/big/or... 6.949410 1035.1631 \n", + "3 s3://nasa-cryo-persistent/h5cloud/atl03/big/re... 13.658601 1035.1631 \n", + "4 s3://nasa-cryo-persistent/h5cloud/atl03/big/or... 1.405302 2049.7554 \n", + "\n", + " size product \n", + "0 1GB ATL03 \n", + "1 1GB ATL03 \n", + "2 7GB ATL03 \n", + "3 7GB ATL03 \n", + "4 2GB ATL03 " + ] + }, + "execution_count": 4, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "df = pd.read_csv(\"benchmarks.csv\", index_col=0)\n", + "df.head()" + ] + }, + { + "cell_type": "markdown", + "id": "8e4e2660-437b-48f7-896f-795e93ae1644", + "metadata": { + "user_expressions": [] + }, + "source": [ + "## Reformat data for plotting" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "5c69cca4-430f-4552-b9e7-88bd07deea33", + "metadata": { + "tags": [] + }, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
formatoptimizedoriginal
cloud-awarenoyesnoyes
toolsize
h5coro1GB4.286047NaN4.562052NaN
2GB1.812065NaN3.110127NaN
7GB11.794484NaN14.072926NaN
h5py1GB4.1571451.9302232.8437941.861841
2GB1.0851730.8633281.4053021.260476
7GB13.6586015.7583516.9494106.602762
kerchunk7GB8.813402NaN10.746919NaN
xarray1GB10.2586752.54299046.50308742.182489
2GB18.1096542.15657247.50670740.316141
7GB81.6751826.67192362.89623748.714591
\n", + "
" + ], + "text/plain": [ + "format optimized original \n", + "cloud-aware no yes no yes\n", + "tool size \n", + "h5coro 1GB 4.286047 NaN 4.562052 NaN\n", + " 2GB 1.812065 NaN 3.110127 NaN\n", + " 7GB 11.794484 NaN 14.072926 NaN\n", + "h5py 1GB 4.157145 1.930223 2.843794 1.861841\n", + " 2GB 1.085173 0.863328 1.405302 1.260476\n", + " 7GB 13.658601 5.758351 6.949410 6.602762\n", + "kerchunk 7GB 8.813402 NaN 10.746919 NaN\n", + "xarray 1GB 10.258675 2.542990 46.503087 42.182489\n", + " 2GB 18.109654 2.156572 47.506707 40.316141\n", + " 7GB 81.675182 6.671923 62.896237 48.714591" + ] + }, + "execution_count": 5, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "pivot_df = df.pivot_table(index=[\"tool\", \"size\"], columns=[\"format\", \"cloud-aware\"], values=\"time\", aggfunc=\"mean\")\n", + "pivot_df" + ] + }, + { + "cell_type": "markdown", + "id": "97f6a3dc-e5eb-46e7-ac46-611a08143d07", + "metadata": { + "user_expressions": [] + }, + "source": [ + "## Plot results" + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "id": "33602bbd-41c0-4133-bab5-76f45e9fe1a5", + "metadata": { + "tags": [] + }, + "outputs": [ + { + "data": { + "image/png": "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u7cGDBhgt+78+fOSsm6qm/1Gt44kJiYqICDA1g+59VflypULXCMAIEupDedXr16tF154QWazWc2bN1f79u114cIF7dq1Szt27NCYMWP04osv2u0zffp0zZ07V56enurUqZNSU1MVFhambdu2acaMGerevXsxPRoAAAAAAJAfDRo00MGDBxUVFaUHHnggz22joqIkSXXq1LnpzWOt8nNFfkk1f/58/fbbb2rRooUmTJiQY31mZqYkqUaNGmrWrFmex3J3d7f7/car5gEAhVcqw/mMjAxNmTJFZrNZH330kXr37m1bFx4erscff1xffvml/v73v6tWrVqSpO3bt2vu3Lny8/PT4sWLVadOHdv2w4cP18SJE9W+fXv5+voWx0MCAAAAAAD50L59e61YsUKbNm3S2LFj8wzd169fb9vnRrGxsapXr16O5RcuXJAkVapUyUUVF40tW7YoNDRUVapU0dtvv50jXJf+ukq/bt26N73RrFXFihUl/XXV/Y2s/QUAKDhjcRfgjJiYGMXFxalevXp2wbwktW7dWvfee68sFosOHjxoWx4aGipJGjt2rC2Yt24/aNAgXb16VcuWLSuS+gEAAAAAgHM6dOigmjVr6tKlS1q0aFGu2+3bt0+bNm2Su7u7+vbtm2P9b7/9lmPZiRMndOzYMXl5edkF99ag23rleUkTHR2tqVOnqkyZMnr33XcdzicvSY0bN5a3t7fCw8N1/fr1fB27RYsWkqTNmzfn+FZBRkaGNm3aVKjaAeBOVirD+fzeLd16MkpNTdX27dslyeFc9NZlGzZscE2BAAAAAADgljAajXrllVdkMpkUGhqqRYsW5QjNd+7cqUmTJsliseipp56y3Xw1u+XLl+vo0aO235OTkzVz5kxZLBb16tXLLnuwXj1+6tSpW/SonBcfH69XX31VqampevXVV9WwYcNct/Xw8NDAgQN17do1vfHGGw6vho+Ojrb74KJ169aqWbOmTp48qcWLF9ttu2DBAsXGxrruwQDAHaZUTmtTs2ZN1axZUzExMfr5559zTGuzdetW1ahRQ23btpWUdaV9Wlqa/P39VaVKlRzHa9q0qaS/5qIDAAAAAKC0aejtWdwl5MrVtTVr1kxTp07VlClT9MUXX2jp0qVq2rSpPDw8FBMToxMnTshoNGrUqFF67LHHHB6je/fuGjt2rFq3bi0fHx/t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ogoVKmj//v1auXKl3bply5bp8OHDCggIuOnUIrdSQECAevbsqdOnT2vatGkO51Y/ePCgduzYYfs9JCRE5cuX1++//64NGzbYlmdmZurTTz+1zWdeWP369ZOUNe1T9ivKk5KSNGPGDFksFj300EMOv22Sl/nz59sFyPHx8fr8888lSY888ohtufVGw1u2bLHrl6tXr+r999+33Ri2oGrUqKFr167l+JBg6dKlCg8Pd+qYBTVixAgZjUZNnz7dduPi7C5duqTly5fbLbvZ63T48OEym8164403HIbmZ86c0c8//1ygOn/44QeHYf/OnTslueb+Bs5gWhsAAAAAAFCqNGvWTFOnTtWUKVP0xRdfaOnSpWratKk8PDwUExOjEydOyGg0atSoUXrsscdyPU737t01duxYtW7dWj4+Ptq3b5/i4uJUp04djRw50m7bjh07av78+frf//6n3bt3y9fXV5L09NNP234uLj/++KNiY2Pl7++vrVu3avPmzTm2adGihR588EFJ0rBhw3TixAmtX79ew4cPV6NGjVS5cmUlJCTo3LlzOnfunDp16qQuXbpI+usDkRdeeEGfffaZ1q9fr1q1aun48eM6efKk+vTpo1WrVhW47latWungwYNKS0vLMae8dd55qWBT2lStWlX169dXVFSUnnnmGdWpU0dGo1GdOnVSp06dClxjSVG2bFlNmjRJr776qv7zn/9o5cqVtg9Mjh49Kk9PT73++utFNr98bv75z3/q3LlzWr9+vbZv364GDRqoYsWKiouL05kzZ3Tp0iU9+uij6tChg6SsecD/7//+T5MnT9bkyZO1bNkyBQYG6o8//lBCQoK6deuW67cFCiIkJEQPPfSQVq5cqZEjR6p169by9PRURESE4uPj1bRp0xyv+ZsJDAxUvXr1NHLkSLVp00Ymk0l79+7VtWvX1Lp1a9sHAlLWN1batm2r3bt3a9iwYXZTN/n6+qpTp07atm1bgR/X0KFDbe+FK1asUEBAgKKjo3Xy5Ek99thjWrp0aYGPWVCtWrXSc889p1mzZumf//yn6tevr+rVqystLU2xsbE6ceKEypYta9cfN3s/feCBB3T8+HF98803evLJJ9WwYUNVrVpV169fV2xsrE6ePKn69eurd+/e+a7zxx9/1EcffaQ6deqoVq1acnNz06lTp3Ts2DGVKVPGdgV9USOc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V16RyWRSaGioFi1apMzMTLttdu7cqUmTJsliseipp55S1apVC93uQw89JCkrX7h8+bJt+YULFzR//vxCH/9matSoIemv3MNq48aNWrt27S1vHwDgeqXyyvncJCUl6bfffpMkPfzww7blqamp2r59uySpZ8+eOfbr2bOnFixYoA0bNmjUqFFFUywAAAAAAC5kquxV3CXkytW1NWvWTFOnTtWUKVP0xRdfaOnSpWratKk8PDwUExOjEydOyGg0atSoUXrsscdc0uYDDzygTZs2KSwsTCNGjFCbNm1kNpu1d+9etW7dWmaz2S60d7XBgwdr165d+uKLL7Rx40bVrFlTp0+fVlRUlAYOHKjFixffsrYBALfGbRXO//LLL0pKSlLTpk3VsGFD2/KYmBilpaXJ399fVapUybFf06ZNJf11oxgAAAAAAEoLs9ksc6ZZFQc1Lu5S8mTONMtsNrvseHfffbe+/vprfffdd9q+fbvCw8OVkZGhihUrqk+fPurfv7/q16/vsvYMBoMmT56sb775RmvWrNGOHTtUsWJFDRgwQMOGDdODDz6o8uXLu6y9GwUHB+vjjz/WnDlzdPToUZ0+fVr16tXTlClT1LBhQ8J5ACiFbqtwfuXKlZKkRx55xG659cYojoJ5SfLy8lL58uWVkJCga9euycfH59YWCgAAAACAi5jNZl28dNFuateSyGx2bTgvSX5+fnryySf15JNP5nufm4XYVatWzXUOd3d3d40YMSLHPPeRkZFKT09XgwYN8nWsXr16qVevXrnWMGPGDIfLmzVrpg8//NDhOmfaAQAUr9smnL948aK2b98uNzc39enTx25dUlKSJMnT0zPX/cuWLavExEQlJSU5Fc4bDIYC73M7s/YH/YKCYNzAGYwbx7L3h8FgoH9uwLiBsxg7pYOrn5/Cvo8ybhzjXJW3go6bWxF8I6eYmBjVqlVLJtNfccq5c+f00UcfSZK6detWXKUBcLEbz1OA1c3+binIeLltwvmffvpJmZmZuu+++xQQEGC3zmKxSMq7Y6zbOCMwMFDe3t5O7387CwwMLO4SUAoxbuAMxo2969ev237mPJU7xg2cxdi5s1SqVMklx2Hc2ONclT+VKlVSQkKC3N3d5e7uXtzl3PE+//xzHT58WA0aNFCFChV04cIFHT58WGlpabrnnnvUu3fvEhHiMVbgDMaNvew3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L+jKK69Uhw4d1L17dz3++OPavXt3mf0+/PBDJSYmqmvXrsrLyyu3fafTqSuvvLJeTcoCAPXJ9OnTlZiYqPbt22vz5s1ut1m5cqWSkpKUmJioTz/91LX80KFD+uCDDzRs2DD17dtXF110kTp16qTrrrtOL774og4dOlTucUtO2FVQUKD33ntPycnJ6tKlS6mJr4pzyIIFC5STk6M33nhDN9xwgzp16qTExET98ssvkqSCggKtXr1a48ePV3Jysnr06KH27dvr0ksv1X333afPPvtMpmmW6cerr76qxMREXX/99RWeJ4fD4TrmggULKj2vAADv8vV1UrFNmzYpJSVF11xzjS666CJ17txZ1113nVJSUrRq1Sq3+5w8eVKTJ0/WzTffrM6dO6tjx47q06ePnnvuOR04cKDcY5W8Njt27JgmTJigvn376sILL1RiYmKpbfPy8jRz5kwNHDhQf/zjH9WhQwf17t1bTz75pH744QevngOgvli4cKHatWunxMREvfbaa6XWrVmzRqNHj1avXr3UoUMHXXzxxRowYID++c9/Kjc31217Tz31lBITE/XUU0/JNE3NmzdPt99+u7p27er2M+Ovv/6ql19+WTfddJMuvvhidezYUVdffbWGDx+uRYsWVVhbcTgcev3119WvXz917NhRXbt21QMPPKBNmza53X7NmjWu95SKlDfp7O/3379/v1JSUtSzZ0+1b99eV1xxhf7yl7/oyJEjFbZfnl9++UV9+/ZVYmKibr75Zh09etSjdqqDYW0AVKqoqEgPPfSQvvrqK1mtVoWFhSknJ0cbN27Uxo0btX//fj3yyCO1cuxffvlFjz/+uDIzMxUWFiar1aqjR4/qs88+0/LlyzV58mRdccUVru1vvPFGvfLKKzp+/LiWLl2qm266yW27q1at0sGDBxUSEqIBAwbUSt8BAJ67//77lZaWptTUVI0ePVqLFi1SVFSUa31GRobrgqN///664YYbXOvGjh2rtWvXuv7foEED5eTkaPfu3dq9e7cWLlyoadOmqUuXLuUePy8vT4MGDVJ6erqsVqsiIyPdbnf8+HElJydr3759stlsCg8PL7X++++/17333uv6v91ul81mU1ZWllatWqVVq1Zp+fLlev3112UY/3tu5rbbbtM777yjn376SevXry+3r59++qlyc3PVoEEDXXvtteW+HgCA9/njOqmoqEgTJkwoNSRORESEnE6nK88tX75c69evL7Xfrl27NHToUB0+fFiSFBoaKqvVqv3792v//v1asGCBXn31VfXt27fcY//8888aPXq0jh496tq/pCNHjmjo0KH68ccfJZ0ZVz8sLEyHDh3S4sWL9emnn+rpp5/WoEGDvHU6gDpv+vTpmjhxogzD0LPPPqu77rpLklRYWKjnn39e8+bNc20bERGhU6dOacuWLdqyZYs++eQTvfvuuzr77LPdtm2aph599FEtW7ZMhmGoQYMGpT5vStKiRYs0btw4VwG++O/2wIEDOnDggL7++mslJiaqTZs2ZdrPzMxUcnKy9u/fr9DQUBmGoePHj+ubb77R6tWrNXXqVF1++eXeOlVlfPfddxo+fLhyc3MVGRkp0zR15MgRzZs3TytXrtT8+fOVkJBQ5fZ27NihoUOHKjMzU5deeqkmT55c6vqjtvDkPIBK/etf/9LatWv10ksvacOGDdqwYYNWrlyp3r17S5KmTp2qffv2ud13165d+tOf/qSOHTuqU6dO6tu3r/7yl79o+/btVTr2hAkTZLPZ9N5772njxo1KT0/XvHnz1Lp1a+Xl5WnUqFGuD5iSFBkZ6SrQlExiv1e87oorrtBZZ51Vpb4AAHzHYrHo5ZdfVmxsrA4cOKDnnnvOtc40TY0dO1ZZWVlq0aKFxo0bV2rfVq1a6fHHH9eSJUu0adMmrV+/Xlu2bNG8efN0+eWX6+TJkxo1apROnz5d7vE/+OAD7dy5UxMmTNCGDRu0du1afffdd2We8pk0aZJycnI0efJkpaena926dVq5cqViY2MlSWFhYfrTn/6k6dOna/Xq1dq8ebPS09O1Zs0aPfPMM4qKitLSpUs1Z86cUu2ee+656tGjh6Sq5bObbrqpzI0BAEDt8sd10muvveYqzP/5z3/W0qVLlZ6ero0bNyo1NVVvvfVWmWKYw+HQgw8+qMOHDyshIUHTp0/Xxo0b9f3332vx4sW66KKLlJ+fryeeeEI7duwo99h///vf1aBBA82cOdO1/9KlSyWduWkwcuRI/fjjj2rQoIFeeeUVff/991q/fr3+85//qHfv3nI6nRo/frxWrlxZ1VMM1FumaWr8+PGaOHGi7Ha7XnvtNVdhXpL+8Y9/aN68eYqLi9Nzzz2nNWvWKD09XZs2bdKsWbPUtm1b7d27VyNHjpTT6XR7jC+//FJfffWVxo4dq3Xr1mnt2rVav3696zPmypUr9dRTTykvL0+dO3fWBx98oM2bN2v9+vXasGGDPvjgA916662y2Wxu23/hhRdks9n0/vvvl6rXnHfeeSooKNBzzz1Xbt+84ZFHHlG3bt20ZMkSff/990pPT9frr7+uyMhIZWRkaOLEiVVu67vvvtOdd96pzMxMXX/99Zo+fbpPCvMSxXkAVXDixAnX1x/DwsIkSU2bNtWbb76pJk2ayOl06osvvnC772+//abdu3crPDxc+fn52rdvn+bNm6fk5GS9/vrrlR779OnTeuedd9S9e3dZLBZJUseOHTVz5kzFxMTI4XDo7bffLrXP7bffLklat26d26Fvjh49qhUrVkiSbr311qqfCACAT8XHx2vChAmyWCz67LPPtHDhQknSP//5T6Wmpspms2nixIllnmofN26chg0bpvPPP9+Vt6xWqzp27Ki3335biYmJysjI0LJly8o9dm5uriZOnKjk5GRXG40aNVJMTEyp7fLy8jR9+nRdc801rguXpk2bugrlF154oSZOnKiePXsqLi7OlctiYmI0ePBgvfjii5LkdlLAgQMHSpKWLl2q7OzsMuu3bdumbdu2SSKfAYA/+Po6ae/evXrvvfckSUOHDtXf//53nXfeea71sbGxuvrqq8vs/69//Uu//PKLbDab3nnnHfXs2dP19GxSUpLrydv8/PwKr9EMw9DMmTN16aWXuvYvPv6yZctcw1i8/vrruvHGG2W32yWdueE8efJkXXjhhZLODN0GBLP8/HyNHj1as2fPVoMGDfTOO++U+gbkjz/+qNmzZys8PFzvvfee7rjjDtdnUJvNpq5du2r27Nlq2rSptm3bpq+//trtcXJzc/XUU0/p3nvvdRWaIyMj1aRJExUWFuqFF16QaZq6+OKL9f7776tLly6uv+2oqCh16dJFf/vb33TBBRe4bT8kJESzZs1St27dZBiGLBaLOnbsqDfeeEOSdPDgQaWnp3vrtJWRlJSkt956S+eff76kM99Qve666zRq1ChJZ96XCgsLK21nyZIlGjp0qBwOh+6++27XDRNfoTgPoFKdO3dWt27dyiy32+2uO647d+4sta5FixYaM2aMli5dqs2bN7vu8r777rtq166dTNPUtGnTXB8uy9OvXz/XG21JsbGxrqLFkiVLSq1LSkpSp06dJLl/2nDBggUqKChQ06ZN1bNnzwqPDwDwr549e+qee+6RdObpnMWLF+vNN9+UJI0aNUodOnSoVnshISGuJwo3bNhQ7natWrXSlVdeWWl7l19+udq2bVutPpTUq1cvSWeGCsjIyCi1rnfv3jrrrLN0+vRpt2MXz507V5JcY84DAHzL19dJixYtktPpVExMTLWGyym+QdC3b1+1bt26zPqoqCgNHTpUkvTtt9/q5MmTbtu56aab1LRpU7friq/JOnXq5HYYC6vVqoceekjSmcLj788LECwcDoeGDh2qJUuWKD4+XnPmzFHXrl1LbTN//nyZpqmePXuW+xkvKipKV199tSTpv//9r9ttGjZsqNtuu83tujVr1rjmSEpJSfGoGH3rrbe6vi1aUmJios455xxJZd8DvenBBx8sM0yPJF111VWSzjzsuX///grbmDVrlkaPHq3CwkI9/vjjevrpp10P0/gKY84DqFTxEw7uNGnSRNKZp0ZKuvHGG8tsW/wh9Y9//KPuvPNObdmyRZMmTdItt9yiBg0auG3f3YfdkuumTZum48eP68CBAzr33HNd6wYOHKj09HQtWrRIo0ePdiUa0zQ1f/58SdKAAQMUEhJSbvsAgMAwevRorV27Vtu2bdOTTz4pSerRo0epsdx/b/369Zo/f742btyoI0eOuJ0wq6KJojp37lylvlVlO4fDoY8++kjffPONdu/erZMnT6qgoMBtf4rzqiTXvCiTJk3Sxx9/XGqM3tzcXH3++eeSeGoeAPzF19dJ33//vSSpe/fuCg0NrVIf8/PzXcWxSy+9tNztunfvLklyOp3atm2b2+uwinLe1q1bKz1Gt27dFBISoqKiIm3dupUbywg6mZmZuuuuu/TDDz+oZcuWevfdd11F7JKKHyD59ttvXX+b7hR/vj106JDb9R06dCi36F78RHt8fHy1H3YpVtl74C+//FLmPdCbOnbsWO6xix0/frzc/SdOnKjp06fLarXqxRdfVP/+/b3cw6rhyXkAlSpvEjxJrkmAqvJVoWKhoaEaPXq0pDPJJC0trdxtK5q8o+QbblZWVql11157rWJiYvTbb79p+fLlruXfffed9u/fz0SwAFCH2O12vfTSS67/N2jQQC+99FK5T7W88soruvPOO7Vw4ULt3btXeXl5atiwoeLi4hQXF6eIiAhJcluwL9a4ceMq9a2y7fbu3avrr79er7zyitatW6esrCxZrVY1btzY1Z9ip06dKrP/LbfcIqvVqh9//FEbN250Lf/888/lcDgUHR3NRLAA4Ce+vk46evSoJKlZs2ZVbvPEiRMqKiqSVPG1Vckn4n9/bVXM3ROyxY4dO1bpMUJDQ9WoUaNS2wPBZO7cufrhhx8UGhqqmTNnui3MS3J9mzI3N1dHjx4t91/xZ9ny5lGq6HNqZmampOq9n/yet98Dq6u8MeFLTlZd3vEPHjyo6dOnSzrzIJC/CvMSxXkAfnLRRRe5fj5w4EC523n6daLQ0FDdfPPNkv73tX9J+vjjjyUxESwA1DXF79/SmSfRf/jhB7fbrV69Wu+8844k6Y477tCnn36qLVu2aO3atVq9erVWr16tu+++u9LjVfWbVZVtl5KSosOHD+vss8/WG2+8oTVr1mjjxo1KS0vT6tWr9e2337q2NU2zzP4JCQmu4XVK5rPiYdtuvPFGJoIFgHqkKtdJnl4jVbRfVdp0N3yEJ+1UZzugPundu7caNGigvLw8paSkuH0wQ5JrEtXHH39cO3furPSfu7mLpKp9ng3Wv8X4+HjXN32mTp3qmjPDHyjOAwhohw8fLnddybF53d0RHjhwoCwWi9auXav9+/crKyvL9RR9eeOuAQACz4oVK1wXHYmJiTJNU0899ZTrCcKSiod66dGjh5577jm1bt26zIWJu/1qw6+//ur6yvBrr72mfv36lZlQtip9KZ5j5YsvvpDD4dDOnTtdFxAMaQMAwSM+Pl7SmSc+q6phw4auPPjrr7+Wu13JdVX99lhJxU/VV3SMvLw81xATnhwDqOvatWunGTNmqGHDhkpLS9OwYcPcfpOz+JuVP/74Y631pfj9pHjceV8p+bk8Ly/P7TblzXvhTXa7XdOmTVOPHj108uRJ3XvvvbU6eW1FKM4D8IuSdyXL+yqXdGaSkvJ89913kqSYmJhS480Xa9mypbp16ybTNPXxxx9r8eLFrolgr7jiihr0HgDgKxkZGUpJSZEkJScn64MPPtDZZ5+tY8eOaezYsWWeNi++qVveJK2mabryR20rWaAorz+pqamVtnPZZZepRYsWOnXqlP7973+7nppnIlgAqH8quk7q1KmTpDPfEiuvqPV7drvdlSsqyn/F+cgwDLVr165afZak9u3bV3qMNWvWuIaY8HSMa6Cu69Chg2bOnKmYmBitXbtW999/v3JyckptUzy/w8qVK8us85biYxw9elRbtmyplWO407BhQ9fP5d3M89VT7GFhYZoyZYquuOIKORwO3Xfffa7x/n2J4jwAr3P3tfyS8vPz9frrr0uSIiIiKpw0aOnSpdqzZ0+Z5VlZWa6v91c01m7x04YLFy50bc9EsABQNzidTj355JP67bff1LJlSz377LNq0KCBJk6cKKvVqlWrVmnGjBml9ikee3LHjh1u2/zwww8rHE7Nm0pO4ueuPw6HQ1OnTq20HYvF4vrG14cffqh///vfknhqHgDqmppeJyUnJyskJETHjx/Xm2++WeXjXnfddZKkZcuWuX0SNycnxzUkXM+ePUvlr+oeIz09XatWrSqzvrCwUFOmTJEktW7dWq1bt672MYD6om3btnr//ffVqFEjrV+/XkOHDpXD4XCtv/XWW2WxWJSdna2XX365wrYKCgo8KuB37drV9ZDjhAkTlJ+fX+02PNGyZUuFhYVJkr788ssy651Op95++22f9EU6MyTyW2+9pd69eysnJ0dDhw7VunXrfHZ8ieI8gFqwbt063XPPPVq8eHGpYWkKCgqUlpamO+64w3UndMSIEYqOji63rdDQUA0dOlSpqamuD7ObN2/WkCFD9NtvvykyMlLDhg0rd/+rr75a8fHxOnbsmPbu3ctEsABQh7zzzjtKS0uTzWbTxIkTXRO5durUSQ899JCkM8PFbNu2zbXP5ZdfLkn69ttv9dZbb7m+Kpydna1p06Zp/PjxZYaWqS0XXHCBa5Ktp59+Wlu3bnWtS09P1+DBg3XixIkqtZWcnCy73a4ff/xRJ06cUHR0tKsQAgCoG2p6ndSiRQvdd999ks7kyGeeeUb79u1zrc/KytKSJUtcObLY7bffrnPOOUcFBQW6//77tXLlSteY1jt37tR9992nX375RTabTY899phHr61v37668MILJUmPPfaYPv30UxUUFEg6M3b+yJEjXUNGPPHEEx4dA6hPkpKSNGvWLMXGxur777/Xfffd5yrQt2nTxjVH0kcffaRHHnlEP/zwg6smUlRUpB07duitt97SNddcU+5cTBUJCQnRs88+K4vFog0bNuiee+7R+vXrXe8NDodDa9as0RNPPKGffvrJS69astls6tOnjyRp2rRpWrJkievGwJ49e/TQQw9p586dXjteVdjtdr355pu66qqrlJubq2HDhvnsm7aSZK18EwCoHtM0lZaWprS0NElnvioUHh4uh8Ph+oBmGIaGDRum+++/v8K2nnrqKf3f//2fhgwZovDwcFksFlehxW6367XXXqtwdnGr1aoBAwa4nkxkIlgAqBs2b97seipw1KhRrq/LF3vwwQeVlpamtWvXavTo0Vq4cKEiIiLUv39/LVq0SOvXr9ebb76pSZMmKTo6WidPnpTT6VSvXr3Upk2bKj2xXlMWi0Xjxo3Tww8/rF27dunPf/6za/LWU6dOKTw8XFOnTtU999xTaVuNGjVSv379XE/N33jjja6njgAAdYM3rpMee+wx5eTk6IMPPtD8+fM1f/58RUREyDRN1+SSv3/yPSoqSlOnTtXQoUN1+PBhDRs2TKGhobLZbK5ioN1u1yuvvKKkpCSPXltISIgmTZqk++67T7t27dITTzyhlJQUhYeHKzs72/XaUlJS1LNnT4+OAdQ3rVu31qxZs3T33Xdr48aNGjJkiN59911FR0frySeflGmaev/997Vs2TItW7ZMoaGhrveM4iGiJM8nde3Zs6deeuklPfvss9qwYYPuvPNO2e12hYWFuf5uJbluCnrL448/ru+++04ZGRkaNWqUbDabQkND5XA4FBkZqWnTpmnQoEFePWZl7Ha73njjDY0ePVpffvmlHnjgAU2bNq3CkR68hSfnAXhd69atNXbsWPXt29f1laWTJ08qNDRUSUlJuuuuu7Ro0SKNGjWq0rbOPfdcLVy4UHfeeacaN26sgoICxcbG6k9/+pMWLVqkXr16VdpGv379XD8zESwABD6Hw6HRo0eroKBA3bt317333ltmG8Mw9MorrygmJkb79u3T3/72N0lnnsZ577339PDDD6tly5ayWq0yTVMdO3bU888/r6lTp/p0aLPevXtrzpw56tWrl6Kjo1VYWKhGjRopOTlZCxYsqNYH/pL5jCFtAKDu8cZ1UkhIiMaNG6d//etfuuGGG9SsWTMVFhbKZrOpVatWGjBggCZNmuT22J9//rlGjhypNm3aKCQkRPn5+WrevLkGDhyozz//vFSe8URCQoI++eQTpaSk6KKLLlJYWJhOnTqls846SzfddJMWLFigwYMH1+gYQH1zwQUXaNasWYqPj9fmzZt1zz336MSJEwoJCdHTTz+thQsX6rbbbtN5552nkJAQORwORUdHq3Pnzho5cqQWL16siy++2OPj9+/fX1988YXuvvtuXXDBBbJarSooKFDz5s119dVX6+WXX9b555/vxVcsNW3aVPPmzdMtt9yihIQESXI9ZLNw4UJdcsklXj1eVdlsNr3++uu69tprdfr0aT3wwANuh+nyNotZ2aBncCsnJ8c1pmnxnR0A3lM8adGsWbPUtWvXGrX13nvv6R//+IeaNm2qr7/+mvHmERTIU0D987e//U1z5sxRp06d9NFHH/m7O0CNkasAAIGMPAVPVSd2GNbGQxEREerevbvrZwCBqaioSB9++KGkM08ZUphHsCBPAfWLw+HQokWLJP1vsnOgriNXAQACGXkKnqpO7FCc95DFYtF///tf188AAo/T6dSbb76pn3/+WREREbr99tv93SXAZ8hTQP2Rn5+vF198UQ6HQ2eddRYTwaLeIFcBAAIZeQqeqk7sUJyvAf4wgcC0dOlSvfzyyzp+/LhycnIkSSNHjlTjxo393DPAt8hTQN02c+ZMzZo1S8eOHdPp06clnZko3W63+7lngPeQqwAAgYw8BU9VNXYozgOod3Jzc3Xw4EHZbDb94Q9/0F133aU777zT390CAKBaTp48qYMHDyo0NFRt2rTRsGHDajxZHwAAAIDAwYSwAAAAAAAAAAD4mOHvDgAAAAAAAAAAEGwozgMAAAAAAAAA4GMU5wEAAAAAAAAA8DGK8wAAAAAAAAAA+BjFeQAAAAAAAAAAfIziPAAAAAAAAAAAPkZxHgAAAAAAAAAAH6M4DwAAAAAAAACAj/0/DlcVRbYR9JIAAAAASUVORK5CYII=\n", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "# Set seaborn plot style\n", + "sns.set_style(\"darkgrid\", rc={'axes.facecolor': '0.9'})\n", + "\n", + "tool_order = [\"h5py\", \"xarray\", \"h5coro\", \"kerchunk\"]\n", + "# Create figure and axis to \"contain\" plot - allows customization via ax object\n", + "fig, ax = plt.subplots(figsize=(15,6), layout=\"constrained\")\n", + "\n", + "# Plot results\n", + "pivot_df.loc[tool_order,:].plot(kind=\"bar\", ax=ax, \n", + " color=[\"tab:cyan\", \"tab:blue\", \"tab:pink\", \"tab:red\"],\n", + " xlabel=\"\", fontsize=15);\n", + "ax.legend(labels = [\"Optimized\", \"Optimized with informed io parameters\", \"Original\", \"Original with informed io parameters\"], fontsize=15)\n", + "ax.set_ylabel(\"Time (s)\", fontsize=20)\n", + "\n", + "## Make two level axis\n", + "\n", + "# helper to create axis labels\n", + "def parse_text(s):\n", + " return re.sub(r\"[()]\", \"\", s).split(\", \")\n", + "\n", + "# Retrieve and parse axis labels and position\n", + "tool, size, x, y = map(np.array, zip(*[(*parse_text(l.get_text()), *l.get_position()) for l in ax.get_xticklabels()]))\n", + "# Make labels and x-positions for seconary axis\n", + "sec_x, sec_label = zip(*[(x[tool == tool_name].mean(), \"\\n\"+tool_name) for tool_name in np.unique(tool)])\n", + "# Assign ticks and labels\n", + "ax.set_xticks(x, size, rotation=0);\n", + "sec = ax.secondary_xaxis(location=0);\n", + "sec.set_xticks(sec_x, sec_label, fontsize=18);\n", + "sec.tick_params(length=0)\n", + "\n", + "sepa_x = np.array([x[tool == tool_name].min()-0.5 for tool_name in np.unique(tool)] + [x.max()+0.5])\n", + "[ax.axvline(xs, c='k', ymin=-.1, clip_on=False, zorder=3) for xs in sepa_x];\n", + "\n", + "# Uncomment to save figure\n", + "# fig.savefig(\"access_time.summary.png\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "815347b5-f23c-4d25-9104-42523e9de093", + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.10.13" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/notebooks/portable-full-comparison.ipynb b/notebooks/portable-full-comparison.ipynb new file mode 100644 index 0000000..400a027 --- /dev/null +++ b/notebooks/portable-full-comparison.ipynb @@ -0,0 +1,799 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "6c9b37e2-2daa-4283-a228-ea581498de0c", + "metadata": { + "tags": [], + "user_expressions": [] + }, + "source": [ + "## AB testing access time for ICESat-2 ATL03 HDF5 files in the cloud.\n", + "\n", + "This notebook requires that we have 2 versions of the same file:\n", + " * Original A: The original file with no modifications on a S3 location.\n", + " * Test Case B: A modified version of the orignal file to test for metadata consolidation, rechunking and other strategies to speed up access to the data in the file.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "aaca84b1-46e9-4b41-a494-24da3a368f38", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "!mamba uninstall -y h5coro \n", + "%pip install git+https://github.com/ICESat2-SlideRule/h5coro.git" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "3b78fb94-10ae-48cb-8e30-521b2c8b7822", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "import xarray as xr\n", + "import h5py\n", + "import fsspec\n", + "import logging\n", + "import re\n", + "import time\n", + "import numpy as np\n", + "import pandas as pd\n", + "import matplotlib.pyplot as plt\n", + "\n", + "\n", + "from h5coro import h5coro, s3driver, filedriver\n", + "driver = s3driver.S3Driver\n", + "\n", + "logger = logging.getLogger('fsspec')\n", + "logger.setLevel(logging.DEBUG)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "431d900d-0656-4b75-af6b-82f0f171d5f8", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "for library in (xr, h5py, fsspec, h5coro):\n", + " print(f'{library.__name__} v{library.__version__}')" + ] + }, + { + "cell_type": "markdown", + "id": "7998cd99-6034-4a1b-9ae5-d651bc265bff", + "metadata": { + "tags": [], + "user_expressions": [] + }, + "source": [ + "For listing files in CryoCloud\n", + "\n", + "```bash\n", + "aws s3 ls s3://nasa-cryo-persistent/h5cloud/ --recursive\n", + "```" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "9850faac-f534-4bc2-9214-c8dababe0f52", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "test_dict = {\n", + " \"ATL03-1GB\": {\n", + " \"links\": {\n", + " \"original\": \"s3://nasa-cryo-persistent/h5cloud/atl03/average/original/ATL03_20230618223036_13681901_006_01.h5\",\n", + " \"optimized\": \"s3://nasa-cryo-persistent/h5cloud/atl03/average/repacked/ATL03_20230618223036_13681901_006_01.h5\"\n", + " },\n", + " \"group\": \"/gt1l/heights\",\n", + " \"variable\": \"h_ph\",\n", + " \"processing\": [\n", + " \"h5repack -S PAGE -G 8000000\"\n", + " ]\n", + " },\n", + " \"ATL03-7GB\": {\n", + " \"links\": {\n", + " \"original\": \"s3://nasa-cryo-persistent/h5cloud/atl03/big/original/ATL03_20181120182818_08110112_006_02.h5\",\n", + " \"optimized\": \"s3://nasa-cryo-persistent/h5cloud/atl03/big/repacked/ATL03_20181120182818_08110112_006_02_repacked.h5\",\n", + " },\n", + " \"group\": \"/gt1l/heights\",\n", + " \"variable\": \"h_ph\",\n", + " \"processing\": [\n", + " \"h5repack -S PAGE -G 8000000\"\n", + " ]\n", + " },\n", + " \"ATL03-7GB-kerchunk\": {\n", + " \"links\": {\n", + " \"original\": \"s3://nasa-cryo-persistent/h5cloud/atl03/big/original/kerchunk/atl03_ATL03_20181120182818_08110112_006_02.json\",\n", + " \"optimized\": \"s3://nasa-cryo-persistent/h5cloud/atl03/big/repacked/kerchunk/atl03_ATL03_20181120182818_08110112_006_02_repacked.json\",\n", + " },\n", + " \"group\": \"/gt1l/heights\",\n", + " \"variable\": \"h_ph\",\n", + " \"processing\": [\n", + " \"h5repack -S PAGE -G 8000000\"\n", + " ]\n", + " }, \n", + " \"ATL03-2GB\": {\n", + " \"links\": {\n", + " \"original\": \"s3://nasa-cryo-persistent/h5cloud/atl03/big/original/ATL03_20210402143840_01341107_006_02.h5\",\n", + " \"optimized\": \"s3://nasa-cryo-persistent/h5cloud/atl03/big/repacked/ATL03_20210402143840_01341107_006_02_repacked.h5\",\n", + " },\n", + " \"group\": \"/gt1l/heights\",\n", + " \"variable\": \"h_ph\",\n", + " \"processing\": [\n", + " \"h5repack -S PAGE -G 8000000\"\n", + " ]\n", + " }\n", + "}\n", + "\n", + "def kerchunk_result(file: str, dataset: str, variable: str):\n", + " fs = fsspec.filesystem(\n", + " \"reference\",\n", + " fo=file,\n", + " remote_protocol=\"s3\",\n", + " remote_options=dict(anon=False),\n", + " skip_instance_cache=True,\n", + " )\n", + " ds = xr.open_dataset(\n", + " fs.get_mapper(\"\"), engine=\"zarr\", consolidated=False, group=dataset\n", + " )\n", + " return ds[variable].mean()\n", + "\n", + "# This will use the embedded credentials in the hub to access the s3://nasa-cryo-persistent bucket\n", + "fs = fsspec.filesystem('s3')\n" + ] + }, + { + "cell_type": "markdown", + "id": "4d166627-6144-40bf-884d-2188e5c764ba", + "metadata": { + "tags": [], + "user_expressions": [] + }, + "source": [ + "## [h5coro](https://github.com/ICESat2-SlideRule/h5coro/)\n", + "\n", + "**h5coro** is optimized for reading HDF5 data in high-latency high-throughput environments. It accomplishes this through a few key design decisions:\n", + "* __All reads are concurrent.__ Each dataset and/or attribute read by **h5coro** is performed in its own thread.\n", + "* __Intelligent range gets__ are used to read as many dataset chunks as possible in each read operation. This drastically reduces the number of HTTP requests to S3 and means there is no longer a need to re-chunk the data (it actually works better on smaller chunk sizes due to the granularity of the request).\n", + "* __Block caching__ is used to minimize the number of GET requests made to S3. S3 has a large first-byte latency (we've measured it at ~60ms on our systems), which means there is a large penalty for each read operation performed. **h5coro** performs all reads to S3 as large block reads and then maintains data in a local cache for access to smaller amounts of data within those blocks.\n", + "* __The system is serverless__ and does not depend on any external services to read the data. This means it scales naturally as the user application scales, and it reduces overall system complexity.\n", + "* __No metadata repository is needed.__ The structure of the file are cached as they are read so that successive reads to other datasets in the same file will not have to re-read and re-build the directory structure of the file.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "efe41d4a-1947-438b-a3c3-7ab954d75e13", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "h5coro_beanchmarks = []\n", + "\n", + "for key, dataset in test_dict.items():\n", + " for k, link in dataset[\"links\"].items():\n", + " print (f\"Processing: {link}\")\n", + " if \"kerchunk\" in link:\n", + " continue\n", + " group = dataset[\"group\"]\n", + " variable = dataset['variable'] \n", + " final_h5coro_array = []\n", + " start = time.time()\n", + " if link.startswith(\"s3://nasa-cryo-persistent/\"):\n", + " h5obj = h5coro.H5Coro(link.replace(\"s3://\", \"\"), s3driver.S3Driver)\n", + " else:\n", + " h5obj = h5coro.H5Coro(link.replace(\"s3://\", \"\"), s3driver.S3Driver, credentials={\"annon\": True})\n", + " ds = h5obj.readDatasets(datasets=[f'{group}/{variable}'], block=True)\n", + " data = ds[f'{group}/{variable}'][:]\n", + " data_mean = np.mean(data)\n", + " elapsed = time.time() - start\n", + " \n", + " h5coro_beanchmarks.append({\"tool\": \"h5coro\",\n", + " \"dataset\": key,\n", + " \"cloud-aware\": \"no\",\n", + " \"format\": k,\n", + " \"file\": link,\n", + " \"time\": elapsed,\n", + " \"mean\": data_mean})\n", + "\n", + "\n", + "df = pd.DataFrame.from_dict(h5coro_beanchmarks)\n", + "\n", + "pivot_df = df.pivot_table(index=['tool','dataset'], columns=['format'], values='time', aggfunc='mean')\n", + "\n", + "# Plotting\n", + "pivot_df.plot(kind='bar', figsize=(10, 6))\n", + "plt.title('h5coro cloud optimized HDF5 performance')\n", + "plt.xlabel('Tool')\n", + "plt.ylabel('Mean Time')\n", + "plt.xticks(rotation=90)\n", + "plt.legend(title='Format')\n", + "plt.grid(True)\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "id": "8f0ba64d-d89c-4879-b965-f00d70956360", + "metadata": { + "tags": [], + "user_expressions": [] + }, + "source": [ + "### Xarray + kerchunk, out of the box performance." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "ff56958f-8c1d-4fd7-b885-6efb81af8da7", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "# this is going to keep our numbers without modifying the i/o paramters\n", + "regular_xarray_benchmarks = []\n", + "kerchunk_benchmarks = []\n", + "\n", + "for key, dataset in test_dict.items():\n", + " for k, link in dataset[\"links\"].items():\n", + " print (f\"Processing: {link}\")\n", + " try:\n", + " log_filename = f\"logs/fsspec-xarray-{key}-{k}-default.log\"\n", + " \n", + " # Create a new FileHandler for each iteration\n", + " file_handler = logging.FileHandler(log_filename)\n", + " file_handler.setLevel(logging.DEBUG)\n", + "\n", + " # Add the handler to the root logger\n", + " logging.getLogger().addHandler(file_handler)\n", + " \n", + " start = time.time()\n", + " if \"kerchunk\" in link:\n", + " data_mean = kerchunk_result(link, dataset[\"group\"], dataset[\"variable\"])\n", + " elapsed = time.time() - start\n", + " kerchunk_benchmarks.append(\n", + " {\"tool\": \"kerchunk\",\n", + " \"dataset\": key,\n", + " \"cloud-aware\": \"no\",\n", + " \"format\": k,\n", + " \"file\": link,\n", + " \"time\": elapsed,\n", + " \"mean\": data_mean}) \n", + " else:\n", + " ds = xr.open_dataset(fs.open(link, mode='rb'), group=dataset[\"group\"], engine=\"h5netcdf\", decode_cf=False)\n", + " data_mean = ds[dataset[\"variable\"]].mean() \n", + " elapsed = time.time() - start\n", + " regular_xarray_benchmarks.append(\n", + " {\"tool\": \"xarray\",\n", + " \"dataset\": key,\n", + " \"cloud-aware\": \"no\",\n", + " \"format\": k,\n", + " \"file\": link,\n", + " \"time\": elapsed,\n", + " \"mean\": data_mean}) \n", + " \n", + " logging.getLogger().removeHandler(file_handler)\n", + " file_handler.close()\n", + "\n", + " except Exception as e:\n", + " print(e)" + ] + }, + { + "cell_type": "markdown", + "id": "92a8e67d-026e-4c6b-aa7d-b19dc10f4afd", + "metadata": { + "tags": [], + "user_expressions": [] + }, + "source": [ + "### Plotting Results" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "149d5972-c5b9-4f29-979a-cf46c9654a06", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "df = pd.DataFrame.from_dict(kerchunk_benchmarks + regular_xarray_benchmarks)\n", + "\n", + "pivot_df = df.pivot_table(index=['tool','dataset'], columns=['format'], values='time', aggfunc='mean')\n", + "\n", + "# Plotting\n", + "pivot_df.plot(kind='bar', figsize=(10, 6))\n", + "\n", + "plt.title(\"Out of the box I/O parameters\", fontsize=10)\n", + "plt.suptitle('Cloud-optimized HDF5 performance (less is better)', fontsize=14)\n", + "\n", + "plt.xlabel('Tool')\n", + "plt.ylabel('Mean Time')\n", + "plt.xticks(rotation=90)\n", + "plt.legend(title='Format')\n", + "plt.grid(True)\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "id": "fa6ac2b9-989c-4246-bb89-b54b711dd695", + "metadata": { + "tags": [], + "user_expressions": [] + }, + "source": [ + "## h5py out of the box performance." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "98c29558-de50-44af-87e9-074092fcd0ac", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "regular_h5py_benchmarks = []\n", + "\n", + "for key, dataset in test_dict.items():\n", + " for k, link in dataset[\"links\"].items():\n", + " try:\n", + " if \"kerchunk\" in link:\n", + " continue \n", + " print (f\"Processing: {link}\")\n", + " log_filename = f\"logs/fsspec-h5py-{key}-{k}_default.log\"\n", + " \n", + " # Create a new FileHandler for each iteration\n", + " file_handler = logging.FileHandler(log_filename)\n", + " file_handler.setLevel(logging.DEBUG)\n", + "\n", + " # Add the handler to the root logger\n", + " logging.getLogger().addHandler(file_handler)\n", + " # this is mostly IO so no perf_counter is needed\n", + " start = time.time()\n", + " with h5py.File(fs.open(link, mode=\"rb\")) as f:\n", + " path = f\"{dataset['group']}/{dataset['variable']}\"\n", + " data = f[path][:]\n", + " data_mean = data.mean()\n", + " elapsed = time.time() - start\n", + " regular_h5py_benchmarks.append(\n", + " {\"tool\": \"h5py\",\n", + " \"dataset\": key,\n", + " \"cloud-aware\": \"no\",\n", + " \"format\": k,\n", + " \"file\": link,\n", + " \"time\": elapsed,\n", + " \"mean\": data_mean})\n", + "\n", + " logging.getLogger().removeHandler(file_handler) \n", + " file_handler.close()\n", + " \n", + " except Exception as e:\n", + " print(e)" + ] + }, + { + "cell_type": "markdown", + "id": "f4232e98-1159-45eb-ba11-0f0dbb905d83", + "metadata": { + "tags": [], + "user_expressions": [] + }, + "source": [ + "### Plotting Results" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d8fa6dca-f408-4298-beca-f2839d4c3b67", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "df = pd.DataFrame.from_dict(regular_h5py_benchmarks)\n", + "\n", + "pivot_df = df.pivot_table(index=['tool','dataset'], columns=['format'], values='time', aggfunc='mean')\n", + "\n", + "# Plotting\n", + "pivot_df.plot(kind='bar', figsize=(10, 6))\n", + "plt.suptitle('Cloud-optimized HDF5 performance (less is better)', fontsize=14)\n", + "plt.title(\"Out of the box I/O parameters\", fontsize=10)\n", + "\n", + "plt.xlabel('Tool')\n", + "plt.ylabel('Mean Time')\n", + "plt.xticks(rotation=45)\n", + "plt.legend(title='Format')\n", + "plt.grid(True)\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "id": "b20b2032-9ab4-46e1-b1f8-2e62b656a265", + "metadata": { + "tags": [], + "user_expressions": [] + }, + "source": [ + "## Aggregated plot by tool and different file sizes" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "64bcc5de-aae3-46aa-9474-1c90b9ff20a9", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "df = pd.DataFrame.from_dict(regular_h5py_benchmarks + kerchunk_benchmarks + regular_xarray_benchmarks + h5coro_beanchmarks)\n", + "\n", + "pivot_df = df.pivot_table(index=['tool','dataset'], columns=['format'], values='time', aggfunc='mean')\n", + "\n", + "# Plotting\n", + "pivot_df.plot(kind='bar', figsize=(10, 6))\n", + "plt.suptitle('Cloud-optimized HDF5 performance (less is better)', fontsize=14)\n", + "plt.title(\"Out of the box I/O parameters\", fontsize=10)\n", + "plt.xlabel('Tool')\n", + "plt.ylabel('Mean Time')\n", + "plt.xticks(rotation=90)\n", + "plt.legend(title='Format')\n", + "plt.grid(True)\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "id": "0ea67b0b-5e7f-4d1f-bca9-1f3cae7fe309", + "metadata": { + "tags": [], + "user_expressions": [] + }, + "source": [ + "## Now let's run the tests with \"informed\" parameters, this is a I/O that aligns to the cloud-optimized granules chunking strategy and consolidated metadata.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "8151834b-0b57-4a3d-98b5-8cfaffa37dc4", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "optimized_h5py_benchmarks = []\n", + "optimized_xarray_benchmarks = []\n", + "\n", + "for key, dataset in test_dict.items():\n", + " for k, link in dataset[\"links\"].items():\n", + " print(f\"Processing: {link}\")\n", + " try:\n", + " log_filename = f\"logs/fsspec-xarray-{key}-{k}.log\"\n", + " \n", + " # Create a new FileHandler for each iteration\n", + " file_handler = logging.FileHandler(log_filename)\n", + " file_handler.setLevel(logging.DEBUG)\n", + "\n", + " # Add the handler to the root logger\n", + " logging.getLogger().addHandler(file_handler)\n", + " \n", + " io_params = {\n", + " \"fsspec_params\": {},\n", + " \"h5py_params\": {}\n", + " }\n", + " \n", + " if \"repacked\" in link: \n", + " io_params ={\n", + " \"fsspec_params\": {\n", + " \"cache_type\": \"blockcache\",\n", + " \"block_size\": 8*1024*1024\n", + " },\n", + " \"h5py_params\" : {\n", + " \"driver_kwds\": {\n", + " \"page_buf_size\": 64*1024*1024,\n", + " \"rdcc_nbytes\": 8*1024*1024\n", + " }\n", + "\n", + " }\n", + " }\n", + "\n", + " if \"kerchunk\" in link:\n", + " continue\n", + " \n", + " start = time.time()\n", + " ds = xr.open_dataset(fs.open(link, mode='rb', **io_params[\"fsspec_params\"]), group=dataset[\"group\"], engine=\"h5netcdf\", decode_cf=False)\n", + " data_mean = ds[dataset[\"variable\"]].mean()\n", + " elapsed = time.time() - start\n", + " optimized_xarray_benchmarks.append(\n", + " {\"tool\": \"xarray\",\n", + " \"dataset\": key,\n", + " \"cloud-aware\": \"yes\",\n", + " \"format\": k,\n", + " \"file\": link,\n", + " \"time\": elapsed,\n", + " \"mean\": data_mean})\n", + " \n", + " logging.getLogger().removeHandler(file_handler)\n", + " file_handler.close()\n", + "\n", + " except Exception as e:\n", + " print(e)\n", + " \n", + "for key, dataset in test_dict.items():\n", + " for k, link in dataset[\"links\"].items():\n", + " try:\n", + " if \"kerchunk\" in link:\n", + " continue \n", + " print (f\"Processing: {link}\")\n", + " log_filename = f\"logs/fsspec-h5py-{key}-{k}_default.log\"\n", + " \n", + " # Create a new FileHandler for each iteration\n", + " file_handler = logging.FileHandler(log_filename)\n", + " file_handler.setLevel(logging.DEBUG)\n", + "\n", + " # Add the handler to the root logger\n", + " logging.getLogger().addHandler(file_handler)\n", + " # this is mostly IO so no perf_counter is needed\n", + " start = time.time()\n", + " io_params = {\n", + " \"fsspec_params\": {},\n", + " \"h5py_params\": {}\n", + " }\n", + " \n", + " if \"repacked\" in link: \n", + " io_params ={\n", + " \"fsspec_params\": {\n", + " \"cache_type\": \"blockcache\",\n", + " \"block_size\": 8*1024*1024\n", + " },\n", + " \"h5py_params\" : {\n", + " \"page_buf_size\": 64*1024*1024,\n", + " \"rdcc_nbytes\": 8*1024*1024\n", + " }\n", + " } \n", + " with h5py.File(fs.open(link, mode=\"rb\", **io_params[\"fsspec_params\"]), **io_params[\"h5py_params\"]) as f:\n", + " path = f\"{dataset['group']}/{dataset['variable']}\"\n", + " data = f[path][:]\n", + " data_mean = data.mean()\n", + " elapsed = time.time() - start\n", + " optimized_h5py_benchmarks.append(\n", + " {\"tool\": \"h5py\",\n", + " \"dataset\": key,\n", + " \"cloud-aware\": \"yes\",\n", + " \"format\": k,\n", + " \"file\": link,\n", + " \"time\": elapsed,\n", + " \"mean\": data_mean})\n", + "\n", + " logging.getLogger().removeHandler(file_handler) \n", + " file_handler.close()\n", + " \n", + "\n", + " except Exception as e:\n", + " print(e)" + ] + }, + { + "cell_type": "markdown", + "id": "04414c2e-0666-4701-8ecc-7842727ede22", + "metadata": { + "tags": [], + "user_expressions": [] + }, + "source": [ + "## Plotting results" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "2db2535a-8d3a-4e65-b21c-8db6b48074c8", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "df = pd.DataFrame.from_dict(optimized_h5py_benchmarks+h5coro_beanchmarks+optimized_xarray_benchmarks+kerchunk_benchmarks)\n", + "\n", + "pivot_df = df.pivot_table(index=['tool','dataset'], columns=['format'], values='time', aggfunc='mean')\n", + "\n", + "# Plotting\n", + "pivot_df.plot(kind='bar', figsize=(10, 6))\n", + "\n", + "plt.suptitle('Cloud-optimized HDF5 performance (less is better)', fontsize=14)\n", + "plt.title(\"Informed I/O parameters\", fontsize=10)\n", + "plt.xlabel('Tool')\n", + "plt.ylabel('Mean Time')\n", + "plt.xticks(rotation=90)\n", + "plt.legend(title='Format')\n", + "plt.grid(True)\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "id": "ea0db03e-5653-4908-ada1-16d723666e18", + "metadata": { + "tags": [], + "user_expressions": [] + }, + "source": [ + "## Ploting tool specific performance" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "47444e8a-6d59-42c2-baff-a3c85c447eb2", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "df = pd.DataFrame.from_dict(regular_xarray_benchmarks+optimized_xarray_benchmarks)\n", + "\n", + "pivot_df = df.pivot_table(index=['dataset','cloud-aware'], columns=['format'], values='time', aggfunc='mean')\n", + "\n", + "# Plotting\n", + "pivot_df.plot(kind='bar', figsize=(10, 6))\n", + "plt.title('Xarray \"Cloud-Aware\" Access Pattern Performance (less is better)')\n", + "plt.xlabel('Tool')\n", + "plt.ylabel('Mean Time')\n", + "plt.xticks(rotation=90)\n", + "plt.legend(title='Format')\n", + "plt.grid(True)\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "id": "8395f794-0ea7-4c26-8f64-0d2f9659d841", + "metadata": { + "tags": [], + "user_expressions": [] + }, + "source": [ + "## Make one comparison plot" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "bbe17a07-22e3-4b99-a50a-d3183425d15c", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "df = pd.DataFrame.from_dict(regular_h5py_benchmarks + \n", + " kerchunk_benchmarks + \n", + " regular_xarray_benchmarks + \n", + " h5coro_beanchmarks + \n", + " optimized_h5py_benchmarks + \n", + " optimized_xarray_benchmarks)\n", + "df[\"size\"] = df.dataset.str.extract(r\"-(\\dGB)\")\n", + "df[\"product\"] = df.dataset.str.extract(r\"(ATL\\d{2})\")\n", + "df.to_csv(\"benchmarks.csv\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "90486527-a1f2-4a92-bee6-1b2f934aa24d", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "pivot_df = df.pivot_table(index=[\"tool\", \"size\"], columns=[\"format\", \"cloud-aware\"], values=\"time\", aggfunc=\"mean\")\n", + "pivot_df" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "88badbc0-a277-4aee-9236-d74327032d0d", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "import seaborn as sns\n", + "\n", + "sns.set_style(\"darkgrid\", rc={'axes.facecolor': '0.9'})\n", + "# sns.set_palette(\"bright\", 4)\n", + "\n", + "fig, ax = plt.subplots(figsize=(15,6), layout=\"constrained\")\n", + "\n", + "pivot_df.plot(kind=\"bar\", ax=ax, \n", + " color=[\"tab:cyan\", \"tab:blue\", \"tab:pink\", \"tab:red\"],\n", + " xlabel=\"\", fontsize=15);\n", + "ax.legend(labels = [\"Optimized\", \"Optimized with informed io parameters\", \"Original\", \"Original with informed io parameters\"], fontsize=15)\n", + "ax.set_ylabel(\"Time (s)\", fontsize=20)\n", + "\n", + "# Make two level axis\n", + "def parse_text(s):\n", + " return re.sub(r\"[()]\", \"\", s).split(\", \")\n", + "\n", + "# Retrieve and parse axis labels and position\n", + "tool, size, x, y = map(np.array, zip(*[(*parse_text(l.get_text()), *l.get_position()) for l in ax.get_xticklabels()]))\n", + "# Make labels and x-positions for seconary axis\n", + "sec_x, sec_label = zip(*[(x[tool == tool_name].mean(), \"\\n\"+tool_name) for tool_name in np.unique(tool)])\n", + "# Assign ticks and labels\n", + "ax.set_xticks(x, size, rotation=0);\n", + "sec = ax.secondary_xaxis(location=0);\n", + "sec.set_xticks(sec_x, sec_label, fontsize=18);\n", + "sec.tick_params(length=0)\n", + "\n", + "sepa_x = np.array([x[tool == tool_name].min()-0.5 for tool_name in np.unique(tool)] + [x.max()+0.5])\n", + "[ax.axvline(xs, c='k', ymin=-.1, clip_on=False, zorder=3) for xs in sepa_x];\n", + "\n", + "# Use plot_benchmark_results.ipynb to generate saveable png" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "c8dd30a5-952e-428f-b908-9897fac81aa7", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "ae595359-3e8a-4072-89b4-bd2e52d9ec12", + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.10.13" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/notebooks/portable-h5coro-test.ipynb b/notebooks/portable-h5coro-test.ipynb new file mode 100644 index 0000000..df2394f --- /dev/null +++ b/notebooks/portable-h5coro-test.ipynb @@ -0,0 +1,162 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 1, + "id": "48daa283-8e1e-46e3-b4ce-1a0271b86d37", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "%load_ext autoreload\n", + "%autoreload \n", + "\n", + "import sys\n", + "import os\n", + "classes_path = os.path.abspath('../h5tests/')\n", + "sys.path.append(classes_path)\n", + "from h5coro_arr_mean import H5CoroArrMean\n", + "import pandas as pd\n", + "\n", + "benchmarks = []" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "d6ce77fd-f9cd-48b1-94cd-1fe57f52e11f", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "original_granules = [\n", + " \"s3://its-live-data/cloud-experiments/h5cloud/atl03/big/original/ATL03_20181120182818_08110112_006_02.h5\",\n", + " \"s3://its-live-data/cloud-experiments/h5cloud/atl03/big/original/ATL03_20190219140808_08110212_006_02.h5\",\n", + "]\n", + "h5coro_original = H5CoroArrMean('atl03-bigsize-original', files=original_granules, store_results=True)" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "60eeeb1b-9531-4fec-a847-3ca5304c4685", + "metadata": { + "tags": [] + }, + "outputs": [ + { + "ename": "FatalError", + "evalue": "invalid credential keys provided, looking for: aws_access_key_id, aws_secret_access_key, and aws_session_token", + "output_type": "error", + "traceback": [ + "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", + "\u001b[0;31mFatalError\u001b[0m Traceback (most recent call last)", + "Cell \u001b[0;32mIn[3], line 2\u001b[0m\n\u001b[1;32m 1\u001b[0m \u001b[38;5;66;03m# takes about ~30 seconds per granule out of region (6+ GB granules)\u001b[39;00m\n\u001b[0;32m----> 2\u001b[0m results \u001b[38;5;241m=\u001b[39m \u001b[43mh5coro_original\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mrun\u001b[49m\u001b[43m(\u001b[49m\u001b[43m)\u001b[49m\n\u001b[1;32m 3\u001b[0m benchmarks\u001b[38;5;241m.\u001b[39mappend({\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mlibrary\u001b[39m\u001b[38;5;124m\"\u001b[39m: \u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mh5coro\u001b[39m\u001b[38;5;124m\"\u001b[39m,\n\u001b[1;32m 4\u001b[0m \u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mformat\u001b[39m\u001b[38;5;124m\"\u001b[39m: \u001b[38;5;124m\"\u001b[39m\u001b[38;5;124moriginal\u001b[39m\u001b[38;5;124m\"\u001b[39m,\n\u001b[1;32m 5\u001b[0m \u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mmean\u001b[39m\u001b[38;5;124m\"\u001b[39m: results[\u001b[38;5;241m0\u001b[39m],\n\u001b[0;32m (...)\u001b[0m\n\u001b[1;32m 8\u001b[0m \u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mtotal_requests\u001b[39m\u001b[38;5;124m\"\u001b[39m: results[\u001b[38;5;241m3\u001b[39m][\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mtotal_reqs\u001b[39m\u001b[38;5;124m\"\u001b[39m],\n\u001b[1;32m 9\u001b[0m \u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mavg_req_size\u001b[39m\u001b[38;5;124m\"\u001b[39m: results[\u001b[38;5;241m3\u001b[39m][\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mavg_req_size\u001b[39m\u001b[38;5;124m\"\u001b[39m]})\n\u001b[1;32m 10\u001b[0m benchmarks\n", + "File \u001b[0;32m~/work/openscapes/h5cloud/h5tests/h5test.py:95\u001b[0m, in \u001b[0;36mtimer_decorator..wrapper\u001b[0;34m(self, *args, **kwargs)\u001b[0m\n\u001b[1;32m 93\u001b[0m __setup_logging(\u001b[38;5;28mself\u001b[39m, tstamp)\n\u001b[1;32m 94\u001b[0m start_time \u001b[38;5;241m=\u001b[39m time\u001b[38;5;241m.\u001b[39mtime()\n\u001b[0;32m---> 95\u001b[0m result \u001b[38;5;241m=\u001b[39m \u001b[43mfunc\u001b[49m\u001b[43m(\u001b[49m\u001b[38;5;28;43mself\u001b[39;49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[43margs\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[43mkwargs\u001b[49m\u001b[43m)\u001b[49m\n\u001b[1;32m 96\u001b[0m end_time \u001b[38;5;241m=\u001b[39m time\u001b[38;5;241m.\u001b[39mtime()\n\u001b[1;32m 97\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mlogs_regex:\n", + "File \u001b[0;32m~/work/openscapes/h5cloud/h5tests/h5coro_arr_mean.py:30\u001b[0m, in \u001b[0;36mH5CoroArrMean.run\u001b[0;34m(self, dataset, variable)\u001b[0m\n\u001b[1;32m 27\u001b[0m credentials \u001b[38;5;241m=\u001b[39m {\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mregion_name\u001b[39m\u001b[38;5;124m\"\u001b[39m: \u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mus-west-2\u001b[39m\u001b[38;5;124m\"\u001b[39m,\n\u001b[1;32m 28\u001b[0m \u001b[38;5;124m\"\u001b[39m\u001b[38;5;124manon\u001b[39m\u001b[38;5;124m\"\u001b[39m: \u001b[38;5;28;01mTrue\u001b[39;00m}\n\u001b[1;32m 29\u001b[0m \u001b[38;5;28;01mfor\u001b[39;00m file \u001b[38;5;129;01min\u001b[39;00m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mfiles:\n\u001b[0;32m---> 30\u001b[0m h5obj \u001b[38;5;241m=\u001b[39m \u001b[43mh5coro\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mH5Coro\u001b[49m\u001b[43m(\u001b[49m\u001b[43mfile\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mreplace\u001b[49m\u001b[43m(\u001b[49m\u001b[38;5;124;43m\"\u001b[39;49m\u001b[38;5;124;43ms3://\u001b[39;49m\u001b[38;5;124;43m\"\u001b[39;49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;124;43m\"\u001b[39;49m\u001b[38;5;124;43m\"\u001b[39;49m\u001b[43m)\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43ms3driver\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mS3Driver\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mcredentials\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mcredentials\u001b[49m\u001b[43m)\u001b[49m\n\u001b[1;32m 31\u001b[0m output \u001b[38;5;241m=\u001b[39m h5obj\u001b[38;5;241m.\u001b[39mreadDatasets(datasets\u001b[38;5;241m=\u001b[39m[\u001b[38;5;124mf\u001b[39m\u001b[38;5;124m'\u001b[39m\u001b[38;5;132;01m{\u001b[39;00mgroup\u001b[38;5;132;01m}\u001b[39;00m\u001b[38;5;124m/\u001b[39m\u001b[38;5;132;01m{\u001b[39;00mvariable\u001b[38;5;132;01m}\u001b[39;00m\u001b[38;5;124m'\u001b[39m], block\u001b[38;5;241m=\u001b[39m\u001b[38;5;28;01mTrue\u001b[39;00m)\n\u001b[1;32m 32\u001b[0m data \u001b[38;5;241m=\u001b[39m h5obj[\u001b[38;5;124mf\u001b[39m\u001b[38;5;124m'\u001b[39m\u001b[38;5;132;01m{\u001b[39;00mgroup\u001b[38;5;132;01m}\u001b[39;00m\u001b[38;5;124m/\u001b[39m\u001b[38;5;132;01m{\u001b[39;00mvariable\u001b[38;5;132;01m}\u001b[39;00m\u001b[38;5;124m'\u001b[39m]\u001b[38;5;241m.\u001b[39mvalues\n", + "File \u001b[0;32m~/.pyenv/versions/mambaforge/envs/h5cloud/lib/python3.12/site-packages/h5coro/h5coro.py:2020\u001b[0m, in \u001b[0;36mH5Coro.__init__\u001b[0;34m(self, resource, driver_class, credentials, datasets, block)\u001b[0m\n\u001b[1;32m 2018\u001b[0m \u001b[38;5;28;01mdef\u001b[39;00m \u001b[38;5;21m__init__\u001b[39m(\u001b[38;5;28mself\u001b[39m, resource, driver_class, credentials\u001b[38;5;241m=\u001b[39m{}, datasets\u001b[38;5;241m=\u001b[39m[], block\u001b[38;5;241m=\u001b[39m\u001b[38;5;28;01mTrue\u001b[39;00m):\n\u001b[1;32m 2019\u001b[0m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mresource \u001b[38;5;241m=\u001b[39m resource\n\u001b[0;32m-> 2020\u001b[0m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mdriver \u001b[38;5;241m=\u001b[39m \u001b[43mdriver_class\u001b[49m\u001b[43m(\u001b[49m\u001b[43mresource\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mcredentials\u001b[49m\u001b[43m)\u001b[49m\n\u001b[1;32m 2022\u001b[0m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mcache \u001b[38;5;241m=\u001b[39m {}\n\u001b[1;32m 2023\u001b[0m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mmetaDataTable \u001b[38;5;241m=\u001b[39m {}\n", + "File \u001b[0;32m~/.pyenv/versions/mambaforge/envs/h5cloud/lib/python3.12/site-packages/h5coro/s3driver.py:43\u001b[0m, in \u001b[0;36mS3Driver.__init__\u001b[0;34m(self, resource, credentials)\u001b[0m\n\u001b[1;32m 41\u001b[0m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39msession \u001b[38;5;241m=\u001b[39m boto3\u001b[38;5;241m.\u001b[39mSession()\n\u001b[1;32m 42\u001b[0m \u001b[38;5;28;01melse\u001b[39;00m:\n\u001b[0;32m---> 43\u001b[0m \u001b[38;5;28;01mraise\u001b[39;00m FatalError(\u001b[38;5;124m'\u001b[39m\u001b[38;5;124minvalid credential keys provided, looking for: aws_access_key_id, aws_secret_access_key, and aws_session_token\u001b[39m\u001b[38;5;124m'\u001b[39m)\n\u001b[1;32m 45\u001b[0m \u001b[38;5;66;03m# open resource\u001b[39;00m\n\u001b[1;32m 46\u001b[0m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mobj \u001b[38;5;241m=\u001b[39m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39msession\u001b[38;5;241m.\u001b[39mresource(\u001b[38;5;124m'\u001b[39m\u001b[38;5;124ms3\u001b[39m\u001b[38;5;124m'\u001b[39m)\u001b[38;5;241m.\u001b[39mObject(\u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mresourcePath[\u001b[38;5;241m0\u001b[39m], \u001b[38;5;124m'\u001b[39m\u001b[38;5;124m/\u001b[39m\u001b[38;5;124m'\u001b[39m\u001b[38;5;241m.\u001b[39mjoin(\u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mresourcePath[\u001b[38;5;241m1\u001b[39m:]))\n", + "\u001b[0;31mFatalError\u001b[0m: invalid credential keys provided, looking for: aws_access_key_id, aws_secret_access_key, and aws_session_token" + ] + } + ], + "source": [ + "# takes about ~30 seconds per granule out of region (6+ GB granules)\n", + "results = h5coro_original.run()\n", + "benchmarks.append({\"library\": \"h5coro\",\n", + " \"format\": \"original\",\n", + " \"mean\": results[0],\n", + " \"time\": results[1],\n", + " \"total_requested_bytes\": results[3][\"total_reqs_bytes\"],\n", + " \"total_requests\": results[3][\"total_reqs\"],\n", + " \"avg_req_size\": results[3][\"avg_req_size\"]})\n", + "benchmarks" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "64c4584f-c527-44bb-8c05-68a96820d1ff", + "metadata": {}, + "outputs": [], + "source": [ + "cloud_optimized_granules = [\n", + " \"s3://its-live-data/cloud-experiments/h5cloud/atl03/big/repacked/ATL03_20181120182818_08110112_006_02_repacked.h5\",\n", + " \"s3://its-live-data/cloud-experiments/h5cloud/atl03/big/repacked/ATL03_20190219140808_08110212_006_02_repacked.h5\",\n", + "]\n", + "h5py_cloud = H5pyArrMean('atl03-bigsize-repacked', files=cloud_optimized_granules, store_results=True)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "9d30d92b-4192-4da1-8b60-41cc94ca2db1", + "metadata": {}, + "outputs": [], + "source": [ + "df = pd.DataFrame.from_dict(benchmarks)\n", + "df" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "3ff4c22f-7f77-4c69-a84c-f13b0fbba1f2", + "metadata": {}, + "outputs": [], + "source": [ + "import matplotlib.pyplot as plt\n", + "\n", + "fig, ax = plt.subplots(figsize=(10, 6))\n", + "\n", + "for name, group in df.groupby(['library', 'format']):\n", + " library, format = name\n", + " x = f'{library}, {format}'\n", + " y = group['time'].mean()\n", + " ax.bar(f'{library}, {format}', group['time'].mean(), label=f'{library}, {format}', align='center')\n", + " ax.text(x, y + 0.05, f'{group[\"time\"].mean():.2f}', ha='center', va='bottom', color='black', fontsize=12)\n", + " ax.text(x, y - (y/2) - 10, f'Total Requests: {group[\"total_requests\"].mean()}', ha='center', va='bottom', color='black', fontsize=8)\n", + " ax.text(x, y - (y/2.5), f'Total Req Bytes (MB): {round(group[\"total_requested_bytes\"].mean() / (1024*1024) , 2)}', ha='center', va='bottom', color='black', fontsize=8)\n", + "\n", + "# Set labels and title\n", + "ax.set_xlabel('Access Pattern')\n", + "ax.set_ylabel('Time in Seconds')\n", + "ax.set_title(f'mean() on photon data for runs on ATL03, less is better ')\n", + "\n", + "# Rotate x-axis labels for better readability\n", + "plt.xticks(rotation=45, ha='right')\n", + "\n", + "# # Show legend\n", + "# ax.legend()\n", + "\n", + "# Show the plot\n", + "with plt.xkcd():\n", + " # This figure will be in XKCD-style\n", + " fig1 = plt.figure()" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.12.1" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/notebooks/portable-h5py-test.ipynb b/notebooks/portable-h5py-test.ipynb new file mode 100644 index 0000000..05c60b8 --- /dev/null +++ b/notebooks/portable-h5py-test.ipynb @@ -0,0 +1,195 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "id": "48daa283-8e1e-46e3-b4ce-1a0271b86d37", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "%load_ext autoreload\n", + "%autoreload \n", + "\n", + "import sys\n", + "import os\n", + "classes_path = os.path.abspath('../h5tests/')\n", + "sys.path.append(classes_path)\n", + "from h5py_arr_mean import H5pyArrMean\n", + "import pandas as pd\n", + "\n", + "benchmarks = []" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d6ce77fd-f9cd-48b1-94cd-1fe57f52e11f", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "original_granules = [\n", + " \"s3://its-live-data/cloud-experiments/h5cloud/atl03/average/original/ATL03_20191225111315_13680501_006_01.h5\",\n", + " # \"s3://its-live-data/cloud-experiments/h5cloud/atl03/big/original/ATL03_20190219140808_08110212_006_02.h5\",\n", + "]\n", + "h5py_original = H5pyArrMean('atl03-bigsize-original', files=original_granules, store_results=True)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "60eeeb1b-9531-4fec-a847-3ca5304c4685", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "# takes about ~30 seconds per granule out of region (6+ GB granules)\n", + "io_params ={\n", + " \"fsspec_params\": {},\n", + " \"h5py_params\" : {}\n", + "}\n", + "\n", + "results = h5py_original.run(io_params)\n", + "\n", + "benchmarks.append({\"library\": \"h5py\",\n", + " \"format\": \"original\",\n", + " \"mean\": results[0],\n", + " \"time\": results[1],\n", + " \"total_requested_bytes\": results[3][\"total_reqs_bytes\"],\n", + " \"total_requests\": results[3][\"total_reqs\"],\n", + " \"avg_req_size\": results[3][\"avg_req_size\"]})\n", + "benchmarks" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "64c4584f-c527-44bb-8c05-68a96820d1ff", + "metadata": {}, + "outputs": [], + "source": [ + "cloud_optimized_granules = [\n", + " \"s3://its-live-data/cloud-experiments/h5cloud/atl03/average/repacked/ATL03_20191225111315_13680501_006_01.h5\",\n", + " # \"s3://its-live-data/cloud-experiments/h5cloud/atl03/big/repacked/ATL03_20190219140808_08110212_006_02_repacked.h5\",\n", + "]\n", + "h5py_cloud = H5pyArrMean('atl03-bigsize-repacked', files=cloud_optimized_granules, store_results=True)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "dfd4e404-0412-4d2f-8eba-ca39a670e369", + "metadata": {}, + "outputs": [], + "source": [ + "# takes about ~30 seconds per granule out of region\n", + "io_params ={\n", + " \"fsspec_params\": {\n", + " # \"skip_instance_cache\": True\n", + " \"cache_type\": \"first\",\n", + " \"block_size\": 8*1024*1024\n", + " },\n", + " \"h5py_params\" : {\n", + " \"page_buf_size\": 32*1024*1024,\n", + " \"rdcc_nbytes\": 1024*1024\n", + " }\n", + "}\n", + "\n", + "results = h5py_cloud.run(io_params)\n", + "\n", + "benchmarks.append({\"library\": \"h5py\",\n", + " \"format\": \"cloud\",\n", + " \"mean\": results[0],\n", + " \"time\": results[1],\n", + " \"total_requested_bytes\": results[3][\"total_reqs_bytes\"],\n", + " \"total_requests\": results[3][\"total_reqs\"],\n", + " \"avg_req_size\": results[3][\"avg_req_size\"]})\n", + "benchmarks" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "3059ebd8-b110-49c5-9250-2a2cd009338f", + "metadata": {}, + "outputs": [], + "source": [ + "for run in range(5):\n", + " results = h5py_cloud.run(io_params)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "9d30d92b-4192-4da1-8b60-41cc94ca2db1", + "metadata": {}, + "outputs": [], + "source": [ + "df = pd.DataFrame.from_dict(benchmarks)\n", + "df" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "3ff4c22f-7f77-4c69-a84c-f13b0fbba1f2", + "metadata": {}, + "outputs": [], + "source": [ + "import matplotlib.pyplot as plt\n", + "\n", + "fig, ax = plt.subplots(figsize=(10, 6))\n", + "\n", + "for name, group in df.groupby(['library', 'format']):\n", + " library, format = name\n", + " x = f'{library}, {format}'\n", + " y = group['time'].mean()\n", + " ax.bar(f'{library}, {format}', group['time'].mean(), label=f'{library}, {format}', align='center')\n", + " ax.text(x, y + 0.05, f'{group[\"time\"].mean():.2f}', ha='center', va='bottom', color='black', fontsize=12)\n", + " ax.text(x, y - (y/2) - 10, f'Total Requests: {group[\"total_requests\"].mean()}', ha='center', va='bottom', color='black', fontsize=8)\n", + " ax.text(x, y - (y/2.5), f'Total Req Bytes (MB): {round(group[\"total_requested_bytes\"].mean() / (1024*1024) , 2)}', ha='center', va='bottom', color='black', fontsize=8)\n", + "\n", + "# Set labels and title\n", + "ax.set_xlabel('Access Pattern')\n", + "ax.set_ylabel('Time in Seconds')\n", + "ax.set_title(f'mean() on photon data for runs on ATL03, less is better ')\n", + "\n", + "# Rotate x-axis labels for better readability\n", + "plt.xticks(rotation=45, ha='right')\n", + "\n", + "# # Show legend\n", + "# ax.legend()\n", + "\n", + "# Show the plot\n", + "with plt.xkcd():\n", + " # This figure will be in XKCD-style\n", + " fig1 = plt.figure()" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.12.1" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/notebooks/portable-xarray-test.ipynb b/notebooks/portable-xarray-test.ipynb new file mode 100644 index 0000000..4e67969 --- /dev/null +++ b/notebooks/portable-xarray-test.ipynb @@ -0,0 +1,305 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 4, + "id": "48daa283-8e1e-46e3-b4ce-1a0271b86d37", + "metadata": { + "tags": [] + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "The autoreload extension is already loaded. To reload it, use:\n", + " %reload_ext autoreload\n", + "xarray v2024.1.1\n", + "h5py v3.10.0\n", + "s3fs v2024.2.0\n" + ] + } + ], + "source": [ + "%load_ext autoreload\n", + "%autoreload \n", + "\n", + "import sys\n", + "import os\n", + "classes_path = os.path.abspath('../h5tests/')\n", + "sys.path.append(classes_path)\n", + "from xarray_arr_mean import XarrayArrMean\n", + "import pandas as pd\n", + "\n", + "import xarray as xr\n", + "import h5py\n", + "import s3fs\n", + "\n", + "benchmarks = []\n", + "\n", + "for library in (xr, h5py, s3fs):\n", + " print(f'{library.__name__} v{library.__version__}')" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "id": "d6ce77fd-f9cd-48b1-94cd-1fe57f52e11f", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "files = [\n", + " \"s3://its-live-data/cloud-experiments/h5cloud/atl03/big/original/ATL03_20181120182818_08110112_006_02.h5\",\n", + " \"s3://its-live-data/cloud-experiments/h5cloud/atl03/big/original/ATL03_20190219140808_08110212_006_02.h5\",\n", + "]\n", + "xarray_original = XarrayArrMean('atl03-bigsize-original', files=files, store_results=True)" + ] + }, + { + "cell_type": "code", + "execution_count": 24, + "id": "60eeeb1b-9531-4fec-a847-3ca5304c4685", + "metadata": { + "tags": [] + }, + "outputs": [ + { + "data": { + "text/plain": [ + "[{'library': 'xarray',\n", + " 'format': 'cloud',\n", + " 'mean': 1032.984130859375,\n", + " 'time': 176.90762186050415,\n", + " 'total_requested_bytes': 720001152,\n", + " 'total_requests': 100,\n", + " 'avg_req_size': 7200011},\n", + " {'library': 'xarray',\n", + " 'format': 'original',\n", + " 'mean': 1032.984130859375,\n", + " 'time': 1456.8166418075562,\n", + " 'total_requested_bytes': 438520591,\n", + " 'total_requests': 26988,\n", + " 'avg_req_size': 16248}]" + ] + }, + "execution_count": 24, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# don't even try this out of region...\n", + "# takes about ~10 minutes per granule out of region (6+ GB granules)\n", + "io_params ={\n", + " \"fsspec_params\": {},\n", + " \"h5py_params\" : {}\n", + "}\n", + "results = xarray_original.run(io_params)\n", + "benchmarks.append({\"library\": \"xarray\",\n", + " \"format\": \"original\",\n", + " \"mean\": results[0],\n", + " \"time\": results[1],\n", + " \"total_requested_bytes\": results[3][\"total_reqs_bytes\"],\n", + " \"total_requests\": results[3][\"total_reqs\"],\n", + " \"avg_req_size\": results[3][\"avg_req_size\"]})\n", + "benchmarks" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "id": "64c4584f-c527-44bb-8c05-68a96820d1ff", + "metadata": {}, + "outputs": [], + "source": [ + "files = [\n", + " \"s3://its-live-data/cloud-experiments/h5cloud/atl03/big/repacked/ATL03_20181120182818_08110112_006_02_repacked.h5\",\n", + " \"s3://its-live-data/cloud-experiments/h5cloud/atl03/big/repacked/ATL03_20190219140808_08110212_006_02_repacked.h5\",\n", + "]\n", + "xarray_cloud = XarrayArrMean('atl03-bigsize-repacked', files=files, store_results=True)" + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "id": "dfd4e404-0412-4d2f-8eba-ca39a670e369", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "[{'library': 'xarray',\n", + " 'format': 'cloud',\n", + " 'mean': 1032.984130859375,\n", + " 'time': 176.90762186050415,\n", + " 'total_requested_bytes': 720001152,\n", + " 'total_requests': 100,\n", + " 'avg_req_size': 7200011}]" + ] + }, + "execution_count": 13, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# takes about ~90 seconds per granule out of region\n", + "io_params ={\n", + " \"fsspec_params\": {\n", + " # \"skip_instance_cache\": True\n", + " \"cache_type\": \"blockcache\",\n", + " \"block_size\": 8*1024*1024\n", + " },\n", + " \"h5py_params\" : {\n", + " \"driver_kwds\": {\n", + " \"page_buf_size\": 32*1024*1024,\n", + " \"rdcc_nbytes\": 8*1024*1024\n", + " }\n", + "\n", + " }\n", + "}\n", + "\n", + "results = xarray_cloud.run(io_params)\n", + "\n", + "benchmarks.append({\"library\": \"xarray\",\n", + " \"format\": \"cloud\",\n", + " \"mean\": results[0],\n", + " \"time\": results[1],\n", + " \"total_requested_bytes\": results[3][\"total_reqs_bytes\"],\n", + " \"total_requests\": results[3][\"total_reqs\"],\n", + " \"avg_req_size\": results[3][\"avg_req_size\"]})\n", + "benchmarks" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "9d30d92b-4192-4da1-8b60-41cc94ca2db1", + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
\n", + "
" + ], + "text/plain": [ + "Empty DataFrame\n", + "Columns: []\n", + "Index: []" + ] + }, + "execution_count": 3, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "df = pd.DataFrame.from_dict(benchmarks)\n", + "df" + ] + }, + { + "cell_type": "code", + "execution_count": 26, + "id": "3ff4c22f-7f77-4c69-a84c-f13b0fbba1f2", + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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kZHhfLRr104Wi0Xr1q1ztt2xY4e6dOmikJAQeXp6ysvLSw888IDsdrv27dvncvyIiIhs18rUqVPH5djHjx+XJIWFheV4Tl5eXipevLj+/PPPXM99zZo1CggIUPfu3V2WZw0d++abb1yWZ13HkSU8PFxhYWEuteWmZ8+eLsPdypUrp6ZNm2rt2rUu7SwWizp37uyy7J+vQX5rz8maNWvUqlUrRUVFZdvH+fPnsw0R69KlS7aaJOV6/klJSdqyZYu6desmX19f5/LAwMBs5yjl72slt/Nq3bq1bDabcx8vvviizpw5o5iYmDzt40KbNm1ScnKyy5BCSYqKitKtt96a42t955135usY+W2fk44dO8rT09P5/J/vT3BwsCpVqqS33npL48aN044dO7INz83J3r17dfz4cfXq1UseHn//Oi9WrJjuvPNObd68WefPn3fZJqevlZSUlEu+/omJifrss8/UtGlTVatWTVLmdZiVKlXS9OnT81Tv5brYtTg5LX/22We1ZcsWLV26VA899JAGDRp00aGx+ZWSkqJvvvlGd9xxh/z9/V1+vnbo0EEpKSnO4aGNGjXSTz/9pAEDBujrr79WfHx8tv01atRI06dP12uvvabNmzdnG76dV5UrV1aJEiX09NNPa9KkSfrll1/ytX1gYGC2r4uePXvK4XDo22+/lZT5e6V48eLq3Lmzy3nXq1dPERERLr9X8mv//v367bffdN9990lSttf1xIkT2YZFF8T3JfBfIWwBV6jg4GCX597e3vL393f5YJy1PGtGMEk6efKkdu3aJS8vL5dHYGCgjDE6ffq0JOnIkSO6+eab9eeff+rdd9/Vd999py1btjivPUhOTnY5TkhISLYafXx8XNpl/f+fNV7I19c3277/6cyZM4qIiMj2YSosLExWq1VnzpzJd225iYiIyHHZP4+T0+vv4+Pj8vrnt/acnDlzJseZxiIjI53rL/TP8/fx8ZGU/T28UGxsrBwOx0XP/UL5/VrJyY8//qi2bdtKkj766CN9//332rJli5577rk87+Ofsl6Hi71WOb1/QUFB+TpGQcz4dqn3x2Kx6JtvvlG7du00ZswYXX/99SpZsqSGDBmS6/WNlzp/h8Oh2NjYfNVyMfPnz1diYqJ69Oihc+fO6dy5c4qLi1OPHj109OhRrVq1KtftL0dWrTl9z5w9ezbbz0hJKlu2rBo2bKgOHTroww8/VP/+/TVixAiX64ou15kzZ5SRkaH3338/28/XDh06SJLz5+uIESM0duxYbd68WbfddptCQkLUqlUrbd261bm/+fPnq3fv3vr444/VpEkTBQcH64EHHlB0dHS+6rLZbFq/fr3q1aunZ599VjVr1lRkZKReeumlPAW48PDwbMuyfgZkvfYnT57UuXPn5O3tne3co6Ojned9OU6ePClJevLJJ7Pte8CAAZKUbf9X6kyMQE6YjRC4yoSGhuZ64XpoaKgkafHixUpKStKiRYtcpmj/N9PyZu377NmzF20TGxvrbHcxISEh+uGHH2SMcQktMTExysjIuOT2+ZXTh5vo6OgcQ9ylFETtISEhOnHiRLblWT2HBXH+JUqUkMViuei5X6ggvlbmzZsnLy8vffXVVy6B9d9MfZ/1/lzstfrn63Q5s5XltI2vr2+Ok0r8mw+c5cqV09SpUyVJ+/bt02effaaRI0cqLS1NkyZNynGbS52/h4eHSpQocdk1XSirtqFDh+Z4v7ypU6eqXbt2BXKsLLVr15Yk/fzzz6pRo4ZzeUZGhn777Tfde++9l9xHo0aNNGnSJB04cOBfz+BXokQJeXp6qlevXho4cGCObSpUqCApc9TBE088oSeeeELnzp3T6tWr9eyzz6pdu3Y6evSo/P39FRoaqvHjx2v8+PE6cuSIlixZomeeeUYxMTFasWJFvmqrXbu25s2bJ2OMdu3apenTp+uVV16Rn5+fnnnmmVy3zQo7F8r6GZD1NRYaGqqQkJCL1vVvbsOQ9X06YsSIbFP6Z/nnhEvMPIiihJ4t4CrTqVMn/fHHHwoJCVHDhg2zPbJmS8v6ZZX1l20p8x5ZWVMsX47q1atLkv74448c1x8/flwpKSkuH5xy0qpVKyUmJmb7ID5z5kzn+oL06aefusyId/jwYW3cuDHHGecuJT+1X6z3rVWrVlqzZo0zXF24D39//8ueSvtCAQEBatSokRYtWuTSM5eQkKAvv/zSpW1+vlYudk4Wi0VWq9VlOF1ycrJmzZp12efQpEkT+fn5Zbtx7bFjx5xDMd2hfPny2rdvn8sMaGfOnNHGjRsLZP/XXXednn/+edWuXVvbt2+/aLuqVauqdOnSmjt3rsvXb1JSkj7//HPnDIX/1q+//qpNmzbpzjvv1Nq1a7M9WrVqpf/7v//LU69tfjRu3FilSpXKds+mhQsXKjEx8aIfzC+0du1aeXh4qGLFiv+6Hn9/f7Vs2VI7duxQnTp1cvz5mtMfaIoXL67u3btr4MCBOnv2bI43Iy5btqwGDRqkNm3a5PqeX4rFYlHdunX1zjvvqHjx4nnaV0JCgpYsWeKybO7cufLw8NAtt9wiKfP3ypkzZ2S323M87wvD0MV+BlysF7Vq1aqqUqWKfvrppxz33bBhQ+6phyKNni3gKjN06FB9/vnnuuWWW/T444+rTp06cjgcOnLkiFauXKlhw4apcePGatOmjby9vXXvvfdq+PDhSklJ0Ycffpht2FF+lClTRhUrVtTmzZs1ZMiQbOuzrmdo2bJlrvt54IEHNHHiRPXu3VuHDh1S7dq1tWHDBo0aNUodOnRQ69atL7vGnMTExOiOO+5Qv379FBcXp5deekm+vr4aMWJEvveVn9pr166tdevW6csvv1SpUqUUGBioqlWr6qWXXtJXX32lli1b6sUXX1RwcLDmzJmjpUuXasyYMbLZbAVy3q+++qrat2+vNm3aaNiwYbLb7XrzzTcVEBDg0juZn6+V2rVra9GiRfrwww/VoEEDeXh4qGHDhurYsaPGjRunnj17qn///jpz5ozGjh3rEuDyq3jx4nrhhRf07LPP6oEHHtC9996rM2fO6OWXX5avr69eeumly953bnr16qXJkyfr/vvvV79+/XTmzBmNGTMm30MUs+zatUuDBg3SXXfdpSpVqsjb21tr1qzRrl27cu2V8PDw0JgxY3TfffepU6dOevjhh5Wamqq33npL586d0xtvvHG5p+giq1dr+PDhOd7jLCEhQd98841mz56txx57LF/7/ucU+FmaN2+ukiVLasyYMerVq5cefvhh3Xvvvfr99981fPhwtWnTRu3bt3e279+/v4KCgtSoUSOFh4fr9OnTWrBggebPn6+nnnrKpVdr+vTpevDBBzVt2rRs1/tdyrvvvqubbrpJN998sx599FGVL19eCQkJ2r9/v7788kutWbNGktS5c2fVqlVLDRs2VMmSJXX48GGNHz9e5cqVU5UqVRQXF6eWLVuqZ8+eqlatmgIDA7VlyxatWLEiTyHyQl999ZU++OADde3aVRUrVpQxRosWLdK5c+fUpk2bS24fEhKiRx99VEeOHNF1112nZcuW6aOPPtKjjz6qsmXLSpLuuecezZkz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Xq5cOf35558FVhgAAAAAFGX57tlyOByy2+3Zlh87dkyBgYEFUhQAAAAAFHX5Dltt2rTR+PHjnc8tFosSExP10ksvqUOHDgVZGwAAAAAUWfkeRvjOO++oZcuWqlGjhlJSUtSzZ0/9/vvvCg0N1aeffuqOGgEAAACgyMl32IqMjNTOnTv16aefavv27XI4HOrbt6/uu+8+lwkzAAAAAOBalu+wJUl+fn566KGH9NBDDxV0PQAAAABwVbissPXnn3/q+++/V0xMjBwOh8u6IUOGFEhhAAAAAFCU5TtsTZs2TY888oi8vb0VEhIii8XiXGexWAhbAAAAAKDLCFsvvviiXnzxRY0YMUIeHvmezBAAAAAArgn5Tkvnz5/XPffcQ9ACAAAAgFzkOzH17dtXCxYscEctAAAAAHDVyPcwwtGjR6tTp05asWKFateuLS8vL5f148aNK7DiAAAAAKCoynfYGjVqlL7++mtVrVpVkrJNkAEAAAAAuIywNW7cOH3yySfq06ePG8oBAAAAgKtDvq/Z8vHxUbNmzdxRCwAAAABcNfIdth577DG9//777qgFAAAAAK4a+R5G+OOPP2rNmjX66quvVLNmzWwTZCxatKjAigMAAACAoirfYat48eLq1q2bO2oBAAAAgKtGvsPWtGnT3FEHAAAAAFxV8n3N1n/tzz//1P3336+QkBD5+/urXr162rZtm3O9MUYjR45UZGSk/Pz81KJFC+3Zs8dlH6mpqRo8eLBCQ0MVEBCgLl266NixY//1qQAAAAC4huSpZ+v666/XN998oxIlSqh+/fq53k9r+/btBVZcbGysmjVrppYtW2r58uUKCwvTH3/8oeLFizvbjBkzRuPGjdP06dN13XXX6bXXXlObNm20d+9eBQYGSpKGDh2qL7/8UvPmzVNISIiGDRumTp06adu2bfL09CywegEAAAAgS57C1u233y4fHx9JUteuXd1Zj4s333xTUVFRLkMXy5cv7/y/MUbjx4/Xc88957yObMaMGQoPD9fcuXP18MMPKy4uTlOnTtWsWbPUunVrSdLs2bMVFRWl1atXq127dv/Z+QAAAAC4dliMMSYvDR966CG9++67zt6i/0KNGjXUrl07HTt2TOvXr1fp0qU1YMAA9evXT5J04MABVapUSdu3b1f9+vWd291+++0qXry4ZsyYoTVr1qhVq1Y6e/as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stlscjgcqlevngYPHqwmTZooIyNDQ4YM0fbt29WmTRs999xz8vX1LeTqgaKDYYTANSJrqEj9+vUVEhKiTz/91LnO09NT9evXV5UqVbRq1SoNHjy4sMoEAPwHsoLW22+/rTlz5mjevHnavXu37rjjDn355ZcaOXKkvv32W1mtVr333nuqUKGCTpw4IR8fn0KuHChaCFvANWD06NF69913lZKSIm9vb3Xu3FkrV67UzJkznW28vLxUq1Ytbdy4UdOmTSvEagEA/4WYmBht3LhRr7zyipo1a6avvvpKEydO1P33369Tp07plVde0aZNm2S1WjV79mxNmTKF63qBfCJsAdeA+Ph4DRs2TNOnT5eXl5eGDRumsLAwvfvuu+rVq5cmTZqk22+/XUePHlWjRo3k4eEhu91e2GUDANwoLCxMw4cPV/v27bVz504NHDhQr732mj788EPdeeed2rx5swYPHqytW7fK09NTHh4ecjgcXM8L5IO1sAsAULAuvOA5y+jRoxUYGKiBAwfKbrdr4MCBeuedd7Ro0SLNnDlT+/fvV2hoqBYtWuT8ZZo1KxUAoOjL6XeDJF1//fXy8vLSjBkzVKdOHfXr10+SFBwcrBtvvFE333yzrr/+emf7nPYB4OIIW8BVJusX4f79+1W5cmXn8meffVYOh0NDhgyRxWLRgAEDNHjwYA0ePFgJCQkKDAyUJGVkZMhq5UcDAFwtjDHO3w1Tp07VsWPH5OnpqREjRsjLy0uSlJqaqmPHjunQoUOqWrWqVq5cqS5dumjw4MGyWCwXDWsAcsd3DXAVWr58ua677jp9+eWXLsuff/55PfPMM3ryySc1ffp0JSYmSpIzaBljCFoAcBW5cNbB5557To8//rg2bdqkt956S7fccov27t0rSWrYsKG8vLzUuXNn1axZU3v37tWAAQOc12gRtIDLw9TvwFXqf//7nxYuXKjZs2erU6dOzl+4P//8sxo3bqyUlBQtWrRIXbt2LexSAQBudvLkSfXv318jR45U7dq1debMGd16663y8fHRZ599psqVK2vZsmXat2+fkpKS9PTTT8tqtcputzOsHPgXCFtAEZfb0I7//e9/mjdvnubNm6dOnTpJkvbu3atZs2apUqVK6tWrFz1ZAHCVWblypVq0aCFvb29J0vjx4zV58mSVKVNGM2fOVKlSpSRJsbGxuummm+Tt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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "import matplotlib.pyplot as plt\n", + "\n", + "fig, ax = plt.subplots(figsize=(10, 6))\n", + "\n", + "for name, group in df.groupby(['library', 'format']):\n", + " library, format = name\n", + " x = f'{library}, {format}'\n", + " y = group['time'].mean()\n", + " ax.bar(f'{library}, {format}', group['time'].mean(), label=f'{library}, {format}', align='center')\n", + " ax.text(x, y + 0.05, f'{group[\"time\"].mean():.2f}', ha='center', va='bottom', color='black', fontsize=12)\n", + " ax.text(x, y - (y/2) - 10, f'Total Requests: {group[\"total_requests\"].mean()}', ha='center', va='bottom', color='black', fontsize=8)\n", + " ax.text(x, y - (y/2.5), f'Total Req Bytes (MB): {round(group[\"total_requested_bytes\"].mean() / (1024*1024) , 2)}', ha='center', va='bottom', color='black', fontsize=8)\n", + "\n", + "# Set labels and title\n", + "ax.set_xlabel('Access Pattern')\n", + "ax.set_ylabel('Time in Seconds')\n", + "ax.set_title(f'mean() on photon data for runs on ATL03, less is better ')\n", + "\n", + "# Rotate x-axis labels for better readability\n", + "plt.xticks(rotation=45, ha='right')\n", + "\n", + "# # Show legend\n", + "# ax.legend()\n", + "\n", + "# Show the plot\n", + "with plt.xkcd():\n", + " # This figure will be in XKCD-style\n", + " fig1 = plt.figure()" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.12.1" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +}