From b06b3883eedf8b43972eec57f3dad07ca33119d0 Mon Sep 17 00:00:00 2001 From: delucchi-cmu Date: Tue, 18 Apr 2023 15:17:13 -0400 Subject: [PATCH 1/4] Initial implementation of association pipeline. --- src/hipscat_import/association/__init__.py | 5 + src/hipscat_import/association/arguments.py | 63 +++++ src/hipscat_import/association/map_reduce.py | 204 ++++++++++++++ .../association/run_association.py | 51 ++++ .../association/test_association_argument.py | 142 ++++++++++ .../association/test_run_association.py | 256 ++++++++++++++++++ tests/hipscat_import/conftest.py | 38 ++- .../Norder=0/Dir=0/Npix=11.parquet | Bin 0 -> 8791 bytes .../small_sky_object_catalog/_common_metadata | Bin 0 -> 3355 bytes .../data/small_sky_object_catalog/_metadata | Bin 0 -> 4631 bytes .../catalog_info.json | 8 + .../partition_info.csv | 2 + .../small_sky_object_catalog/point_map.fits | Bin 0 -> 8640 bytes .../provenance_info.json | 53 ++++ .../Norder=0/Dir=0/Npix=4.parquet | Bin 0 -> 11408 bytes .../Norder=1/Dir=0/Npix=47.parquet | Bin 0 -> 146699 bytes .../Norder=2/Dir=0/Npix=176.parquet | Bin 0 -> 30122 bytes .../Norder=2/Dir=0/Npix=177.parquet | Bin 0 -> 94207 bytes .../Norder=2/Dir=0/Npix=178.parquet | Bin 0 -> 101250 bytes .../Norder=2/Dir=0/Npix=179.parquet | Bin 0 -> 109236 bytes .../Norder=2/Dir=0/Npix=180.parquet | Bin 0 -> 45413 bytes .../Norder=2/Dir=0/Npix=181.parquet | Bin 0 -> 59299 bytes .../Norder=2/Dir=0/Npix=182.parquet | Bin 0 -> 79485 bytes .../Norder=2/Dir=0/Npix=183.parquet | Bin 0 -> 73727 bytes .../Norder=2/Dir=0/Npix=184.parquet | Bin 0 -> 87558 bytes .../Norder=2/Dir=0/Npix=185.parquet | Bin 0 -> 178114 bytes .../Norder=2/Dir=0/Npix=186.parquet | Bin 0 -> 33887 bytes .../Norder=2/Dir=0/Npix=187.parquet | Bin 0 -> 47115 bytes .../small_sky_source_catalog/_common_metadata | Bin 0 -> 4863 bytes .../data/small_sky_source_catalog/_metadata | Bin 0 -> 30665 bytes .../catalog_info.json | 8 + .../partition_info.csv | 15 + .../small_sky_source_catalog/point_map.fits | Bin 0 -> 8640 bytes .../provenance_info.json | 49 ++++ 34 files changed, 893 insertions(+), 1 deletion(-) create mode 100644 src/hipscat_import/association/__init__.py create mode 100644 src/hipscat_import/association/arguments.py create mode 100644 src/hipscat_import/association/map_reduce.py create mode 100644 src/hipscat_import/association/run_association.py create mode 100644 tests/hipscat_import/association/test_association_argument.py create mode 100644 tests/hipscat_import/association/test_run_association.py create mode 100644 tests/hipscat_import/data/small_sky_object_catalog/Norder=0/Dir=0/Npix=11.parquet create mode 100644 tests/hipscat_import/data/small_sky_object_catalog/_common_metadata create mode 100644 tests/hipscat_import/data/small_sky_object_catalog/_metadata create mode 100644 tests/hipscat_import/data/small_sky_object_catalog/catalog_info.json create mode 100644 tests/hipscat_import/data/small_sky_object_catalog/partition_info.csv create mode 100644 tests/hipscat_import/data/small_sky_object_catalog/point_map.fits create mode 100644 tests/hipscat_import/data/small_sky_object_catalog/provenance_info.json create mode 100644 tests/hipscat_import/data/small_sky_source_catalog/Norder=0/Dir=0/Npix=4.parquet create mode 100644 tests/hipscat_import/data/small_sky_source_catalog/Norder=1/Dir=0/Npix=47.parquet create mode 100644 tests/hipscat_import/data/small_sky_source_catalog/Norder=2/Dir=0/Npix=176.parquet create mode 100644 tests/hipscat_import/data/small_sky_source_catalog/Norder=2/Dir=0/Npix=177.parquet create mode 100644 tests/hipscat_import/data/small_sky_source_catalog/Norder=2/Dir=0/Npix=178.parquet create mode 100644 tests/hipscat_import/data/small_sky_source_catalog/Norder=2/Dir=0/Npix=179.parquet create mode 100644 tests/hipscat_import/data/small_sky_source_catalog/Norder=2/Dir=0/Npix=180.parquet create mode 100644 tests/hipscat_import/data/small_sky_source_catalog/Norder=2/Dir=0/Npix=181.parquet create mode 100644 tests/hipscat_import/data/small_sky_source_catalog/Norder=2/Dir=0/Npix=182.parquet create mode 100644 tests/hipscat_import/data/small_sky_source_catalog/Norder=2/Dir=0/Npix=183.parquet create mode 100644 tests/hipscat_import/data/small_sky_source_catalog/Norder=2/Dir=0/Npix=184.parquet create mode 100644 tests/hipscat_import/data/small_sky_source_catalog/Norder=2/Dir=0/Npix=185.parquet create mode 100644 tests/hipscat_import/data/small_sky_source_catalog/Norder=2/Dir=0/Npix=186.parquet create mode 100644 tests/hipscat_import/data/small_sky_source_catalog/Norder=2/Dir=0/Npix=187.parquet create mode 100644 tests/hipscat_import/data/small_sky_source_catalog/_common_metadata create mode 100644 tests/hipscat_import/data/small_sky_source_catalog/_metadata create mode 100644 tests/hipscat_import/data/small_sky_source_catalog/catalog_info.json create mode 100644 tests/hipscat_import/data/small_sky_source_catalog/partition_info.csv create mode 100644 tests/hipscat_import/data/small_sky_source_catalog/point_map.fits create mode 100644 tests/hipscat_import/data/small_sky_source_catalog/provenance_info.json diff --git a/src/hipscat_import/association/__init__.py b/src/hipscat_import/association/__init__.py new file mode 100644 index 00000000..4d721422 --- /dev/null +++ b/src/hipscat_import/association/__init__.py @@ -0,0 +1,5 @@ +"""Modules for creating a new association table from an equijoin between two catalogs""" + +from .arguments import AssociationArguments +from .map_reduce import map_association, reduce_association +from .run_association import run diff --git a/src/hipscat_import/association/arguments.py b/src/hipscat_import/association/arguments.py new file mode 100644 index 00000000..de75bcfe --- /dev/null +++ b/src/hipscat_import/association/arguments.py @@ -0,0 +1,63 @@ +"""Utility to hold all arguments required throughout association pipeline""" + +from dataclasses import dataclass + +from hipscat.catalog import CatalogParameters + +from hipscat_import.runtime_arguments import RuntimeArguments + + +@dataclass +class AssociationArguments(RuntimeArguments): + """Data class for holding association arguments""" + + ## Input - Primary + primary_input_catalog_path: str = "" + primary_id_column: str = "" + primary_join_column: str = "" + + ## Input - Join + join_input_catalog_path: str = "" + join_id_column: str = "" + join_foreign_key: str = "" + + def __post_init__(self): + RuntimeArguments._check_arguments(self) + self._check_arguments() + + def _check_arguments(self): + if not self.primary_input_catalog_path: + raise ValueError("primary_input_catalog_path is required") + if not self.primary_id_column: + raise ValueError("primary_id_column is required") + if not self.primary_join_column: + raise ValueError("primary_join_column is required") + + if not self.join_input_catalog_path: + raise ValueError("join_input_catalog_path is required") + if not self.join_id_column: + raise ValueError("join_id_column is required") + if not self.join_foreign_key: + raise ValueError("join_foreign_key is required") + + def to_catalog_parameters(self) -> CatalogParameters: + """Convert importing arguments into hipscat catalog parameters. + + Returns: + CatalogParameters for catalog being created. + """ + return CatalogParameters( + catalog_name=self.output_catalog_name, + catalog_type="association", + output_path=self.output_path, + ) + + def additional_runtime_provenance_info(self): + return { + "primary_input_catalog_path": str(self.primary_input_catalog_path), + "primary_id_column": self.primary_id_column, + "primary_join_column": self.primary_join_column, + "join_input_catalog_path": str(self.join_input_catalog_path), + "join_id_column": self.join_id_column, + "join_foreign_key": self.join_foreign_key, + } diff --git a/src/hipscat_import/association/map_reduce.py b/src/hipscat_import/association/map_reduce.py new file mode 100644 index 00000000..f959df02 --- /dev/null +++ b/src/hipscat_import/association/map_reduce.py @@ -0,0 +1,204 @@ +"""Create partitioned association table between two catalogs""" + +import dask.dataframe as dd +import pyarrow.parquet as pq +from hipscat.io import file_io, paths + + +def map_association(args): + """Using dask dataframes, create an association between two catalogs. + This will write out sharded parquet files to the temp (intermediate) + directory. + + Implementation notes: + + Because we may be joining to a column that is NOT the natural/primary key + (on either side of the join), we fetch both the identifying column and the + join predicate column, possibly duplicating one of the columns. + + This way, when we drop the join predicate columns at the end of the process, + we will still have the identifying columns. However, it makes the loading + of each input catalog more verbose. + """ + ## Read and massage primary input data + ## NB: We may be joining on a column that is NOT the natural primary key. + single_primary_column = args.primary_id_column == args.primary_join_column + read_columns = [ + "_hipscat_index", + "Norder", + "Dir", + "Npix", + ] + if single_primary_column: + read_columns = [args.primary_id_column] + read_columns + else: + read_columns = [args.primary_join_column, args.primary_id_column] + read_columns + + primary_index = dd.read_parquet( + path=args.primary_input_catalog_path, + columns=read_columns, + dataset={"partitioning": "hive"}, + ) + if single_primary_column: + ## Duplicate the column to simplify later steps + primary_index["primary_id"] = primary_index[args.primary_join_column] + rename_columns = { + args.primary_join_column: "primary_join", + "_hipscat_index": "primary_hipscat_index", + } + if not single_primary_column: + rename_columns[args.primary_id_column] = "primary_id" + primary_index = primary_index.rename(columns=rename_columns).set_index( + "primary_join" + ) + + ## Read and massage join input data + single_join_column = args.join_id_column == args.join_foreign_key + read_columns = [ + "_hipscat_index", + "Norder", + "Dir", + "Npix", + ] + if single_join_column: + read_columns = [args.join_id_column] + read_columns + else: + read_columns = [args.join_id_column, args.join_foreign_key] + read_columns + + join_index = dd.read_parquet( + path=args.join_input_catalog_path, + columns=read_columns, + dataset={"partitioning": "hive"}, + ) + if single_join_column: + ## Duplicate the column to simplify later steps + join_index["join_id"] = join_index[args.join_id_column] + rename_columns = { + args.join_foreign_key: "join_to_primary", + "_hipscat_index": "join_hipscat_index", + "Norder": "join_Norder", + "Dir": "join_Dir", + "Npix": "join_Npix", + } + if not single_join_column: + rename_columns[args.join_id_column] = "join_id" + join_index = join_index.rename(columns=rename_columns).set_index("join_to_primary") + + ## Join the two data sets on the shared join predicate. + join_data = primary_index.merge( + join_index, how="inner", left_index=True, right_index=True + ).reset_index() + + ## Write out a summary of each partition join + groups = ( + join_data.groupby( + ["Norder", "Dir", "Npix", "join_Norder", "join_Dir", "join_Npix"], + group_keys=False, + )[ + "Norder", + "Dir", + "Npix", + "join_Norder", + "join_Dir", + "join_Npix", + "primary_hipscat_index", + ] + .count() + .compute() + ) + intermediate_partitions_file = file_io.append_paths_to_pointer( + args.tmp_path, "partitions.csv" + ) + file_io.write_dataframe_to_csv( + dataframe=groups, file_pointer=intermediate_partitions_file + ) + + ## Drop join predicate columns + join_data = join_data[ + [ + "Norder", + "Dir", + "Npix", + "join_Norder", + "join_Dir", + "join_Npix", + "primary_id", + "primary_hipscat_index", + "join_id", + "join_hipscat_index", + ] + ] + + ## Write out association table shards. + join_data.to_parquet( + path=args.tmp_path, + engine="pyarrow", + partition_on=["Norder", "Dir", "Npix", "join_Norder", "join_Dir", "join_Npix"], + compute_kwargs={"partition_size": 1_000_000_000}, + write_index=False, + ) + + +def reduce_association(input_path, output_path): + """Collate sharded parquet files into a single parquet file per partition""" + intermediate_partitions_file = file_io.append_paths_to_pointer( + input_path, "partitions.csv" + ) + data_frame = file_io.load_csv_to_pandas(intermediate_partitions_file) + + ## Clean up the dataframe and write out as our new partition join info file. + data_frame = data_frame[data_frame["primary_hipscat_index"] != 0] + data_frame["num_rows"] = data_frame["primary_hipscat_index"] + data_frame = data_frame[ + ["Norder", "Dir", "Npix", "join_Norder", "join_Dir", "join_Npix", "num_rows"] + ] + data_frame = data_frame.sort_values(["Norder", "Npix", "join_Norder", "join_Npix"]) + file_io.write_dataframe_to_csv( + dataframe=data_frame, + file_pointer=file_io.append_paths_to_pointer( + output_path, "partition_join_info.csv" + ), + index=False, + ) + + ## For each partition, join all parquet shards into single parquet file. + for _, partition in data_frame.iterrows(): + input_dir = paths.create_hive_directory_name( + input_path, + ["Norder", "Dir", "Npix", "join_Norder", "join_Dir", "join_Npix"], + [ + partition["Norder"], + partition["Dir"], + partition["Npix"], + partition["join_Norder"], + partition["join_Dir"], + partition["join_Npix"], + ], + ) + output_dir = paths.pixel_association_directory( + output_path, + partition["Norder"], + partition["Npix"], + partition["join_Norder"], + partition["join_Npix"], + ) + file_io.make_directory(output_dir, exist_ok=True) + output_file = paths.pixel_association_file( + output_path, + partition["Norder"], + partition["Npix"], + partition["join_Norder"], + partition["join_Npix"], + ) + table = pq.read_table(input_dir) + rows_written = len(table) + + if rows_written != partition["num_rows"]: + raise ValueError( + "Unexpected number of objects ", + f" Expected {partition['num_rows']}, wrote {rows_written}", + ) + + pq.write_table(table, where=output_file) + + return data_frame["num_rows"].sum() diff --git a/src/hipscat_import/association/run_association.py b/src/hipscat_import/association/run_association.py new file mode 100644 index 00000000..6d2e7ce6 --- /dev/null +++ b/src/hipscat_import/association/run_association.py @@ -0,0 +1,51 @@ +"""Create partitioned association table between two catalogs +using dask dataframes for parallelization + +Methods in this file set up a dask pipeline using dataframes. +The actual logic of the map reduce is in the `map_reduce.py` file. +""" + +from hipscat.io import file_io, write_metadata +from tqdm import tqdm + +from hipscat_import.association.arguments import AssociationArguments +from hipscat_import.association.map_reduce import (map_association, + reduce_association) + + +def _validate_args(args): + if not args: + raise ValueError("args is required and should be type AssociationArguments") + if not isinstance(args, AssociationArguments): + raise ValueError("args must be type AssociationArguments") + + +def run(args): + """Run the association pipeline""" + _validate_args(args) + + with tqdm(total=1, desc="Mapping ", disable=not args.progress_bar) as step_progress: + map_association(args) + step_progress.update(1) + + rows_written = 0 + with tqdm( + total=1, desc="Reducing ", disable=not args.progress_bar + ) as step_progress: + rows_written = reduce_association(args.tmp_path, args.catalog_path) + step_progress.update(1) + + # All done - write out the metadata + with tqdm( + total=4, desc="Finishing", disable=not args.progress_bar + ) as step_progress: + catalog_params = args.to_catalog_parameters() + catalog_params.total_rows = int(rows_written) + write_metadata.write_provenance_info(catalog_params, args.provenance_info()) + step_progress.update(1) + write_metadata.write_catalog_info(catalog_params) + step_progress.update(1) + write_metadata.write_parquet_metadata(args.catalog_path) + step_progress.update(1) + file_io.remove_directory(args.tmp_path, ignore_errors=True) + step_progress.update(1) diff --git a/tests/hipscat_import/association/test_association_argument.py b/tests/hipscat_import/association/test_association_argument.py new file mode 100644 index 00000000..e80a0273 --- /dev/null +++ b/tests/hipscat_import/association/test_association_argument.py @@ -0,0 +1,142 @@ +"""Tests of argument validation, in the absense of command line parsing""" + + +import pytest + +from hipscat_import.association.arguments import AssociationArguments + +# pylint: disable=protected-access + + +def test_none(): + """No arguments provided. Should error for required args.""" + with pytest.raises(ValueError): + AssociationArguments() + + +def test_empty_required(tmp_path, small_sky_object_catalog): + """All non-runtime arguments are required.""" + ## primary_input_catalog_path is missing + with pytest.raises(ValueError, match="primary_input_catalog_path"): + AssociationArguments( + primary_input_catalog_path=None, ## empty + primary_id_column="id", + primary_join_column="id", + join_input_catalog_path=small_sky_object_catalog, + join_id_column="id", + join_foreign_key="id", + output_path=tmp_path, + output_catalog_name="small_sky_self_join", + ) + + with pytest.raises(ValueError, match="primary_id_column"): + AssociationArguments( + primary_input_catalog_path=small_sky_object_catalog, + primary_id_column="", ## empty + primary_join_column="id", + join_input_catalog_path=small_sky_object_catalog, + join_id_column="id", + join_foreign_key="id", + output_path=tmp_path, + output_catalog_name="small_sky_self_join", + overwrite=True, + ) + + with pytest.raises(ValueError, match="primary_join_column"): + AssociationArguments( + primary_input_catalog_path=small_sky_object_catalog, + primary_id_column="id", + primary_join_column="", ## empty + join_input_catalog_path=small_sky_object_catalog, + join_id_column="id", + join_foreign_key="id", + output_path=tmp_path, + output_catalog_name="small_sky_self_join", + overwrite=True, + ) + + with pytest.raises(ValueError, match="join_input_catalog_path"): + AssociationArguments( + primary_input_catalog_path=small_sky_object_catalog, + primary_id_column="id", + primary_join_column="id", + join_input_catalog_path="", ## empty + join_id_column="id", + join_foreign_key="id", + output_path=tmp_path, + output_catalog_name="small_sky_self_join", + overwrite=True, + ) + + with pytest.raises(ValueError, match="join_id_column"): + AssociationArguments( + primary_input_catalog_path=small_sky_object_catalog, + primary_id_column="id", + primary_join_column="id", + join_input_catalog_path=small_sky_object_catalog, + join_id_column="", ## empty + join_foreign_key="id", + output_path=tmp_path, + output_catalog_name="small_sky_self_join", + overwrite=True, + ) + + with pytest.raises(ValueError, match="join_foreign_key"): + AssociationArguments( + primary_input_catalog_path=small_sky_object_catalog, + primary_id_column="id", + primary_join_column="id", + join_input_catalog_path=small_sky_object_catalog, + join_id_column="id", + join_foreign_key="", ## empty + output_path=tmp_path, + output_catalog_name="small_sky_self_join", + overwrite=True, + ) + + +def test_all_required_args(tmp_path, small_sky_object_catalog): + """Required arguments are provided.""" + AssociationArguments( + primary_input_catalog_path=small_sky_object_catalog, + primary_id_column="id", + primary_join_column="id", + join_input_catalog_path=small_sky_object_catalog, + join_id_column="id", + join_foreign_key="id", + output_path=tmp_path, + output_catalog_name="small_sky_self_join", + ) + + +def test_to_catalog_parameters(small_sky_object_catalog, tmp_path): + """Verify creation of catalog parameters for index to be created.""" + args = AssociationArguments( + primary_input_catalog_path=small_sky_object_catalog, + primary_id_column="id", + primary_join_column="id", + join_input_catalog_path=small_sky_object_catalog, + join_id_column="id", + join_foreign_key="id", + output_path=tmp_path, + output_catalog_name="small_sky_self_join", + ) + catalog_parameters = args.to_catalog_parameters() + assert catalog_parameters.catalog_name == args.output_catalog_name + + +def test_provenance_info(small_sky_object_catalog, tmp_path): + """Verify that provenance info includes association-specific fields.""" + args = AssociationArguments( + primary_input_catalog_path=small_sky_object_catalog, + primary_id_column="id", + primary_join_column="id", + join_input_catalog_path=small_sky_object_catalog, + join_id_column="id", + join_foreign_key="id", + output_path=tmp_path, + output_catalog_name="small_sky_self_join", + ) + + runtime_args = args.provenance_info()["runtime_args"] + assert "primary_input_catalog_path" in runtime_args diff --git a/tests/hipscat_import/association/test_run_association.py b/tests/hipscat_import/association/test_run_association.py new file mode 100644 index 00000000..e0df037b --- /dev/null +++ b/tests/hipscat_import/association/test_run_association.py @@ -0,0 +1,256 @@ +"""test stuff.""" + +import os + +import numpy as np +import numpy.testing as npt +import pandas as pd +import pytest + +import hipscat_import.association.run_association as runner +from hipscat_import.association.arguments import AssociationArguments + + +def test_empty_args(): + """Runner should fail with empty arguments""" + with pytest.raises(ValueError): + runner.run(None) + + +def test_bad_args(): + """Runner should fail with mis-typed arguments""" + args = {"output_catalog_name": "bad_arg_type"} + with pytest.raises(ValueError): + runner.run(args) + + +@pytest.mark.filterwarnings("ignore::DeprecationWarning") +@pytest.mark.timeout(15) +def test_object_to_source( + small_sky_object_catalog, + small_sky_source_catalog, + tmp_path, + assert_text_file_matches, +): + """test stuff""" + + args = AssociationArguments( + primary_input_catalog_path=small_sky_object_catalog, + primary_id_column="id", + primary_join_column="id", + join_input_catalog_path=small_sky_source_catalog, + output_catalog_name="small_sky_association", + join_id_column="source_id", + join_foreign_key="object_id", + output_path=tmp_path, + progress_bar=False, + ) + runner.run(args) + + # Check that the catalog metadata file exists + expected_metadata_lines = [ + "{", + ' "catalog_name": "small_sky_association",', + ' "catalog_type": "association",', + ' "epoch": "J2000",', + ' "ra_kw": "ra",', + ' "dec_kw": "dec",', + ' "total_rows": 17161', + "}", + ] + metadata_filename = os.path.join(args.catalog_path, "catalog_info.json") + assert_text_file_matches(expected_metadata_lines, metadata_filename) + + # Check that the partition *join* info file exists + expected_lines = [ + "Norder,Dir,Npix,join_Norder,join_Dir,join_Npix,num_rows", + "0,0,11,0,0,4,50", + "0,0,11,1,0,47,2395", + "0,0,11,2,0,176,385", + "0,0,11,2,0,177,1510", + "0,0,11,2,0,178,1634", + "0,0,11,2,0,179,1773", + "0,0,11,2,0,180,655", + "0,0,11,2,0,181,903", + "0,0,11,2,0,182,1246", + "0,0,11,2,0,183,1143", + "0,0,11,2,0,184,1390", + "0,0,11,2,0,185,2942", + "0,0,11,2,0,186,452", + "0,0,11,2,0,187,683", + ] + metadata_filename = os.path.join(args.catalog_path, "partition_join_info.csv") + assert_text_file_matches(expected_lines, metadata_filename) + + ## Test one pixel that will have 50 rows in it. + output_file = os.path.join( + tmp_path, + "small_sky_association", + "Norder=0", + "Dir=0", + "Npix=11", + "join_Norder=0", + "join_Dir=0", + "join_Npix=4.parquet", + ) + assert os.path.exists(output_file), f"file not found [{output_file}]" + data_frame = pd.read_parquet(output_file, engine="pyarrow") + npt.assert_array_equal( + data_frame.columns, + ["primary_id", "primary_hipscat_index", "join_id", "join_hipscat_index"], + ) + assert len(data_frame) == 50 + ids = data_frame["primary_id"] + assert np.logical_and(ids >= 700, ids < 832).all() + ids = data_frame["join_id"] + assert np.logical_and(ids >= 70_000, ids < 87161).all() + + +@pytest.mark.filterwarnings("ignore::DeprecationWarning") +@pytest.mark.timeout(15) +def test_source_to_object( + small_sky_object_catalog, + small_sky_source_catalog, + tmp_path, + assert_text_file_matches, +): + """test stuff""" + + args = AssociationArguments( + primary_input_catalog_path=small_sky_source_catalog, + primary_id_column="source_id", + primary_join_column="object_id", + join_input_catalog_path=small_sky_object_catalog, + join_id_column="id", + join_foreign_key="id", + output_catalog_name="small_sky_association", + output_path=tmp_path, + progress_bar=False, + ) + runner.run(args) + + # Check that the catalog metadata file exists + expected_metadata_lines = [ + "{", + ' "catalog_name": "small_sky_association",', + ' "catalog_type": "association",', + ' "epoch": "J2000",', + ' "ra_kw": "ra",', + ' "dec_kw": "dec",', + ' "total_rows": 17161', + "}", + ] + metadata_filename = os.path.join(args.catalog_path, "catalog_info.json") + assert_text_file_matches(expected_metadata_lines, metadata_filename) + + # Check that the partition *join* info file exists + expected_lines = [ + "Norder,Dir,Npix,join_Norder,join_Dir,join_Npix,num_rows", + "0,0,4,0,0,11,50", + "1,0,47,0,0,11,2395", + "2,0,176,0,0,11,385", + "2,0,177,0,0,11,1510", + "2,0,178,0,0,11,1634", + "2,0,179,0,0,11,1773", + "2,0,180,0,0,11,655", + "2,0,181,0,0,11,903", + "2,0,182,0,0,11,1246", + "2,0,183,0,0,11,1143", + "2,0,184,0,0,11,1390", + "2,0,185,0,0,11,2942", + "2,0,186,0,0,11,452", + "2,0,187,0,0,11,683", + ] + metadata_filename = os.path.join(args.catalog_path, "partition_join_info.csv") + assert_text_file_matches(expected_lines, metadata_filename) + + ## Test one pixel that will have 50 rows in it. + output_file = os.path.join( + tmp_path, + "small_sky_association", + "Norder=0", + "Dir=0", + "Npix=4", + "join_Norder=0", + "join_Dir=0", + "join_Npix=11.parquet", + ) + assert os.path.exists(output_file), f"file not found [{output_file}]" + data_frame = pd.read_parquet(output_file, engine="pyarrow") + npt.assert_array_equal( + data_frame.columns, + ["primary_id", "primary_hipscat_index", "join_id", "join_hipscat_index"], + ) + assert len(data_frame) == 50 + ids = data_frame["primary_id"] + assert np.logical_and(ids >= 70_000, ids < 87161).all() + ids = data_frame["join_id"] + assert np.logical_and(ids >= 700, ids < 832).all() + + +@pytest.mark.filterwarnings("ignore::DeprecationWarning") +@pytest.mark.timeout(15) +def test_self_join( + small_sky_object_catalog, + tmp_path, + assert_text_file_matches, +): + """test stuff""" + + args = AssociationArguments( + primary_input_catalog_path=small_sky_object_catalog, + primary_id_column="id", + primary_join_column="id", + join_input_catalog_path=small_sky_object_catalog, + output_catalog_name="small_sky_self_association", + join_foreign_key="id", + join_id_column="id", + output_path=tmp_path, + progress_bar=False, + ) + runner.run(args) + + # Check that the catalog metadata file exists + expected_metadata_lines = [ + "{", + ' "catalog_name": "small_sky_self_association",', + ' "catalog_type": "association",', + ' "epoch": "J2000",', + ' "ra_kw": "ra",', + ' "dec_kw": "dec",', + ' "total_rows": 131', + "}", + ] + metadata_filename = os.path.join(args.catalog_path, "catalog_info.json") + assert_text_file_matches(expected_metadata_lines, metadata_filename) + + # Check that the partition *join* info file exists + expected_lines = [ + "Norder,Dir,Npix,join_Norder,join_Dir,join_Npix,num_rows", + "0,0,11,0,0,11,131", + ] + metadata_filename = os.path.join(args.catalog_path, "partition_join_info.csv") + assert_text_file_matches(expected_lines, metadata_filename) + + ## Test one pixel that will have 50 rows in it. + output_file = os.path.join( + tmp_path, + "small_sky_self_association", + "Norder=0", + "Dir=0", + "Npix=11", + "join_Norder=0", + "join_Dir=0", + "join_Npix=11.parquet", + ) + assert os.path.exists(output_file), f"file not found [{output_file}]" + data_frame = pd.read_parquet(output_file, engine="pyarrow") + npt.assert_array_equal( + data_frame.columns, + ["primary_id", "primary_hipscat_index", "join_id", "join_hipscat_index"], + ) + assert len(data_frame) == 131 + ids = data_frame["primary_id"] + assert np.logical_and(ids >= 700, ids < 832).all() + ids = data_frame["join_id"] + assert np.logical_and(ids >= 700, ids < 832).all() diff --git a/tests/hipscat_import/conftest.py b/tests/hipscat_import/conftest.py index 864b86bd..b7fec6aa 100644 --- a/tests/hipscat_import/conftest.py +++ b/tests/hipscat_import/conftest.py @@ -1,3 +1,5 @@ +"""Fixtures for testing import tool actions.""" + import os import re @@ -7,7 +9,7 @@ from dask.distributed import Client -@pytest.fixture(scope="package", name="dask_client") +@pytest.fixture(scope="session", name="dask_client") def dask_client(): """Create a single client for use by all unit test cases.""" client = Client() @@ -34,11 +36,19 @@ def small_sky_single_file(test_data_dir): return os.path.join(test_data_dir, "small_sky", "catalog.csv") +@pytest.fixture +def small_sky_object_catalog(test_data_dir): + return os.path.join(test_data_dir, "small_sky_object_catalog") + @pytest.fixture def small_sky_source_dir(test_data_dir): return os.path.join(test_data_dir, "small_sky_source") +@pytest.fixture +def small_sky_source_catalog(test_data_dir): + return os.path.join(test_data_dir, "small_sky_source_catalog") + @pytest.fixture def blank_data_dir(test_data_dir): return os.path.join(test_data_dir, "blank") @@ -171,3 +181,29 @@ def assert_parquet_file_ids(file_name, id_column, expected_ids): npt.assert_array_equal(ids, expected_ids) return assert_parquet_file_ids + + +@pytest.fixture +def assert_parquet_file_index(): + def assert_parquet_file_index(file_name, expected_values): + """ + Convenience method to read a parquet file and compare the index values to + a list of expected objects. + + Args: + file_name (str): fully-specified path of the file to read + expected_values (:obj:`int[]`): list of expected values in index + """ + assert os.path.exists(file_name), f"file not found [{file_name}]" + + data_frame = pd.read_parquet(file_name, engine="pyarrow") + values = data_frame.index.values.tolist() + values.sort() + + assert len(values) == len( + expected_values + ), f"object list not the same size ({len(values)} vs {len(expected_values)})" + + npt.assert_array_equal(values, expected_values) + + return assert_parquet_file_index diff --git a/tests/hipscat_import/data/small_sky_object_catalog/Norder=0/Dir=0/Npix=11.parquet b/tests/hipscat_import/data/small_sky_object_catalog/Norder=0/Dir=0/Npix=11.parquet new file mode 100644 index 0000000000000000000000000000000000000000..6cb445dc6435af18ae35262ff84a65a11eb45dd7 GIT binary patch literal 8791 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 literal 0 HcmV?d00001 diff --git a/tests/hipscat_import/data/small_sky_object_catalog/_common_metadata b/tests/hipscat_import/data/small_sky_object_catalog/_common_metadata new file mode 100644 index 0000000000000000000000000000000000000000..8d29bf76c56e62c4888ad14e74ac9c0bd0bf36be GIT binary patch literal 3355 zcmcIndvDrS6t~){d#HOa6Ok${Lh4^@$0N{|{!XoDZqg!}?bu8j)@L#lq9 z{nwAM53`T5bM7?`1e$JIX9aWbdH&Aho=eV1r?Pi%|IfNmxIcA=erWG~FYMiWbf<79 z@Pxh3aWS#^;;!erocHO(?)j6+c*3`z0b_mtO|KV3p1x!2=zU-pJUJaz4a zvmZS?e)ruk-wJ}TcL)63Z;vOQ4<>PQ_bA9#_u6r=5PlH$AM8Ia+>hx1Dg>4JUm%ws*Q&W8CHlnqk{hM*PUGR&BWL zZ=G|)X`ZdkLgwTT#Ks$a+hcwPkoECg?rPB;jsD+#vhJMtb~2u@4yJ)Ku_sGx;CN}n zVNWrzxj&f(;|QsZpUXd&i_l}MXxDgE&Jzj`NT+kEzu=>#@YmNRe9A;2gq*_%pI-^7 z5ph6dLIy+{Fq$ZC@KYoC95PHKW>|JFRZwL?^*%AUY~-^lgx}jA7qVx>ZYG^iSkQU!~=#DrqC9x~MpM zeI-NPrTSEr6R=kwI{F3Ln0fkQdMuH^3MqAzPTvXT#Ci#GO1?P z6(^)%zeCj^9e}-xV~BO*uv2#xv`Ye~s=c=KcA4`l)Kg@LsWVhZxa`I$0cd8C8>xjwDs=Pk{dHquTYeOJY6z>kvMe1&d{wi z%Dy44cr)+F%`-(0zMSN|U3Cl{r7^rM zFE{We7Ss@8Dw}zaEE>lW_UL;2a^EtvrE{pVJZ$xrq%F;EgSwG6cLRod7q-}mZp#y0 ztLJ8aIZ=1&w4je#8+~&L{rDkVTQa+9xX~+7H*s5Z{cNn`{s;NG9z6{(^d8 zcWPXTeS_Eff5=(I!~Qo9H40}HYx9)lP@&M%^3une!#wbHdFh4?s4krS$bveyW2ahU z=l=9%Ej=EQ0MCnheGaS84_CI{n6Yy@QfDVpEmfm3jy{;Hgp_b>9uHtFN+&ogySNuq z*dMvbkMO0id0E3HFo1RSQ>pYE7EKAHfynr5iCaE&#ZyYBwX#!Ri>om{@8*;Y1Zd1oJ|)rMk{&JWcth1KGSDO$gdg* z4W;D9*fzu~6XwCH54d%cNbJgB_XQ(~XK-I3zRaG+s0;=R1#*47-rzaE zVXwuh?2Ru8?I|!@D%m+kyhkGdf%PCF^W1us?N{|#-H#}O4=Rb*vw&qroPtt`u`lo~ zjkCQ-7S~i(f3_uf#}c?7N2Kk5DEe1PImds(p)gstWGm*nr49Lp?$#~;0^VG<^lZ_@A>Ru@$j`!_%XI8uV;Sp#Es)8?03sS_QT|W X{bJdF_fQaS`4hhSLJ-d2-=e<(9fJp6 literal 0 HcmV?d00001 diff --git a/tests/hipscat_import/data/small_sky_object_catalog/_metadata b/tests/hipscat_import/data/small_sky_object_catalog/_metadata new file mode 100644 index 0000000000000000000000000000000000000000..7aac13dcfdaf7fd541f79e354abcea4ee6d528f2 GIT binary patch literal 4631 zcmcIoPmB{)7%$7Zu7C!GHr-ftQ^Uk4&_8U!l7PIKwzE@ryKI5}Gp5c=r`str)6zfN zc7cQtLkK5No(u;QFNP2g9y}1j!Eo|qj4{Ro2M-=hObju`gYo;`n@-zpS#*WT?(2Ks z_r3S~ec$`u_a<|iq(koMxjPY;cc!UUG*fo(ce$qzOnImDlFPjdI}Mq2c9t}i>1G>p zQEN164K{oZ6vN{ei$&ckY2Bist!qXxSS&KT7m5|V-c;olQy=gx-P(8RC6~+Po&ubi zRIO3cU`ZV8jOli7I#t)ZF3+{8Ip5qZ?`r{Obs`u53usY$A{07Ymm3$_TFY}|j}QEb z>~+n|p$qOQnCW@A&vSJT_h6Qr^?7Gc?gT`5yt8+vSkL2nV}jH*chEiU20AWi_QDTL zc`W=_`+e?RefWmV;BW8p{652d_ab-ydBCTbzqC);z3UfFA^x~@Tlf!O9^m7CF#>mI zJ%2yLeKg1Y;qDW-61`^=_+a&(LtsR@EdqbNI*7lMCN_&5p8GqvpI_m=e5KC>Q@?aGv{m{%O;={l-+EG9$mR~>F?jZ*cR@e@A%wP z_Mxy(Im7#Shv()F?&>?-k0*WJX>^^nkO@PhZDHQw2bfq)h5_#r?k0KCfPcjXfV5@} zBYqg;ei8t2H+2zPba616;G*Z62&Idg^?ZA5zYFxMh!3eE!osyiq$p@uqf-AQ7Vqg7*Mg*tD~X6ZRu*Qr1`Lpv)LL>$ZpmPN(XUxxu3Kw3_uSdUp||~)FAvC!vheFAoIk?CmTh98 zTdkuD;~X6E5CwGY@at&CZ4~Z#5<$bU>fKQN!gpgJE$LC!_76`va z5`?S~VU5aoenj6vVcgNDIx#kemaane5+NM|^Wc{*0>5+;w0ZdFJNaznd_K1fi05EF ze@rrsc0tT6mttu4D5_l}n38 zhw6l6NHGmqKW#{QXARa1DLFopJCcs5BCbnxC7ira$fkl!A2Lc}0(?qWq-t$V>m?1v zOsyA0@C!K!$xJ4mZ>&eFN>YN1D3BOGwrWf`q(GIh$z~647=(8#2%i;&h(k zS$st?(-k!o4Qe^QuWOfLd7IBTUx;HcRbACF7D_e~%xBY;k{EYlR6f%|-UBQvSz*2v z>Jazyxo|(GL|4r?T}KrYuy2Wt;kXpc?s`7A8cs1EApW6_mKo+*Xe!~vf8%1NjePG> zF>!R0_R&;CV`x7|^GmTZO=S`CXmKQO&O<&vj@OvYq&*zT6)V$H7meYwG0gj4 z#A}N)C?w0daR2OgjgpyZ<~I0pj_vhRh*_wF`JX?MG~le_-W;Vd6b;DfL{Gy#hkBr8 z6Ft?8!tTP^w+gV2sk#zgpyz(&%tB|WN_0Fg(%>A1As;s6Y_v_!X;o@33kw~|3S#Ml zx=Kg@%jVJ=ltp0~Yh?zV8X}coJ4Y`j|Bp(yi*XfKVFsSB+xocXLiRaEzOgEh!2KXRMP58>kNf-6 zzaP;AADqMwY5|Q5--Jm4sy>IUP_xH8=?NeQK7c9 zOM4W0F+<0<^F{kUTKIgZI4sP=8n- oK2O+RK@O?)`XTz>GH<__%+oiPxm!M$Yl}bEZulB=8a@gA16+CYbpQYW literal 0 HcmV?d00001 diff --git a/tests/hipscat_import/data/small_sky_object_catalog/catalog_info.json b/tests/hipscat_import/data/small_sky_object_catalog/catalog_info.json new file mode 100644 index 00000000..451af75e --- /dev/null +++ b/tests/hipscat_import/data/small_sky_object_catalog/catalog_info.json @@ -0,0 +1,8 @@ +{ + "catalog_name": "small_sky_object_catalog", + "catalog_type": "object", + "epoch": "J2000", + "ra_kw": "ra", + "dec_kw": "dec", + "total_rows": 131 +} diff --git a/tests/hipscat_import/data/small_sky_object_catalog/partition_info.csv b/tests/hipscat_import/data/small_sky_object_catalog/partition_info.csv new file mode 100644 index 00000000..ed015721 --- /dev/null +++ b/tests/hipscat_import/data/small_sky_object_catalog/partition_info.csv @@ -0,0 +1,2 @@ +Norder,Dir,Npix,num_rows +0,0,11,131 diff --git a/tests/hipscat_import/data/small_sky_object_catalog/point_map.fits b/tests/hipscat_import/data/small_sky_object_catalog/point_map.fits new file mode 100644 index 0000000000000000000000000000000000000000..e7287c9ffe13671c674f9d9a3f1d268f789dc544 GIT binary patch literal 8640 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 literal 0 HcmV?d00001 diff --git a/tests/hipscat_import/data/small_sky_source_catalog/Norder=1/Dir=0/Npix=47.parquet b/tests/hipscat_import/data/small_sky_source_catalog/Norder=1/Dir=0/Npix=47.parquet new file mode 100644 index 0000000000000000000000000000000000000000..fb86e97aeb7de711aed3af3576a9a539ab035948 GIT binary patch literal 146699 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

k9eq>=(d$Jwtu{Q1~5AP*q@s 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