diff --git a/activitysim/core/configuration/network.py b/activitysim/core/configuration/network.py index 480433308b..f84080a1be 100644 --- a/activitysim/core/configuration/network.py +++ b/activitysim/core/configuration/network.py @@ -125,6 +125,20 @@ class TAZ_Settings(PydanticBase): This is treated as a fallback for the raw input data, if ZARR format data is not available. + + As an alternative to OMX, skim files can instead be provided in Parquet + format (using a ``.parquet`` or ``.pq`` file extension). The input format is + auto-detected from the file extension, so no other settings need to + change to use Parquet input. Parquet skim files should have an origin + column and a destination column (the first two columns in the file), + followed by one column for each named skim matrix (matching the naming + conventions used for OMX skims, including double-underscore delimited + time periods). Parquet skim data may be dense (one row for every + origin-destination combination, sorted in row-major or column-major order + using any stable zone-ID order) or sparse (only some origin-destination + combinations present, in any order). Parquet inputs are supported by both + the legacy skim-dictionary loaders and Sharrow when Sharrow 2.16 or newer + is installed. """ zarr: str = None @@ -219,10 +233,11 @@ class NetworkSettings(PydanticReadable, extra="forbid"): """Instructions for how to load and pre-process skim matrices. If given as a string or a list of strings, it is interpreted as the location - for OMX file(s), either as a single file or as a glob-matching pattern for - multiple files. The time period for the matrix must be represented at the end - of the matrix name and be seperated by a double_underscore (e.g. `BUS_IVT__AM` - indicates base skim BUS_IVT with a time period of AM. + for OMX or Parquet skim file(s), either as a single file or as a glob-matching + pattern for multiple files. Formats are detected from each file's extension + and may be mixed. The time period for the matrix must be represented at the + end of the matrix name and be separated by a double underscore (e.g. + `BUS_IVT__AM` indicates base skim BUS_IVT with a time period of AM). Alternatively, this can be given as a nested dictionary defined via the TAZ_Settings class, which allows for ZARR transformation and pre-processing. diff --git a/activitysim/core/skim_dataset.py b/activitysim/core/skim_dataset.py index 12a132f16a..51cebaa4dc 100644 --- a/activitysim/core/skim_dataset.py +++ b/activitysim/core/skim_dataset.py @@ -18,6 +18,7 @@ from activitysim.core import flow as __flow # noqa: 401 from activitysim.core import workflow from activitysim.core.input import read_input_file +from activitysim.core.skim_parquet import SPARSE, ParquetSkimFile, is_parquet_file logger = logging.getLogger(__name__) @@ -698,6 +699,235 @@ def load_sparse_maz_skims( return dataset +def _matrix_time_periods(matrix_names, ignore): + """Collect the time-period pages physically present in a source group.""" + if isinstance(ignore, str): + ignore = [ignore] + + available_periods = {} + for matrix_name in matrix_names: + if ignore and any(re.match(pattern, matrix_name) for pattern in ignore): + continue + base_name, separator, period_name = matrix_name.partition("__") + if separator: + available_periods.setdefault(base_name, set()).add(period_name) + return available_periods + + +def _mask_synthetic_time_periods(dataset, available_periods): + """Replace loader-created zero pages with missing values before merging.""" + + for base_name, periods in available_periods.items(): + if base_name not in dataset or "time_period" not in dataset[base_name].dims: + continue + if not set(dataset.time_period.values).issubset(periods): + dataset[base_name] = dataset[base_name].where( + dataset.time_period.isin(list(periods)) + ) + return dataset + + +def _restore_synthetic_time_periods(dataset, available_periods): + """Restore zero pages for periods absent from every physical source.""" + for base_name, periods in available_periods.items(): + if base_name not in dataset or "time_period" not in dataset[base_name].dims: + continue + if not set(dataset.time_period.values).issubset(periods): + dataset[base_name] = dataset[base_name].where( + dataset.time_period.isin(list(periods)), 0 + ) + return dataset + + +def _zero_fill_sparse_parquet(dataset, parquet_sources, ignore): + """Match the legacy loader's zero fill for absent sparse OD pairs.""" + if isinstance(ignore, str): + ignore = [ignore] + + # Sharrow's duplicate-column behavior is last-file-wins, so use the same + # source when selecting the OD pairs that are physically present. + matrix_sources = {} + for _, parquet_file in parquet_sources: + for matrix_name in parquet_file.data_cols: + if ignore and any(re.match(pattern, matrix_name) for pattern in ignore): + continue + matrix_sources[matrix_name] = parquet_file + + presence_by_file = {} + for matrix_name, parquet_file in matrix_sources.items(): + if parquet_file.layout != SPARSE: + continue + + presence = presence_by_file.get(parquet_file) + if presence is None: + presence = np.zeros(parquet_file.shape, dtype=bool) + presence[parquet_file._orig_idx, parquet_file._dest_idx] = True + presence = xr.DataArray( + presence, + dims=("otaz", "dtaz"), + coords={ + "otaz": parquet_file.zone_ids, + "dtaz": parquet_file.zone_ids, + }, + ) + presence_by_file[parquet_file] = presence + + base_name, separator, period_name = matrix_name.partition("__") + if separator: + if base_name not in dataset: + continue + # Preserve explicit NaNs at present OD pairs and other periods; + # only combinations absent from this physical page become zero. + keep_value = presence | (dataset.time_period != period_name) + dataset[base_name] = dataset[base_name].where(keep_value, 0) + elif matrix_name in dataset: + dataset[matrix_name] = dataset[matrix_name].where(presence, 0) + return dataset + + +def _load_skim_dataset_from_sources( + skim_file_paths, + *, + time_periods, + max_float_precision, + ignore, + parquet_file_metadata=None, +): + """ + Load OMX and/or Parquet skim files into one Sharrow-compatible Dataset. + + Parquet index columns are identified from the first two columns in each + file, consistent with the legacy skim reader. Files with different index + column names are loaded in separate groups and aligned by their zone labels. + + Returns + ------- + dataset : xarray.Dataset + omx_file_handles : list + Open OMX handles retained for the optimized shared-memory reload path. + """ + omx_file_paths = [f for f in skim_file_paths if not is_parquet_file(f)] + parquet_file_paths = [f for f in skim_file_paths if is_parquet_file(f)] + omx_file_handles = [] + datasets = [] + + try: + if omx_file_paths: + omx_file_handles = [ + openmatrix.open_file(f, mode="r") for f in omx_file_paths + ] + omx_dataset = sh.dataset.from_omx_3d( + omx_file_handles, + index_names=("otaz", "dtaz", "time_period"), + time_periods=time_periods, + max_float_precision=max_float_precision, + ignore=ignore, + ) + omx_matrix_names = [ + matrix_name + for handle in omx_file_handles + for matrix_name in handle.listMatrices() + ] + datasets.append((omx_dataset, omx_matrix_names)) + + if parquet_file_paths: + if not hasattr(sh.dataset, "from_parquet_3d"): + raise ImportError( + "Parquet skims with Sharrow require Sharrow 2.16 or newer" + ) + metadata_by_path = { + os.fspath(path): metadata + for path, metadata in (parquet_file_metadata or {}).items() + } + parquet_groups = {} + for file_path in parquet_file_paths: + parquet_file = metadata_by_path.get(os.fspath(file_path)) + if parquet_file is None: + parquet_file = ParquetSkimFile(file_path) + # Load sparse files independently. Sharrow derives each sparse + # axis only from labels present on that axis, so grouping a + # sparse file that omits an entire origin or destination can + # otherwise discard valid rows from another file in the group. + sparse_source = file_path if parquet_file.layout == SPARSE else None + group_key = ( + parquet_file.orig_col, + parquet_file.dest_col, + sparse_source, + ) + parquet_groups.setdefault(group_key, []).append( + (file_path, parquet_file) + ) + + for (orig_col, dest_col, _), parquet_sources in parquet_groups.items(): + file_paths = [source[0] for source in parquet_sources] + parquet_dataset = sh.dataset.from_parquet_3d( + file_paths, + index_names=(orig_col, dest_col, "time_period"), + time_periods=time_periods, + max_float_precision=max_float_precision, + ignore=ignore, + ) + + # Rename through temporary names so even swapped source names + # (e.g. dtaz/otaz) cannot collide during the rename. + parquet_dataset = parquet_dataset.rename( + { + orig_col: "__activitysim_parquet_origin__", + dest_col: "__activitysim_parquet_destination__", + } + ).rename( + { + "__activitysim_parquet_origin__": "otaz", + "__activitysim_parquet_destination__": "dtaz", + } + ) + # Sparse xarray expansion can omit an entire coordinate when no + # row uses it. Normalize both dimensions to the full zone set, + # which also gives dense nonascending inputs legacy-compatible + # canonical ordering. + parquet_zone_ids = parquet_sources[0][1].zone_ids + parquet_dataset = parquet_dataset.reindex( + otaz=parquet_zone_ids, dtaz=parquet_zone_ids + ) + parquet_dataset = _zero_fill_sparse_parquet( + parquet_dataset, parquet_sources, ignore + ) + parquet_matrix_names = [ + matrix_name + for _, parquet_file in parquet_sources + for matrix_name in parquet_file.data_cols + ] + datasets.append((parquet_dataset, parquet_matrix_names)) + + if not datasets: + raise ValueError("no OMX or Parquet skim files were provided") + if len(datasets) == 1: + dataset = datasets[0][0] + else: + all_available_periods = {} + masked_datasets = [] + for source_dataset, matrix_names in datasets: + source_periods = _matrix_time_periods(matrix_names, ignore) + for base_name, periods in source_periods.items(): + all_available_periods.setdefault(base_name, set()).update(periods) + masked_datasets.append( + _mask_synthetic_time_periods(source_dataset, source_periods) + ) + dataset = xr.merge(masked_datasets, compat="no_conflicts", join="outer") + dataset = _restore_synthetic_time_periods(dataset, all_available_periods) + + # SkimDataset expects this coordinate even when all source matrices are + # time-agnostic and therefore do not otherwise create the dimension. + if "time_period" not in dataset.coords: + dataset = dataset.assign_coords(time_period=time_periods) + + return dataset, omx_file_handles + except Exception: + for handle in omx_file_handles: + handle.close() + raise + + def load_skim_dataset_to_shared_memory(state, skim_tag="taz") -> xr.Dataset: """ Load skims from disk into shared memory. @@ -718,11 +948,12 @@ def load_skim_dataset_to_shared_memory(state, skim_tag="taz") -> xr.Dataset: if network_los_preload is None: raise ValueError("missing network_los_preload") - # find which OMX files are to be used. + # Find the source skim files to use; formats may be mixed. omx_file_paths = state.filesystem.expand_input_file_list( network_los_preload.omx_file_names(skim_tag), ) omx_file_handles = [] + source_has_parquet = any(is_parquet_file(f) for f in omx_file_paths) zarr_file = network_los_preload.zarr_file_name(skim_tag) if state.settings.disable_zarr: @@ -834,23 +1065,22 @@ def _should_ignore(ignore, x): d = sh.dataset.from_zarr_with_attr(zarr_file) zarr_write_time = d.attrs.get("ZARR_WRITE_TIME", 0) if zarr_write_time < latest_file_modification_time(omx_file_paths): - logger.warning("zarr skims older than omx, not using them") + logger.warning("zarr skims older than source skims, not using them") do_not_save_zarr = True d = None else: d = d.max_float_precision(max_float_precision) if d is None: if zarr_file and not do_not_save_zarr: - logger.info("did not find zarr skims, loading omx") - omx_file_handles = [ - openmatrix.open_file(f, mode="r") for f in omx_file_paths - ] - d = sh.dataset.from_omx_3d( - omx_file_handles, - index_names=("otaz", "dtaz", "time_period"), + logger.info("did not find zarr skims, loading source skim files") + d, omx_file_handles = _load_skim_dataset_from_sources( + omx_file_paths, time_periods=time_periods, max_float_precision=max_float_precision, ignore=state.settings.omx_ignore_patterns, + parquet_file_metadata=network_los_preload.skims_info[ + skim_tag + ].parquet_files, ) if zarr_file: @@ -950,11 +1180,15 @@ def _should_ignore(ignore, x): logger.info( "store_skims_in_shm is False, keeping skims in process-local memory" ) + for f in omx_file_handles: + f.close() return d else: logger.info("writing skims to shared memory") - if dask_required: - # setting `load` to True uses dask to load the data into memory + if dask_required or source_has_parquet: + # Parquet-backed datasets cannot use reload_from_omx_3d, so copy + # their already-loaded data into shared memory. The same path is + # required when coordinate realignment created a dask graph. d_shared_mem = d.shm.to_shared_memory(backing, mode="r", load=True) else: # setting `load` to false then calling `reload_from_omx_3d` avoids diff --git a/activitysim/core/skim_dict_factory.py b/activitysim/core/skim_dict_factory.py index 2ca90c4c66..97aa396cb6 100644 --- a/activitysim/core/skim_dict_factory.py +++ b/activitysim/core/skim_dict_factory.py @@ -15,6 +15,7 @@ from activitysim.core import skim_dictionary, util from activitysim.core.exceptions import TableTypeError +from activitysim.core.skim_parquet import ParquetSkimFile, is_parquet_file logger = logging.getLogger(__name__) @@ -60,18 +61,24 @@ def __init__(self, state, skim_tag, network_los): skim_tag: str (e.g. 'TAZ') dtype_name: str (e.g. 'float32') - omx_manifest: dict dict mapping { omx_key: omx_file_name } - omx_shape: 2D tuple shape of omx matrix: (, ) - num_skims: int total number of individual skim matrices in omx files + omx_manifest: dict dict mapping { omx_key: skim_file_name }, whether the skim + file is an omx file or a parquet file + omx_shape: 2D tuple shape of skim matrix: (, ) + num_skims: int total number of individual skim matrices in omx/parquet files skim_data_shape: 3D tuple (num_skims, omx_shape[0], omx_shape[1]) if ROW_MAJOR_LAYOUT - offset_map: dict or None 1D ndarray as returned by omx_file.mapentries, if omx file has mappings - offset_map_name: str name of offset_map in omx_filecorresponding to offset_map, if there was one + offset_map: dict or None 1D ndarray as returned by omx_file.mapentries, if omx file has + mappings, or the (sorted) zone ids found in a parquet skim file + offset_map_name: str name of offset_map in omx_file corresponding to offset_map, if there was one omx_keys: dict dict mapping skim key (str or tuple) to skim key in omx file {DISTWALK: DISTWALK, ('DRV_COM_WLK_BOARDS', 'AM'): DRV_COM_WLK_BOARDS__AM, ...} base_keys: list of str e.g. 'BIKEDIST' or 'SOVTOLL_VTOLL' (base key of 3d skim) block_offsets: dict dict mapping skim key tuple to offset + Skim files can be in either OMX or Parquet format; the format is auto-detected + from each file's extension (``.omx`` vs ``.parquet``/``.pq``), and OMX and Parquet + files can be freely mixed within the list of files for a single skim_tag. + Parameters ---------- skim_tag @@ -92,6 +99,10 @@ def __init__(self, state, skim_tag, network_los): self.base_keys = None self.block_offsets = None + # cache of ParquetSkimFile instances, keyed by file path, so files + # opened during load_skim_info are not re-parsed when reading data + self.parquet_files = {} + if skim_tag: self.load_skim_info(state, skim_tag) @@ -119,6 +130,44 @@ def load_skim_info(self, state, skim_tag): for omx_file_path in self.omx_file_paths: logger.debug(f"load_skim_info {skim_tag} reading {omx_file_path}") + if is_parquet_file(omx_file_path): + # Skim data provided as a parquet file (auto-detected by extension) + # instead of an omx file. The file is inspected here (and cached) + # to determine its zone list, shape, and dense/sparse layout. + parquet_skim_file = ParquetSkimFile(omx_file_path) + self.parquet_files[omx_file_path] = parquet_skim_file + + # Check the shape of the skims, same as is done for omx files below. + if self.omx_shape is None: + self.omx_shape = parquet_skim_file.shape + else: + assert ( + self.omx_shape == parquet_skim_file.shape + ), f"Mismatch shape {self.omx_shape} != {parquet_skim_file.shape}" + + for skim_name in parquet_skim_file.data_cols: + if skim_name in self.omx_manifest: + warnings.warn( + f"duplicate skim '{skim_name}' found in {self.omx_manifest[skim_name]} and {omx_file_path}" + ) + self.omx_manifest[skim_name] = omx_file_path + + # The origin/destination (zone id) values found in the parquet file + # serve the same purpose as an omx file's offset mapping. Each parquet + # file is checked independently (it need not have zones in the same + # order as other files) but the set of zone ids found must match. + if self.offset_map is None: + self.offset_map_name = f"{omx_file_path} zone ids" + self.offset_map = parquet_skim_file.zone_ids + assert len(self.offset_map) == self.omx_shape[0] + else: + if not np.array_equal(self.offset_map, parquet_skim_file.zone_ids): + raise RuntimeError( + f"Mismatched zone ids in parquet skim file {omx_file_path}: " + f"expected zone ids consistent with {self.offset_map_name}" + ) + continue + with omx.open_file(omx_file_path, mode="r") as omx_file: # Check the shape of the skims. All skim files loaded within this @@ -154,13 +203,14 @@ def load_skim_info(self, state, skim_tag): # mapping, although it can appear multiple times (e.g. once in # each file). for m in omx_file.listMappings(): + omx_zone_ids = np.asarray(omx_file.mapentries(m)) if self.offset_map is None: self.offset_map_name = m - self.offset_map = omx_file.mapentries(self.offset_map_name) + self.offset_map = omx_zone_ids assert len(self.offset_map) == self.omx_shape[0] else: # don't really expect more than one, but ok if they are all the same - if not (self.offset_map == omx_file.mapentries(m)): + if not np.array_equal(self.offset_map, omx_zone_ids): raise RuntimeError( f"Multiple mappings in omx file: {self.offset_map_name} != {m}" ) @@ -322,7 +372,7 @@ def load_skim_info(self, state, skim_tag): def _read_skims_from_omx(self, skim_info, skim_data): """ - read skims from omx file into skim_data + read skims from omx and/or parquet files into skim_data """ skim_tag = skim_info.skim_tag @@ -334,6 +384,35 @@ def _read_skims_from_omx(self, skim_info, skim_data): logger.info(f"_read_skims_from_omx {omx_file_path}") + if is_parquet_file(omx_file_path): + parquet_skim_file = skim_info.parquet_files.get(omx_file_path) + if parquet_skim_file is None: + parquet_skim_file = ParquetSkimFile(omx_file_path) + skim_info.parquet_files[omx_file_path] = parquet_skim_file + for skim_key, omx_key in omx_keys.items(): + if omx_manifest[omx_key] == omx_file_path: + offset = skim_info.block_offsets[skim_key] + logger.debug( + f"_read_skims_from_omx (parquet) file {omx_file_path} " + f"omx_key {omx_key} skim_key {skim_key} to offset {offset}" + ) + + if skim_dictionary.ROW_MAJOR_LAYOUT: + a = skim_data[offset, :, :] + else: + a = skim_data[:, :, offset] + + a[:] = parquet_skim_file.read_matrix( + omx_key, dtype=skim_info.dtype_name + ) + + num_skims_loaded += 1 + + logger.info( + f"_read_skims_from_omx loaded {num_skims_loaded} skims from {omx_file_path}" + ) + continue + # read skims into skim_data with omx.open_file(omx_file_path, mode="r") as omx_file: for skim_key, omx_key in omx_keys.items(): diff --git a/activitysim/core/skim_parquet.py b/activitysim/core/skim_parquet.py new file mode 100644 index 0000000000..a350d815e8 --- /dev/null +++ b/activitysim/core/skim_parquet.py @@ -0,0 +1,176 @@ +# ActivitySim +# See full license in LICENSE.txt. +from __future__ import annotations + +import logging +import os + +import numpy as np +import pyarrow.parquet as pq + +logger = logging.getLogger(__name__) + +PARQUET_SUFFIXES = (".parquet", ".pq") + +# layout tags +ROW_MAJOR = "row_major" +COL_MAJOR = "col_major" +SPARSE = "sparse" + + +def is_parquet_file(file_path): + """ + Return True if file_path appears to be a parquet skim file, based on its extension. + + Parameters + ---------- + file_path : str or Path + + Returns + ------- + bool + """ + return os.fspath(file_path).lower().endswith(PARQUET_SUFFIXES) + + +class ParquetSkimFile: + """ + Inspect and read a single skim matrix table stored in parquet format. + + The parquet file is expected to have an origin column and a destination column + (the first two columns in the file, whatever their names), followed by one + column per named skim matrix (analogous to the matrices in an omx file). The + origin/destination values are used to determine the (square) shape of the + matrices, and whether the data is arranged 'densely' (i.e. every combination + of origin and destination is present exactly once) in row-major or column-major + order using any stable zone-ID order, or is instead 'sparse' (i.e. not every + combination is present, or the dense data is not sorted in row-major or + column-major order). + """ + + def __init__(self, file_path): + self.file_path = file_path + + parquet_file = pq.ParquetFile(file_path) + column_names = list(parquet_file.schema_arrow.names) + if len(column_names) < 3: + raise ValueError( + f"parquet skim file {file_path} must have at least 3 columns " + f"(origin, destination, and at least one data column), " + f"found {len(column_names)}: {column_names}" + ) + + self.orig_col = column_names[0] + self.dest_col = column_names[1] + self.data_cols = column_names[2:] + + od_table = parquet_file.read(columns=[self.orig_col, self.dest_col]) + origins = od_table[self.orig_col].to_numpy(zero_copy_only=False) + destinations = od_table[self.dest_col].to_numpy(zero_copy_only=False) + + zone_ids = np.unique(np.concatenate([origins, destinations])) + self.zone_ids = zone_ids + self.n_zones = len(zone_ids) + + self.shape = (self.n_zones, self.n_zones) + + n_rows = len(origins) + if n_rows == 0: + raise ValueError(f"parquet skim file {file_path} contains no rows") + self.is_dense = n_rows == self.n_zones * self.n_zones + + orig_idx = np.searchsorted(zone_ids, origins) + dest_idx = np.searchsorted(zone_ids, destinations) + + if self.is_dense: + self.layout, source_order = self._detect_dense_layout(orig_idx, dest_idx) + canonical_order = np.argsort(source_order) + if np.array_equal(canonical_order, np.arange(self.n_zones)): + canonical_order = None + self._dense_reindex = canonical_order + # Dense reads only need the layout. Retaining two n^2 index arrays + # for the lifetime of every skim file can consume gigabytes. + self._orig_idx = None + self._dest_idx = None + else: + self.layout = SPARSE + self._dense_reindex = None + self._orig_idx = orig_idx + self._dest_idx = dest_idx + + def _detect_dense_layout(self, orig_idx, dest_idx): + """ + Determine whether dense data is arranged in row-major or column-major + order, allowing any stable zone order. Returns the layout and the source + zone order expressed as indexes into the canonical sorted zone mapping. + + Raises a ValueError if the data is dense but not sorted in either dense + layout, or if the origin and destination axes use different zone orders. + """ + n = self.n_zones + + expected = np.arange(n) + orig_2d = orig_idx.reshape(n, n) + dest_2d = dest_idx.reshape(n, n) + + # Check the repeated/tiled patterns using reductions that allocate + # O(n) temporary arrays instead of two additional O(n^2) arrays. + row_major_order = orig_2d[:, 0] + if ( + np.array_equal(np.sort(row_major_order), expected) + and np.array_equal(dest_2d[0, :], row_major_order) + and np.all(np.ptp(orig_2d, axis=1) == 0) + and np.all(np.ptp(dest_2d, axis=0) == 0) + ): + return ROW_MAJOR, row_major_order + + # col-major orig/dest patterns are the same as row-major dest/orig + col_major_order = orig_2d[0, :] + if ( + np.array_equal(np.sort(col_major_order), expected) + and np.array_equal(dest_2d[:, 0], col_major_order) + and np.all(np.ptp(orig_2d, axis=0) == 0) + and np.all(np.ptp(dest_2d, axis=1) == 0) + ): + return COL_MAJOR, col_major_order + + raise ValueError( + f"parquet skim file {self.file_path} appears to contain dense data " + f"(one row for every origin-destination pair) but the rows are not " + f"sorted in row-major or column-major order. Dense parquet skim data " + f"must be sorted so it can be read efficiently; alternatively, omit " + f"rows to store the data in (unsorted or sorted) sparse format." + ) + + def read_matrix(self, column_name, dtype=None): + """ + Read a single named skim matrix from the parquet file as a dense 2D array. + + Parameters + ---------- + column_name : str + dtype : dtype convertible, optional + + Returns + ------- + np.ndarray, shape (n_zones, n_zones) + """ + table = pq.read_table(self.file_path, columns=[column_name]) + values = table[column_name].to_numpy(zero_copy_only=False) + if dtype is not None: + values = values.astype(dtype, copy=False) + + n = self.n_zones + if self.layout == ROW_MAJOR: + matrix = values.reshape(n, n) + elif self.layout == COL_MAJOR: + matrix = values.reshape(n, n, order="F") + else: + # sparse layout (may or may not be sorted); scatter into dense matrix + matrix = np.zeros((n, n), dtype=values.dtype) + matrix[self._orig_idx, self._dest_idx] = values + return matrix + + if self._dense_reindex is not None: + matrix = matrix[self._dense_reindex][:, self._dense_reindex] + return np.ascontiguousarray(matrix) diff --git a/activitysim/core/test/los/configs_1z_parquet/network_los.yaml b/activitysim/core/test/los/configs_1z_parquet/network_los.yaml new file mode 100644 index 0000000000..8dd3273f47 --- /dev/null +++ b/activitysim/core/test/los/configs_1z_parquet/network_los.yaml @@ -0,0 +1,9 @@ +zone_system: 1 + +taz_skims: z1_taz_skims.parquet + +skim_time_periods: + time_window: 1440 + period_minutes: 60 + periods: [0, 6, 11, 16, 20, 24] + labels: ['EA', 'AM', 'MD', 'PM', 'EV'] diff --git a/activitysim/core/test/los/configs_1z_parquet/settings.yaml b/activitysim/core/test/los/configs_1z_parquet/settings.yaml new file mode 100644 index 0000000000..6e94895e71 --- /dev/null +++ b/activitysim/core/test/los/configs_1z_parquet/settings.yaml @@ -0,0 +1,2 @@ + +multiprocess: False diff --git a/activitysim/core/test/los/configs_1z_parquet_multi/network_los.yaml b/activitysim/core/test/los/configs_1z_parquet_multi/network_los.yaml new file mode 100644 index 0000000000..549471e5a0 --- /dev/null +++ b/activitysim/core/test/los/configs_1z_parquet_multi/network_los.yaml @@ -0,0 +1,11 @@ +zone_system: 1 + +taz_skims: + - z1_taz_skims_part1.parquet + - z1_taz_skims_part2.parquet + +skim_time_periods: + time_window: 1440 + period_minutes: 60 + periods: [0, 6, 11, 16, 20, 24] + labels: ['EA', 'AM', 'MD', 'PM', 'EV'] diff --git a/activitysim/core/test/los/configs_1z_parquet_multi/settings.yaml b/activitysim/core/test/los/configs_1z_parquet_multi/settings.yaml new file mode 100644 index 0000000000..6e94895e71 --- /dev/null +++ b/activitysim/core/test/los/configs_1z_parquet_multi/settings.yaml @@ -0,0 +1,2 @@ + +multiprocess: False diff --git a/activitysim/core/test/los/data/z1_taz_skims.parquet b/activitysim/core/test/los/data/z1_taz_skims.parquet new file mode 100644 index 0000000000..1deb5a74af Binary files /dev/null and b/activitysim/core/test/los/data/z1_taz_skims.parquet differ diff --git a/activitysim/core/test/los/data/z1_taz_skims_part1.parquet b/activitysim/core/test/los/data/z1_taz_skims_part1.parquet new file mode 100644 index 0000000000..726a2bfb5f Binary files /dev/null and b/activitysim/core/test/los/data/z1_taz_skims_part1.parquet differ diff --git a/activitysim/core/test/los/data/z1_taz_skims_part2.parquet b/activitysim/core/test/los/data/z1_taz_skims_part2.parquet new file mode 100644 index 0000000000..3978ef1829 Binary files /dev/null and b/activitysim/core/test/los/data/z1_taz_skims_part2.parquet differ diff --git a/activitysim/core/test/test_los.py b/activitysim/core/test/test_los.py index ab4c81e87f..50894899fd 100644 --- a/activitysim/core/test/test_los.py +++ b/activitysim/core/test/test_los.py @@ -84,6 +84,69 @@ def test_one_zone(): ) +def test_one_zone_parquet(): + # same as test_one_zone, but skims are stored in parquet format rather than omx, + # to confirm parquet skims are auto-detected and read correctly + state = add_canonical_dirs("configs_1z_parquet").load_settings() + + network_los = los.Network_LOS(state) + + assert network_los.setting("zone_system") == los.ONE_ZONE + + assert "z1_taz_skims.parquet" in network_los.omx_file_names("taz") + + network_los.load_data() + + od_df = pd.DataFrame({"orig": [5, 23, 23, 23], "dest": [7, 20, 21, 22]}) + + skim_dict = network_los.get_default_skim_dict() + + skims = skim_dict.wrap("orig", "dest") + skims.set_df(od_df) + pdt.assert_series_equal( + skims["DIST"], pd.Series([0.4, 2.55, 1.9, 0.62]).astype(np.float32) + ) + pdt.assert_series_equal( + skims["DISTBIKE"], pd.Series([0.4, 2.55, 1.9, 0.62]).astype(np.float32) + ) + + skims = skim_dict.wrap("dest", "orig") + skims.set_df(od_df) + pdt.assert_series_equal( + skims["DIST"], pd.Series([0.46, 2.45, 1.89, 0.89]).astype(np.float32) + ) + + +def test_one_zone_parquet_multi_file(): + # skims are split across two parquet files, and the second file's rows are + # shuffled (not row-major or column-major), to confirm that each parquet + # skim file is independently inspected for its own layout/zone ordering + state = add_canonical_dirs("configs_1z_parquet_multi").load_settings() + + network_los = los.Network_LOS(state) + + assert network_los.setting("zone_system") == los.ONE_ZONE + + file_names = network_los.omx_file_names("taz") + assert "z1_taz_skims_part1.parquet" in file_names + assert "z1_taz_skims_part2.parquet" in file_names + + network_los.load_data() + + od_df = pd.DataFrame({"orig": [5, 23, 23, 23], "dest": [7, 20, 21, 22]}) + + skim_dict = network_los.get_default_skim_dict() + + skims = skim_dict.wrap("orig", "dest") + skims.set_df(od_df) + pdt.assert_series_equal( + skims["DIST"], pd.Series([0.4, 2.55, 1.9, 0.62]).astype(np.float32) + ) + pdt.assert_series_equal( + skims["DISTBIKE"], pd.Series([0.4, 2.55, 1.9, 0.62]).astype(np.float32) + ) + + def test_two_zone(): state = add_canonical_dirs("configs_2z").load_settings() diff --git a/activitysim/core/test/test_skim_parquet.py b/activitysim/core/test/test_skim_parquet.py new file mode 100644 index 0000000000..76606f40ab --- /dev/null +++ b/activitysim/core/test/test_skim_parquet.py @@ -0,0 +1,305 @@ +# ActivitySim +# See full license in LICENSE.txt. +from __future__ import annotations + +from pathlib import Path + +import numpy as np +import openmatrix +import pandas as pd +import pytest + +from activitysim.core.skim_dataset import _load_skim_dataset_from_sources +from activitysim.core.skim_parquet import ( + COL_MAJOR, + ROW_MAJOR, + SPARSE, + ParquetSkimFile, + is_parquet_file, +) + + +def _dense_row_major_df(zone_ids, values): + n = len(zone_ids) + orig = np.repeat(zone_ids, n) + dest = np.tile(zone_ids, n) + return pd.DataFrame({"orig": orig, "dest": dest, "VALUE": values.flatten()}) + + +def _dense_col_major_df(zone_ids, values): + n = len(zone_ids) + orig = np.tile(zone_ids, n) + dest = np.repeat(zone_ids, n) + # column-major order means dest varies slowest + return pd.DataFrame( + {"orig": orig, "dest": dest, "VALUE": values.flatten(order="F")} + ) + + +@pytest.fixture +def zone_ids(): + return np.array([10, 20, 30, 40]) + + +@pytest.fixture +def values(zone_ids): + n = len(zone_ids) + return np.arange(n * n, dtype="float32").reshape((n, n)) + + +def test_is_parquet_file(): + assert is_parquet_file("foo.parquet") + assert is_parquet_file("foo.PARQUET") + assert is_parquet_file("foo.pq") + assert not is_parquet_file("foo.omx") + assert not is_parquet_file("foo.csv") + + +def test_row_major_dense(tmp_path, zone_ids, values): + df = _dense_row_major_df(zone_ids, values) + file_path = tmp_path / "skims.parquet" + df.to_parquet(file_path, index=False) + + skim_file = ParquetSkimFile(str(file_path)) + assert skim_file.is_dense + assert skim_file.layout == ROW_MAJOR + assert skim_file.shape == (4, 4) + np.testing.assert_array_equal(skim_file.zone_ids, zone_ids) + assert skim_file._orig_idx is None + assert skim_file._dest_idx is None + + matrix = skim_file.read_matrix("VALUE") + np.testing.assert_array_equal(matrix, values) + + +def test_col_major_dense(tmp_path, zone_ids, values): + df = _dense_col_major_df(zone_ids, values) + file_path = tmp_path / "skims.parquet" + df.to_parquet(file_path, index=False) + + skim_file = ParquetSkimFile(str(file_path)) + assert skim_file.is_dense + assert skim_file.layout == COL_MAJOR + assert skim_file._orig_idx is None + assert skim_file._dest_idx is None + + matrix = skim_file.read_matrix("VALUE") + np.testing.assert_array_equal(matrix, values) + + +@pytest.mark.parametrize( + "dataframe_factory,expected_layout", + [ + (_dense_row_major_df, ROW_MAJOR), + (_dense_col_major_df, COL_MAJOR), + ], +) +def test_dense_nonascending_zone_order( + tmp_path, values, dataframe_factory, expected_layout +): + source_zone_ids = np.array([30, 10, 40, 20]) + df = dataframe_factory(source_zone_ids, values) + file_path = tmp_path / "skims.parquet" + df.to_parquet(file_path, index=False) + + skim_file = ParquetSkimFile(file_path) + assert skim_file.layout == expected_layout + + # The legacy loader uses one zone mapping for both dimensions, so dense + # source order is normalized to the canonical ascending zone mapping. + order = np.argsort(source_zone_ids) + np.testing.assert_array_equal(skim_file.zone_ids, source_zone_ids[order]) + np.testing.assert_array_equal( + skim_file.read_matrix("VALUE"), values[order][:, order] + ) + + +def test_sparse_unsorted(tmp_path, zone_ids, values): + # omit one od pair, and shuffle the rows, to force sparse handling + df = _dense_row_major_df(zone_ids, values) + df = df.drop(df.index[5]) + df = df.sample(frac=1, random_state=42).reset_index(drop=True) + file_path = tmp_path / "skims.parquet" + df.to_parquet(file_path, index=False) + + skim_file = ParquetSkimFile(str(file_path)) + assert not skim_file.is_dense + assert skim_file.layout == SPARSE + + matrix = skim_file.read_matrix("VALUE") + expected = values.copy() + # the dropped entry defaults to 0 in the dense reconstruction + dropped_orig_idx, dropped_dest_idx = 1, 1 + expected[dropped_orig_idx, dropped_dest_idx] = 0 + np.testing.assert_array_equal(matrix, expected) + + +def test_sharrow_sparse_parquet_fills_missing_pairs_with_zero( + tmp_path, zone_ids, values +): + df = _dense_row_major_df(zone_ids, values).drop(index=range(4, 8)) + df.loc[1, "VALUE"] = np.nan + file_path = tmp_path / "sparse.parquet" + df.to_parquet(file_path, index=False) + + dataset, omx_handles = _load_skim_dataset_from_sources( + [file_path], + time_periods=["AM", "PM"], + max_float_precision=32, + ignore=None, + ) + + assert omx_handles == [] + expected = values.copy() + expected[1, :] = 0 + expected[0, 1] = np.nan + np.testing.assert_array_equal(dataset.otaz, zone_ids) + np.testing.assert_array_equal(dataset.dtaz, zone_ids) + np.testing.assert_array_equal(dataset.VALUE, expected) + + +def test_sharrow_sparse_file_does_not_truncate_dense_file(tmp_path, zone_ids, values): + sparse = _dense_row_major_df(zone_ids, values).drop(index=range(4, 8)) + sparse_path = tmp_path / "sparse.parquet" + sparse.to_parquet(sparse_path, index=False) + + dense = _dense_row_major_df(zone_ids, values * 10).rename( + columns={"VALUE": "VALUE2"} + ) + dense_path = tmp_path / "dense.parquet" + dense.to_parquet(dense_path, index=False) + + dataset, omx_handles = _load_skim_dataset_from_sources( + [sparse_path, dense_path], + time_periods=["AM", "PM"], + max_float_precision=32, + ignore=None, + ) + + assert omx_handles == [] + expected_sparse = values.copy() + expected_sparse[1, :] = 0 + np.testing.assert_array_equal(dataset.VALUE, expected_sparse) + np.testing.assert_array_equal(dataset.VALUE2, values * 10) + + +def test_dense_unsorted_raises(tmp_path, zone_ids, values): + df = _dense_row_major_df(zone_ids, values) + # shuffle rows so every od pair is present, but not in row-major or + # column-major order -- this must raise, since the code should not + # silently read badly-sorted "dense" data via the optimized path, + # nor should it silently accept a wrong shape via the sparse path. + df = df.sample(frac=1, random_state=42).reset_index(drop=True) + file_path = tmp_path / "skims.parquet" + df.to_parquet(file_path, index=False) + + with pytest.raises(ValueError): + ParquetSkimFile(str(file_path)) + + +def test_multiple_data_columns(tmp_path, zone_ids, values): + df = _dense_row_major_df(zone_ids, values) + df["VALUE2"] = df["VALUE"] * 10 + file_path = tmp_path / "skims.parquet" + df.to_parquet(file_path, index=False) + + skim_file = ParquetSkimFile(str(file_path)) + assert skim_file.data_cols == ["VALUE", "VALUE2"] + + np.testing.assert_array_equal(skim_file.read_matrix("VALUE"), values) + np.testing.assert_array_equal(skim_file.read_matrix("VALUE2"), values * 10) + + +def test_empty_parquet_raises(tmp_path): + file_path = tmp_path / "empty.parquet" + pd.DataFrame(columns=["orig", "dest", "VALUE"]).to_parquet(file_path, index=False) + + with pytest.raises(ValueError, match="contains no rows"): + ParquetSkimFile(file_path) + + +def test_sharrow_parquet_sources(tmp_path, zone_ids, values): + first = _dense_row_major_df(zone_ids, values).rename( + columns={"orig": "from_zone", "dest": "to_zone", "VALUE": "DIST"} + ) + first["TIME__AM"] = first["DIST"] * 2 + first_path = tmp_path / "first.parquet" + first.to_parquet(first_path, index=False) + + # A different pair of index-column names confirms each Parquet source is + # inspected independently before all sources are aligned to otaz/dtaz. + second = _dense_row_major_df(zone_ids, values).rename( + columns={"orig": "O", "dest": "D", "VALUE": "DISTBIKE"} + ) + second_path = tmp_path / "second.parquet" + second.to_parquet(second_path, index=False) + + time_agnostic_dataset, _ = _load_skim_dataset_from_sources( + [second_path], + time_periods=["AM", "PM"], + max_float_precision=32, + ignore=None, + ) + np.testing.assert_array_equal(time_agnostic_dataset.time_period, ["AM", "PM"]) + + dataset, omx_handles = _load_skim_dataset_from_sources( + [first_path, second_path], + time_periods=["AM", "PM"], + max_float_precision=32, + ignore=None, + ) + + assert omx_handles == [] + np.testing.assert_array_equal(dataset.otaz, zone_ids) + np.testing.assert_array_equal(dataset.dtaz, zone_ids) + np.testing.assert_array_equal(dataset.DIST, values) + np.testing.assert_array_equal(dataset.DISTBIKE, values) + np.testing.assert_array_equal(dataset.TIME.sel(time_period="AM"), values * 2) + np.testing.assert_array_equal( + dataset.TIME.sel(time_period="PM"), np.zeros_like(values) + ) + + +def test_sharrow_mixed_omx_parquet_sources(): + data_dir = Path(__file__).parent / "los" / "data" + dataset, omx_handles = _load_skim_dataset_from_sources( + [data_dir / "z1_taz_skims.omx", data_dir / "z1_taz_skims.parquet"], + time_periods=["EA", "AM", "MD", "PM", "EV"], + max_float_precision=32, + ignore=None, + ) + + try: + assert {"DIST", "DISTBIKE", "SOV_TIME"} <= set(dataset.data_vars) + assert float(dataset.DIST.sel(otaz=5, dtaz=7)) == pytest.approx(0.4) + assert float(dataset.DISTBIKE.sel(otaz=23, dtaz=20)) == pytest.approx(2.55) + finally: + for handle in omx_handles: + handle.close() + + +def test_sharrow_mixed_sources_split_time_periods(tmp_path, zone_ids, values): + omx_path = tmp_path / "am.omx" + with openmatrix.open_file(omx_path, mode="w") as omx_file: + omx_file["TIME__AM"] = values * 2 + omx_file.create_mapping("zone_number", zone_ids) + + parquet = _dense_row_major_df(zone_ids, values * 3).rename( + columns={"VALUE": "TIME__PM"} + ) + parquet_path = tmp_path / "pm.parquet" + parquet.to_parquet(parquet_path, index=False) + + dataset, omx_handles = _load_skim_dataset_from_sources( + [omx_path, parquet_path], + time_periods=["AM", "PM"], + max_float_precision=32, + ignore=None, + ) + + try: + np.testing.assert_array_equal(dataset.TIME.sel(time_period="AM"), values * 2) + np.testing.assert_array_equal(dataset.TIME.sel(time_period="PM"), values * 3) + finally: + for handle in omx_handles: + handle.close() diff --git a/pyproject.toml b/pyproject.toml index 6854c027d3..df86451c8f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -26,7 +26,7 @@ dependencies = [ "requests >= 2.7", "scikit-learn >= 1.2", "setuptools>=80.9.0", - "sharrow>=2.15.0", + "sharrow>=2.16.0", "sparse", "tables >= 3.9", # pytables is tables in pypi "xarray >= 2024.05", diff --git a/uv.lock b/uv.lock index 446c40ca02..2311a89646 100644 --- a/uv.lock +++ b/uv.lock @@ -1,5 +1,5 @@ version = 1 -revision = 3 +revision = 2 requires-python = ">=3.10" resolution-markers = [ "python_full_version >= '3.14'", @@ -122,7 +122,7 @@ requires-dist = [ { name = "requests", specifier = ">=2.7" }, { name = "scikit-learn", specifier = ">=1.2" }, { name = "setuptools", specifier = ">=80.9.0" }, - { name = "sharrow", specifier = ">=2.15.0" }, + { name = "sharrow", specifier = ">=2.16.0" }, { name = "sparse" }, { name = "tables", specifier = ">=3.9" }, { name = "xarray", specifier = ">=2024.5" }, @@ -1660,6 +1660,7 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/7d/ed/6bfa4109fcb23a58819600392564fea69cdc6551ffd5e69ccf1d52a40cbc/greenlet-3.2.4-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:8c68325b0d0acf8d91dde4e6f930967dd52a5302cd4062932a6b2e7c2969f47c", size = 271061, upload-time = "2025-08-07T13:17:15.373Z" }, { url = "https://files.pythonhosted.org/packages/2a/fc/102ec1a2fc015b3a7652abab7acf3541d58c04d3d17a8d3d6a44adae1eb1/greenlet-3.2.4-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:94385f101946790ae13da500603491f04a76b6e4c059dab271b3ce2e283b2590", size = 629475, upload-time = "2025-08-07T13:42:54.009Z" }, { url = "https://files.pythonhosted.org/packages/c5/26/80383131d55a4ac0fb08d71660fd77e7660b9db6bdb4e8884f46d9f2cc04/greenlet-3.2.4-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:f10fd42b5ee276335863712fa3da6608e93f70629c631bf77145021600abc23c", size = 640802, upload-time = "2025-08-07T13:45:25.52Z" }, + { url = "https://files.pythonhosted.org/packages/9f/7c/e7833dbcd8f376f3326bd728c845d31dcde4c84268d3921afcae77d90d08/greenlet-3.2.4-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:c8c9e331e58180d0d83c5b7999255721b725913ff6bc6cf39fa2a45841a4fd4b", size = 636703, upload-time = "2025-08-07T13:53:12.622Z" }, { url = "https://files.pythonhosted.org/packages/e9/49/547b93b7c0428ede7b3f309bc965986874759f7d89e4e04aeddbc9699acb/greenlet-3.2.4-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:58b97143c9cc7b86fc458f215bd0932f1757ce649e05b640fea2e79b54cedb31", size = 635417, upload-time = "2025-08-07T13:18:25.189Z" }, { url = "https://files.pythonhosted.org/packages/7f/91/ae2eb6b7979e2f9b035a9f612cf70f1bf54aad4e1d125129bef1eae96f19/greenlet-3.2.4-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c2ca18a03a8cfb5b25bc1cbe20f3d9a4c80d8c3b13ba3df49ac3961af0b1018d", size = 584358, upload-time = "2025-08-07T13:18:23.708Z" }, { url = "https://files.pythonhosted.org/packages/f7/85/433de0c9c0252b22b16d413c9407e6cb3b41df7389afc366ca204dbc1393/greenlet-3.2.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:9fe0a28a7b952a21e2c062cd5756d34354117796c6d9215a87f55e38d15402c5", size = 1113550, upload-time = "2025-08-07T13:42:37.467Z" }, @@ -1670,6 +1671,7 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/a4/de/f28ced0a67749cac23fecb02b694f6473f47686dff6afaa211d186e2ef9c/greenlet-3.2.4-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:96378df1de302bc38e99c3a9aa311967b7dc80ced1dcc6f171e99842987882a2", size = 272305, upload-time = "2025-08-07T13:15:41.288Z" }, { url = "https://files.pythonhosted.org/packages/09/16/2c3792cba130000bf2a31c5272999113f4764fd9d874fb257ff588ac779a/greenlet-3.2.4-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:1ee8fae0519a337f2329cb78bd7a8e128ec0f881073d43f023c7b8d4831d5246", size = 632472, upload-time = "2025-08-07T13:42:55.044Z" }, { url = "https://files.pythonhosted.org/packages/ae/8f/95d48d7e3d433e6dae5b1682e4292242a53f22df82e6d3dda81b1701a960/greenlet-3.2.4-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:94abf90142c2a18151632371140b3dba4dee031633fe614cb592dbb6c9e17bc3", size = 644646, upload-time = "2025-08-07T13:45:26.523Z" }, + { url = "https://files.pythonhosted.org/packages/d5/5e/405965351aef8c76b8ef7ad370e5da58d57ef6068df197548b015464001a/greenlet-3.2.4-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:4d1378601b85e2e5171b99be8d2dc85f594c79967599328f95c1dc1a40f1c633", size = 640519, upload-time = "2025-08-07T13:53:13.928Z" }, { url = "https://files.pythonhosted.org/packages/25/5d/382753b52006ce0218297ec1b628e048c4e64b155379331f25a7316eb749/greenlet-3.2.4-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0db5594dce18db94f7d1650d7489909b57afde4c580806b8d9203b6e79cdc079", size = 639707, upload-time = "2025-08-07T13:18:27.146Z" }, { url = "https://files.pythonhosted.org/packages/1f/8e/abdd3f14d735b2929290a018ecf133c901be4874b858dd1c604b9319f064/greenlet-3.2.4-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2523e5246274f54fdadbce8494458a2ebdcdbc7b802318466ac5606d3cded1f8", size = 587684, upload-time = "2025-08-07T13:18:25.164Z" }, { url = "https://files.pythonhosted.org/packages/5d/65/deb2a69c3e5996439b0176f6651e0052542bb6c8f8ec2e3fba97c9768805/greenlet-3.2.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:1987de92fec508535687fb807a5cea1560f6196285a4cde35c100b8cd632cc52", size = 1116647, upload-time = "2025-08-07T13:42:38.655Z" }, @@ -1680,6 +1682,7 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/44/69/9b804adb5fd0671f367781560eb5eb586c4d495277c93bde4307b9e28068/greenlet-3.2.4-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:3b67ca49f54cede0186854a008109d6ee71f66bd57bb36abd6d0a0267b540cdd", size = 274079, upload-time = "2025-08-07T13:15:45.033Z" }, { url = "https://files.pythonhosted.org/packages/46/e9/d2a80c99f19a153eff70bc451ab78615583b8dac0754cfb942223d2c1a0d/greenlet-3.2.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ddf9164e7a5b08e9d22511526865780a576f19ddd00d62f8a665949327fde8bb", size = 640997, upload-time = "2025-08-07T13:42:56.234Z" }, { url = "https://files.pythonhosted.org/packages/3b/16/035dcfcc48715ccd345f3a93183267167cdd162ad123cd93067d86f27ce4/greenlet-3.2.4-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:f28588772bb5fb869a8eb331374ec06f24a83a9c25bfa1f38b6993afe9c1e968", size = 655185, upload-time = "2025-08-07T13:45:27.624Z" }, + { url = "https://files.pythonhosted.org/packages/31/da/0386695eef69ffae1ad726881571dfe28b41970173947e7c558d9998de0f/greenlet-3.2.4-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:5c9320971821a7cb77cfab8d956fa8e39cd07ca44b6070db358ceb7f8797c8c9", size = 649926, upload-time = "2025-08-07T13:53:15.251Z" }, { url = "https://files.pythonhosted.org/packages/68/88/69bf19fd4dc19981928ceacbc5fd4bb6bc2215d53199e367832e98d1d8fe/greenlet-3.2.4-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c60a6d84229b271d44b70fb6e5fa23781abb5d742af7b808ae3f6efd7c9c60f6", size = 651839, upload-time = "2025-08-07T13:18:30.281Z" }, { url = "https://files.pythonhosted.org/packages/19/0d/6660d55f7373b2ff8152401a83e02084956da23ae58cddbfb0b330978fe9/greenlet-3.2.4-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3b3812d8d0c9579967815af437d96623f45c0f2ae5f04e366de62a12d83a8fb0", size = 607586, upload-time = "2025-08-07T13:18:28.544Z" }, { url = "https://files.pythonhosted.org/packages/8e/1a/c953fdedd22d81ee4629afbb38d2f9d71e37d23caace44775a3a969147d4/greenlet-3.2.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:abbf57b5a870d30c4675928c37278493044d7c14378350b3aa5d484fa65575f0", size = 1123281, upload-time = "2025-08-07T13:42:39.858Z" }, @@ -1690,6 +1693,7 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/49/e8/58c7f85958bda41dafea50497cbd59738c5c43dbbea5ee83d651234398f4/greenlet-3.2.4-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:1a921e542453fe531144e91e1feedf12e07351b1cf6c9e8a3325ea600a715a31", size = 272814, upload-time = "2025-08-07T13:15:50.011Z" }, { url = "https://files.pythonhosted.org/packages/62/dd/b9f59862e9e257a16e4e610480cfffd29e3fae018a68c2332090b53aac3d/greenlet-3.2.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:cd3c8e693bff0fff6ba55f140bf390fa92c994083f838fece0f63be121334945", size = 641073, upload-time = "2025-08-07T13:42:57.23Z" }, { url = "https://files.pythonhosted.org/packages/f7/0b/bc13f787394920b23073ca3b6c4a7a21396301ed75a655bcb47196b50e6e/greenlet-3.2.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:710638eb93b1fa52823aa91bf75326f9ecdfd5e0466f00789246a5280f4ba0fc", size = 655191, upload-time = "2025-08-07T13:45:29.752Z" }, + { url = "https://files.pythonhosted.org/packages/f2/d6/6adde57d1345a8d0f14d31e4ab9c23cfe8e2cd39c3baf7674b4b0338d266/greenlet-3.2.4-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:c5111ccdc9c88f423426df3fd1811bfc40ed66264d35aa373420a34377efc98a", size = 649516, upload-time = "2025-08-07T13:53:16.314Z" }, { url = "https://files.pythonhosted.org/packages/7f/3b/3a3328a788d4a473889a2d403199932be55b1b0060f4ddd96ee7cdfcad10/greenlet-3.2.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d76383238584e9711e20ebe14db6c88ddcedc1829a9ad31a584389463b5aa504", size = 652169, upload-time = "2025-08-07T13:18:32.861Z" }, { url = "https://files.pythonhosted.org/packages/ee/43/3cecdc0349359e1a527cbf2e3e28e5f8f06d3343aaf82ca13437a9aa290f/greenlet-3.2.4-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:23768528f2911bcd7e475210822ffb5254ed10d71f4028387e5a99b4c6699671", size = 610497, upload-time = "2025-08-07T13:18:31.636Z" }, { url = "https://files.pythonhosted.org/packages/b8/19/06b6cf5d604e2c382a6f31cafafd6f33d5dea706f4db7bdab184bad2b21d/greenlet-3.2.4-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:00fadb3fedccc447f517ee0d3fd8fe49eae949e1cd0f6a611818f4f6fb7dc83b", size = 1121662, upload-time = "2025-08-07T13:42:41.117Z" }, @@ -1700,6 +1704,7 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/22/5c/85273fd7cc388285632b0498dbbab97596e04b154933dfe0f3e68156c68c/greenlet-3.2.4-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:49a30d5fda2507ae77be16479bdb62a660fa51b1eb4928b524975b3bde77b3c0", size = 273586, upload-time = "2025-08-07T13:16:08.004Z" }, { url = "https://files.pythonhosted.org/packages/d1/75/10aeeaa3da9332c2e761e4c50d4c3556c21113ee3f0afa2cf5769946f7a3/greenlet-3.2.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:299fd615cd8fc86267b47597123e3f43ad79c9d8a22bebdce535e53550763e2f", size = 686346, upload-time = "2025-08-07T13:42:59.944Z" }, { url = "https://files.pythonhosted.org/packages/c0/aa/687d6b12ffb505a4447567d1f3abea23bd20e73a5bed63871178e0831b7a/greenlet-3.2.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:c17b6b34111ea72fc5a4e4beec9711d2226285f0386ea83477cbb97c30a3f3a5", size = 699218, upload-time = "2025-08-07T13:45:30.969Z" }, + { url = "https://files.pythonhosted.org/packages/dc/8b/29aae55436521f1d6f8ff4e12fb676f3400de7fcf27fccd1d4d17fd8fecd/greenlet-3.2.4-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b4a1870c51720687af7fa3e7cda6d08d801dae660f75a76f3845b642b4da6ee1", size = 694659, upload-time = "2025-08-07T13:53:17.759Z" }, { url = "https://files.pythonhosted.org/packages/92/2e/ea25914b1ebfde93b6fc4ff46d6864564fba59024e928bdc7de475affc25/greenlet-3.2.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:061dc4cf2c34852b052a8620d40f36324554bc192be474b9e9770e8c042fd735", size = 695355, upload-time = "2025-08-07T13:18:34.517Z" }, { url = "https://files.pythonhosted.org/packages/72/60/fc56c62046ec17f6b0d3060564562c64c862948c9d4bc8aa807cf5bd74f4/greenlet-3.2.4-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:44358b9bf66c8576a9f57a590d5f5d6e72fa4228b763d0e43fee6d3b06d3a337", size = 657512, upload-time = "2025-08-07T13:18:33.969Z" }, { url = "https://files.pythonhosted.org/packages/23/6e/74407aed965a4ab6ddd93a7ded3180b730d281c77b765788419484cdfeef/greenlet-3.2.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:2917bdf657f5859fbf3386b12d68ede4cf1f04c90c3a6bc1f013dd68a22e2269", size = 1612508, upload-time = "2025-11-04T12:42:23.427Z" }, @@ -1728,6 +1733,82 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" }, ] +[[package]] +name = "h5py" +version = "3.16.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "numpy", version = "2.3.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/db/33/acd0ce6863b6c0d7735007df01815403f5589a21ff8c2e1ee2587a38f548/h5py-3.16.0.tar.gz", hash = "sha256:a0dbaad796840ccaa67a4c144a0d0c8080073c34c76d5a6941d6818678ef2738", size = 446526, upload-time = "2026-03-06T13:49:08.07Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3a/6b/231413e58a787a89b316bb0d1777da3c62257e4797e09afd8d17ad3549dc/h5py-3.16.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e06f864bedb2c8e7c1358e6c73af48519e317457c444d6f3d332bb4e8fa6d7d9", size = 3724137, upload-time = "2026-03-06T13:47:35.242Z" }, + { url = "https://files.pythonhosted.org/packages/74/f9/557ce3aad0fe8471fb5279bab0fc56ea473858a022c4ce8a0b8f303d64e9/h5py-3.16.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ec86d4fffd87a0f4cb3d5796ceb5a50123a2a6d99b43e616e5504e66a953eca3", size = 3090112, upload-time = "2026-03-06T13:47:37.634Z" }, + { url = "https://files.pythonhosted.org/packages/7a/f5/e15b3d0dc8a18e56409a839e6468d6fb589bc5207c917399c2e0706eeb44/h5py-3.16.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:86385ea895508220b8a7e45efa428aeafaa586bd737c7af9ee04661d8d84a10d", size = 4844847, upload-time = "2026-03-06T13:47:39.811Z" }, + { url = "https://files.pythonhosted.org/packages/cb/92/a8851d936547efe30cc0ce5245feac01f3ec6171f7899bc3f775c72030b3/h5py-3.16.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:8975273c2c5921c25700193b408e28d6bdd0111c37468b2d4e25dcec4cd1d84d", size = 5065352, upload-time = "2026-03-06T13:47:41.489Z" }, + { url = "https://files.pythonhosted.org/packages/2b/ae/f2adc5d0ca9626db3277a3d87516e124cbc5d0eea0bd79bc085702d04f2c/h5py-3.16.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:1677ad48b703f44efc9ea0c3ab284527f81bc4f318386aaaebc5fede6bbae56f", size = 4839173, upload-time = "2026-03-06T13:47:43.586Z" }, + { url = "https://files.pythonhosted.org/packages/64/0b/e0c8c69da1d8838da023a50cd3080eae5d475691f7636b35eff20bb6ef20/h5py-3.16.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:7c4dd4cf5f0a4e36083f73172f6cfc25a5710789269547f132a20975bfe2434c", size = 5076216, upload-time = "2026-03-06T13:47:45.315Z" }, + { url = "https://files.pythonhosted.org/packages/66/35/d88fd6718832133c885004c61ceeeb24dbd6397ef877dbed6b3a64d6a286/h5py-3.16.0-cp310-cp310-win_amd64.whl", hash = "sha256:bdef06507725b455fccba9c16529121a5e1fbf56aa375f7d9713d9e8ff42454d", size = 3183639, upload-time = "2026-03-06T13:47:47.041Z" }, + { url = "https://files.pythonhosted.org/packages/ba/95/a825894f3e45cbac7554c4e97314ce886b233a20033787eda755ca8fecc7/h5py-3.16.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:719439d14b83f74eeb080e9650a6c7aa6d0d9ea0ca7f804347b05fac6fbf18af", size = 3721663, upload-time = "2026-03-06T13:47:49.599Z" }, + { url = "https://files.pythonhosted.org/packages/bf/3b/38ff88b347c3e346cda1d3fc1b65a7aa75d40632228d8b8a5d7b58508c24/h5py-3.16.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c3f0a0e136f2e95dd0b67146abb6668af4f1a69c81ef8651a2d316e8e01de447", size = 3087630, upload-time = "2026-03-06T13:47:51.249Z" }, + { url = "https://files.pythonhosted.org/packages/98/a8/2594cef906aee761601eff842c7dc598bea2b394a3e1c00966832b8eeb7c/h5py-3.16.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:a6fbc5367d4046801f9b7db9191b31895f22f1c6df1f9987d667854cac493538", size = 4823472, upload-time = "2026-03-06T13:47:53.085Z" }, + { url = "https://files.pythonhosted.org/packages/52/a0/c1f604538ff6db22a0690be2dc44ab59178e115f63c917794e529356ab23/h5py-3.16.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:fb1720028d99040792bb2fb31facb8da44a6f29df7697e0b84f0d79aff2e9bd3", size = 5027150, upload-time = "2026-03-06T13:47:55.043Z" }, + { url = "https://files.pythonhosted.org/packages/2e/fd/301739083c2fc4fd89950f9bcfce75d6e14b40b0ca3d40e48a8993d1722c/h5py-3.16.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:314b6054fe0b1051c2b0cb2df5cbdab15622fb05e80f202e3b6a5eee0d6fe365", size = 4814544, upload-time = "2026-03-06T13:47:56.893Z" }, + { url = "https://files.pythonhosted.org/packages/4c/42/2193ed41ccee78baba8fcc0cff2c925b8b9ee3793305b23e1f22c20bf4c7/h5py-3.16.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ffbab2fedd6581f6aa31cf1639ca2cb86e02779de525667892ebf4cc9fd26434", size = 5034013, upload-time = "2026-03-06T13:47:59.01Z" }, + { url = "https://files.pythonhosted.org/packages/f7/20/e6c0ff62ca2ad1a396a34f4380bafccaaf8791ff8fccf3d995a1fc12d417/h5py-3.16.0-cp311-cp311-win_amd64.whl", hash = "sha256:17d1f1630f92ad74494a9a7392ab25982ce2b469fc62da6074c0ce48366a2999", size = 3191673, upload-time = "2026-03-06T13:48:00.626Z" }, + { url = "https://files.pythonhosted.org/packages/f2/48/239cbe352ac4f2b8243a8e620fa1a2034635f633731493a7ff1ed71e8658/h5py-3.16.0-cp311-cp311-win_arm64.whl", hash = "sha256:85b9c49dd58dc44cf70af944784e2c2038b6f799665d0dcbbc812a26e0faa859", size = 2673834, upload-time = "2026-03-06T13:48:02.579Z" }, + { url = "https://files.pythonhosted.org/packages/c8/c0/5d4119dba94093bbafede500d3defd2f5eab7897732998c04b54021e530b/h5py-3.16.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c5313566f4643121a78503a473f0fb1e6dcc541d5115c44f05e037609c565c4d", size = 3685604, upload-time = "2026-03-06T13:48:04.198Z" }, + { url = "https://files.pythonhosted.org/packages/b0/42/c84efcc1d4caebafb1ecd8be4643f39c85c47a80fe254d92b8b43b1eadaf/h5py-3.16.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:42b012933a83e1a558c673176676a10ce2fd3759976a0fedee1e672d1e04fc9d", size = 3061940, upload-time = "2026-03-06T13:48:05.783Z" }, + { url = "https://files.pythonhosted.org/packages/89/84/06281c82d4d1686fde1ac6b0f307c50918f1c0151062445ab3b6fa5a921d/h5py-3.16.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:ff24039e2573297787c3063df64b60aab0591980ac898329a08b0320e0cf2527", size = 5198852, upload-time = "2026-03-06T13:48:07.482Z" }, + { url = "https://files.pythonhosted.org/packages/9e/e9/1a19e42cd43cc1365e127db6aae85e1c671da1d9a5d746f4d34a50edb577/h5py-3.16.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:dfc21898ff025f1e8e67e194965a95a8d4754f452f83454538f98f8a3fcb207e", size = 5405250, upload-time = "2026-03-06T13:48:09.628Z" }, + { url = "https://files.pythonhosted.org/packages/b7/8e/9790c1655eabeb85b92b1ecab7d7e62a2069e53baefd58c98f0909c7a948/h5py-3.16.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:698dd69291272642ffda44a0ecd6cd3bda5faf9621452d255f57ce91487b9794", size = 5190108, upload-time = "2026-03-06T13:48:11.26Z" }, + { url = "https://files.pythonhosted.org/packages/51/d7/ab693274f1bd7e8c5f9fdd6c7003a88d59bedeaf8752716a55f532924fbb/h5py-3.16.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2b2c02b0a160faed5fb33f1ba8a264a37ee240b22e049ecc827345d0d9043074", size = 5419216, upload-time = "2026-03-06T13:48:13.322Z" }, + { url = "https://files.pythonhosted.org/packages/03/c1/0976b235cf29ead553e22f2fb6385a8252b533715e00d0ae52ed7b900582/h5py-3.16.0-cp312-cp312-win_amd64.whl", hash = "sha256:96b422019a1c8975c2d5dadcf61d4ba6f01c31f92bbde6e4649607885fe502d6", size = 3182868, upload-time = "2026-03-06T13:48:15.759Z" }, + { url = "https://files.pythonhosted.org/packages/14/d9/866b7e570b39070f92d47b0ff1800f0f8239b6f9e45f02363d7112336c1f/h5py-3.16.0-cp312-cp312-win_arm64.whl", hash = "sha256:39c2838fb1e8d97bcf1755e60ad1f3dd76a7b2a475928dc321672752678b96db", size = 2653286, upload-time = "2026-03-06T13:48:17.279Z" }, + { url = "https://files.pythonhosted.org/packages/0f/9e/6142ebfda0cb6e9349c091eae73c2e01a770b7659255248d637bec54a88b/h5py-3.16.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:370a845f432c2c9619db8eed334d1e610c6015796122b0e57aa46312c22617d9", size = 3671808, upload-time = "2026-03-06T13:48:19.737Z" }, + { url = "https://files.pythonhosted.org/packages/b0/65/5e088a45d0f43cd814bc5bec521c051d42005a472e804b1a36c48dada09b/h5py-3.16.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:42108e93326c50c2810025aade9eac9d6827524cdccc7d4b75a546e5ab308edb", size = 3045837, upload-time = "2026-03-06T13:48:21.854Z" }, + { url = "https://files.pythonhosted.org/packages/da/1e/6172269e18cc5a484e2913ced33339aad588e02ba407fafd00d369e22ef3/h5py-3.16.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:099f2525c9dcf28de366970a5fb34879aab20491589fa89ce2863a84218bb524", size = 5193860, upload-time = "2026-03-06T13:48:24.071Z" }, + { url = "https://files.pythonhosted.org/packages/bd/98/ef2b6fe2903e377cbe870c3b2800d62552f1e3dbe81ce49e1923c53d1c5c/h5py-3.16.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:9300ad32dea9dfc5171f94d5f6948e159ed93e4701280b0f508773b3f582f402", size = 5400417, upload-time = "2026-03-06T13:48:25.728Z" }, + { url = "https://files.pythonhosted.org/packages/bc/81/5b62d760039eed64348c98129d17061fdfc7839fc9c04eaaad6dee1004e4/h5py-3.16.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:171038f23bccddfc23f344cadabdfc9917ff554db6a0d417180d2747fe4c75a7", size = 5185214, upload-time = "2026-03-06T13:48:27.436Z" }, + { url = "https://files.pythonhosted.org/packages/28/c4/532123bcd9080e250696779c927f2cb906c8bf3447df98f5ceb8dcded539/h5py-3.16.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:7e420b539fb6023a259a1b14d4c9f6df8cf50d7268f48e161169987a57b737ff", size = 5414598, upload-time = "2026-03-06T13:48:29.49Z" }, + { url = "https://files.pythonhosted.org/packages/c3/d9/a27997f84341fc0dfcdd1fe4179b6ba6c32a7aa880fdb8c514d4dad6fba3/h5py-3.16.0-cp313-cp313-win_amd64.whl", hash = "sha256:18f2bbcd545e6991412253b98727374c356d67caa920e68dc79eab36bf5fedad", size = 3175509, upload-time = "2026-03-06T13:48:31.131Z" }, + { url = "https://files.pythonhosted.org/packages/a5/23/bb8647521d4fd770c30a76cfc6cb6a2f5495868904054e92f2394c5a78ff/h5py-3.16.0-cp313-cp313-win_arm64.whl", hash = "sha256:656f00e4d903199a1d58df06b711cf3ca632b874b4207b7dbec86185b5c8c7d4", size = 2647362, upload-time = "2026-03-06T13:48:33.411Z" }, + { url = "https://files.pythonhosted.org/packages/48/3c/7fcd9b4c9eed82e91fb15568992561019ae7a829d1f696b2c844355d95dd/h5py-3.16.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:9c9d307c0ef862d1cd5714f72ecfafe0a5d7529c44845afa8de9f46e5ba8bd65", size = 3678608, upload-time = "2026-03-06T13:48:35.183Z" }, + { url = "https://files.pythonhosted.org/packages/6a/b7/9366ed44ced9b7ef357ab48c94205280276db9d7f064aa3012a97227e966/h5py-3.16.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:8c1eff849cdd53cbc73c214c30ebdb6f1bb8b64790b4b4fc36acdb5e43570210", size = 3054773, upload-time = "2026-03-06T13:48:37.139Z" }, + { url = "https://files.pythonhosted.org/packages/58/a5/4964bc0e91e86340c2bbda83420225b2f770dcf1eb8a39464871ad769436/h5py-3.16.0-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:e2c04d129f180019e216ee5f9c40b78a418634091c8782e1f723a6ca3658b965", size = 5198886, upload-time = "2026-03-06T13:48:38.879Z" }, + { url = "https://files.pythonhosted.org/packages/f1/16/d905e7f53e661ce2c24686c38048d8e2b750ffc4350009d41c4e6c6c9826/h5py-3.16.0-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:e4360f15875a532bc7b98196c7592ed4fc92672a57c0a621355961cafb17a6dd", size = 5404883, upload-time = "2026-03-06T13:48:41.324Z" }, + { url = "https://files.pythonhosted.org/packages/4b/f2/58f34cb74af46d39f4cd18ea20909a8514960c5a3e5b92fd06a28161e0a8/h5py-3.16.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:3fae9197390c325e62e0a1aa977f2f62d994aa87aab182abbea85479b791197c", size = 5192039, upload-time = "2026-03-06T13:48:43.117Z" }, + { url = "https://files.pythonhosted.org/packages/ce/ca/934a39c24ce2e2db017268c08da0537c20fa0be7e1549be3e977313fc8f5/h5py-3.16.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:43259303989ac8adacc9986695b31e35dba6fd1e297ff9c6a04b7da5542139cc", size = 5421526, upload-time = "2026-03-06T13:48:44.838Z" }, + { url = "https://files.pythonhosted.org/packages/3e/14/615a450205e1b56d16c6783f5ccd116cde05550faad70ae077c955654a75/h5py-3.16.0-cp314-cp314-win_amd64.whl", hash = "sha256:fa48993a0b799737ba7fd21e2350fa0a60701e58180fae9f2de834bc39a147ab", size = 3183263, upload-time = "2026-03-06T13:48:47.117Z" }, + { url = "https://files.pythonhosted.org/packages/7b/48/a6faef5ed632cae0c65ac6b214a6614a0b510c3183532c521bdb0055e117/h5py-3.16.0-cp314-cp314-win_arm64.whl", hash = "sha256:1897a771a7f40d05c262fc8f37376ec37873218544b70216872876c627640f63", size = 2663450, upload-time = "2026-03-06T13:48:48.707Z" }, + { url = "https://files.pythonhosted.org/packages/5d/32/0c8bb8aedb62c772cf7c1d427c7d1951477e8c2835f872bc0a13d1f85f86/h5py-3.16.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:15922e485844f77c0b9d275396d435db3baa58292a9c2176a386e072e0cf2491", size = 3760693, upload-time = "2026-03-06T13:48:50.453Z" }, + { url = "https://files.pythonhosted.org/packages/1d/1f/fcc5977d32d6387c5c9a694afee716a5e20658ac08b3ff24fdec79fb05f2/h5py-3.16.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:df02dd29bd247f98674634dfe41f89fd7c16ba3d7de8695ec958f58404a4e618", size = 3181305, upload-time = "2026-03-06T13:48:52.221Z" }, + { url = "https://files.pythonhosted.org/packages/f5/a1/af87f64b9f986889884243643621ebbd4ac72472ba8ec8cec891ac8e2ca1/h5py-3.16.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:0f456f556e4e2cebeebd9d66adf8dc321770a42593494a0b6f0af54a7567b242", size = 5074061, upload-time = "2026-03-06T13:48:54.089Z" }, + { url = "https://files.pythonhosted.org/packages/cc/d0/146f5eaff3dc246a9c7f6e5e4f42bd45cc613bce16693bcd4d1f7c958bf5/h5py-3.16.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:3e6cb3387c756de6a9492d601553dffea3fe11b5f22b443aac708c69f3f55e16", size = 5279216, upload-time = "2026-03-06T13:48:56.75Z" }, + { url = "https://files.pythonhosted.org/packages/a1/9d/12a13424f1e604fc7df9497b73c0356fb78c2fb206abd7465ce47226e8fd/h5py-3.16.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8389e13a1fd745ad2856873e8187fd10268b2d9677877bb667b41aebd771d8b7", size = 5070068, upload-time = "2026-03-06T13:48:59.169Z" }, + { url = "https://files.pythonhosted.org/packages/41/8c/bbe98f813722b4873818a8db3e15aa3e625b59278566905ac439725e8070/h5py-3.16.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:346df559a0f7dcb31cf8e44805319e2ab24b8957c45e7708ce503b2ec79ba725", size = 5300253, upload-time = "2026-03-06T13:49:02.033Z" }, + { url = "https://files.pythonhosted.org/packages/32/9e/87e6705b4d6890e7cecdf876e2a7d3e40654a2ae37482d79a6f1b87f7b92/h5py-3.16.0-cp314-cp314t-win_amd64.whl", hash = "sha256:4c6ab014ab704b4feaa719ae783b86522ed0bf1f82184704ed3c9e4e3228796e", size = 3381671, upload-time = "2026-03-06T13:49:04.351Z" }, + { url = "https://files.pythonhosted.org/packages/96/91/9fad90cfc5f9b2489c7c26ad897157bce82f0e9534a986a221b99760b23b/h5py-3.16.0-cp314-cp314t-win_arm64.whl", hash = "sha256:faca8fb4e4319c09d83337adc80b2ca7d5c5a343c2d6f1b6388f32cfecca13c1", size = 2740706, upload-time = "2026-03-06T13:49:06.347Z" }, +] + +[[package]] +name = "hdf5plugin" +version = "7.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "h5py" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3d/64/0fc6b68e5bc671e7b81d67b930fbed3a4e8a2a92dc4af0f7282b2a2ff988/hdf5plugin-7.0.0.tar.gz", hash = "sha256:e6e6b1f8b0c4d2ca87e616ddc31d08330b36207e466357040f95269e5e0401c8", size = 68284761, upload-time = "2026-06-25T20:59:40.102Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/03/5a/00d0f491d420d491b5134ee044cd8ead5103382d88f4c8aee623258a6348/hdf5plugin-7.0.0-py3-none-macosx_10_13_x86_64.whl", hash = "sha256:e0ff0a81e6319575ffc8bf230b8df43f3f19f6fe96843e113e04c5ba9f7f3141", size = 6941923, upload-time = "2026-06-25T20:59:24.517Z" }, + { url = "https://files.pythonhosted.org/packages/96/ad/b28f4102e619c262b29bfcd13ccf6799e26f9ff113d2fdce19050ae29448/hdf5plugin-7.0.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:97ea4ff6114223c5e8ccce7e23cc6cd398b58c02f4e988e967aeea914fcb8030", size = 6259223, upload-time = "2026-06-25T20:59:26.246Z" }, + { url = "https://files.pythonhosted.org/packages/1f/f4/67263173ff61c49800eec4f16b4d84e6a39170be8d4871d4eb0d540b71ee/hdf5plugin-7.0.0-py3-none-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0f3ba9f4e2a370340b45e7042d1b1b1577ab259c1ddf00956264836fa33052ef", size = 42789778, upload-time = "2026-06-25T20:59:28.438Z" }, + { url = "https://files.pythonhosted.org/packages/75/d2/66673e2d0ef8499d08dd7061caa194e4ddec12df73ab512431c60538f675/hdf5plugin-7.0.0-py3-none-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1a4cb2207ec3ac538fc728e4596e9e49aed6c4487a641071fb370c04a361e7bf", size = 45295544, upload-time = "2026-06-25T20:59:31.765Z" }, + { url = "https://files.pythonhosted.org/packages/d7/0b/855e50e27eab8338c71c3157672c065cd2a2ab38887b3ea4bf128cbd89a1/hdf5plugin-7.0.0-py3-none-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4ad4ab0d3367699d132e61b1cc382a0c640a7dba56536e5105508205ebbe8762", size = 45012013, upload-time = "2026-06-25T20:59:35.029Z" }, + { url = "https://files.pythonhosted.org/packages/4e/14/32cf2aae083c74b95678875e11d25d0cb9e51a33d27f4218eb9aeacfcd70/hdf5plugin-7.0.0-py3-none-win_amd64.whl", hash = "sha256:2e052af8d7848e8bac92646584617503a08bb9b466cfa810a49ecd93e89b7ffa", size = 3523827, upload-time = "2026-06-25T20:59:37.758Z" }, +] + [[package]] name = "httpcore" version = "1.0.9" @@ -5124,11 +5205,15 @@ wheels = [ [[package]] name = "sharrow" -version = "2.15.0" +version = "2.16.2" source = { registry = "https://pypi.org/simple" } dependencies = [ + { name = "blosc2", version = "2.7.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "blosc2", version = "3.1.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, { name = "dask" }, { name = "filelock" }, + { name = "h5py" }, + { name = "hdf5plugin" }, { name = "networkx", version = "3.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, { name = "networkx", version = "3.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, { name = "numba" }, @@ -5140,9 +5225,9 @@ dependencies = [ { name = "pyarrow" }, { name = "xarray" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/b1/2b/cd163ad3fc6ad48b650cd472bd161f99bcc7cc4befe2b1b04188084c9f9d/sharrow-2.15.0.tar.gz", hash = "sha256:d4e6881f8abfe0719b009963c491285d14c61226c64382ded16b3ce3d6f232fe", size = 2344691, upload-time = "2025-10-31T00:35:35.171Z" } +sdist = { url = "https://files.pythonhosted.org/packages/9a/8b/09ba6d4eb1d5b761b9e0258538073823a3bf4b303cacd4cb41d01946cfa4/sharrow-2.16.2.tar.gz", hash = "sha256:acac5b99cc08557575e61599ebb9f5c7aa15e9863b0b66cf871b3ecfaf559d03", size = 2368709, upload-time = "2026-08-05T21:17:32.58Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/81/93/ff7835535a690a38c9bb3142e3c5b4b2512de09230bf9a20456e6f3db06f/sharrow-2.15.0-py3-none-any.whl", hash = "sha256:8b76bbe1f1ac941711d151cd84b7bc4aec6d2b44d62b56c831ead79eb533aad0", size = 2241380, upload-time = "2025-10-31T00:35:33.473Z" }, + { url = "https://files.pythonhosted.org/packages/da/fc/07c627b1237bae4b4c85b9671ae4db002c6c76c142d1a85e099fe05e506f/sharrow-2.16.2-py3-none-any.whl", hash = "sha256:b6a6ebb4849b1346003ca3b31147ba2fe823dc2aa32f90c371ad4c0e950600f3", size = 2251200, upload-time = "2026-08-05T21:17:31.253Z" }, ] [[package]]