diff --git a/.github/workflows/fuzzy-testing.yml b/.github/workflows/fuzzy-testing.yml new file mode 100644 index 0000000..e5946c3 --- /dev/null +++ b/.github/workflows/fuzzy-testing.yml @@ -0,0 +1,279 @@ +# Copyright (C) 2024 Roberto Rossini (roberros@uio.no) +# SPDX-License-Identifier: MIT + +name: Run fuzzy tests + +on: + push: + branches: [main] + paths: + - ".github/workflows/fuzzy-testing.yml" + - "cmake/**" + - "src/**" + - "test/scripts/fuzzer.py" + - "utils/devel/stubgen.py" + - "utils/devel/symlink_pyarrow_libs.py" + - "CMakeLists.txt" + - "conanfile.py" + - "pyproject.toml" + + pull_request: + paths: + - "cmake/**" + - "src/**" + - "test/scripts/fuzzer.py" + - "utils/devel/stubgen.py" + - "utils/devel/symlink_pyarrow_libs.py" + - "CMakeLists.txt" + - "conanfile.py" + - "pyproject.toml" + + schedule: + # Run weekly + - cron: "15 3 * * 0" + + workflow_dispatch: + inputs: + duration: + description: "Test duration in seconds" + required: true + default: "600" + type: string + + resolution: + description: "Matrix resolution to use for testing" + required: true + default: "random" + type: string + +# https://stackoverflow.com/a/72408109 +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +defaults: + run: + shell: bash + +jobs: + build-project: + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + include: + - { + dataset: "4DNFIYECESRC", + format: "cool", + normalization: "NONE", + bin-type: "fixed", + } + - { + dataset: "4DNFIYECESRC", + format: "cool", + normalization: "weight", + bin-type: "fixed", + } + - { + dataset: "4DNFIYECESRC", + format: "cool", + normalization: "VC", + bin-type: "fixed", + } + - { + dataset: "4DNFIYECESRC", + format: "cool", + normalization: "NONE", + bin-type: "variable", + } + - { + dataset: "4DNFIYECESRC", + format: "hic8", + normalization: "NONE", + bin-type: "fixed", + } + - { + dataset: "4DNFIYECESRC", + format: "hic8", + normalization: "KR", + bin-type: "fixed", + } + - { + dataset: "4DNFIYECESRC", + format: "hic9", + normalization: "NONE", + bin-type: "fixed", + } + - { + dataset: "4DNFIYECESRC", + format: "hic9", + normalization: "VC", + bin-type: "fixed", + } + + container: + image: ghcr.io/paulsengroup/ci-docker-images/ubuntu-24.04-cxx-clang-19 + options: "--user=root" + + env: + CCACHE_DISABLE: "1" + CONAN_HOME: "/opt/conan/" + HICTK_CI: "1" + + steps: + - name: Clone hictkpy + uses: actions/checkout@v4 + + - name: Fix permissions + run: | + chown -R $(id -u):$(id -g) $PWD + + - name: Generate cache key + id: cache-key + run: | + hash="${{ hashFiles('conanfile.py') }}" + + echo "key=fuzzer-$hash" >> $GITHUB_OUTPUT + + - name: Install Python + run: | + apt-get update + apt-get install -y python3.12 python3.12-dev + + - name: Restore Conan cache + id: cache-conan + uses: actions/cache/restore@v4 + with: + key: conan-${{ steps.cache-key.outputs.key }} + path: ${{ env.CONAN_HOME }}/p + + - name: Clean Conan cache (pre-build) + if: steps.cache-conan.outputs.cache-hit != 'true' + run: | + conan cache clean "*" --build + conan cache clean "*" --download + conan cache clean "*" --source + conan remove --confirm "*" + + - name: Build and install + run: pip install --verbose . + + - name: Clean Conan cache (post-build) + if: steps.cache-conan.outputs.cache-hit != 'true' + run: | + conan cache clean "*" --build + conan cache clean "*" --download + conan cache clean "*" --source + + - name: Save Conan cache + uses: actions/cache/save@v4 + if: steps.cache-conan.outputs.cache-hit != 'true' + with: + key: conan-${{ steps.cache-key.outputs.key }} + path: ${{ env.CONAN_HOME }}/p + + - name: Install test dependencies + run: | + pip install --no-cache-dir 'cooler==0.10.*' 'numpy<2' + + - name: Detect CI type + id: ci-type + run: | + if git log --format=%B -n 1 ${{ github.event.after }} | grep -qF '[ci full]'; then + echo "type=full" >> $GITHUB_OUTPUT + else + echo "type=short" >> $GITHUB_OUTPUT + fi + + - name: Prepare for test + id: test-params + run: | + duration=120 + if [[ ${{ github.event_name }} == 'workflow_dispatch' ]]; then + duration='${{ inputs.duration }}' + elif [[ '${{ steps.ci-type.outputs.type }}' == 'full' ]]; then + duration=3600 + fi + + resolution=50000 + if [[ ${{ github.event_name }} == 'workflow_dispatch' ]]; then + if [[ '${{ inputs.resolution }}' == 'random' ]]; then + resolution="$( + python3 -c 'import random; import sys; print(random.choice([int(x) for x in sys.argv[1:]]))' \ + 1000 5000 10000 50000 100000 500000 + )" + else + resolution='${{ inputs.resolution }}' + fi + fi + + if [[ '${{ matrix.bin-type }}' == variable ]]; then + resolution=0 + fi + + 2>&1 echo "Duration: ${duration}" + 2>&1 echo "Resolution: ${resolution}" + + echo "duration=$duration" | tee -a "$GITHUB_OUTPUT" + echo "resolution=$resolution" | tee -a "$GITHUB_OUTPUT" + + - name: Clone hictk + uses: actions/checkout@v4 + with: + repository: "paulsengroup/hictk" + path: hictk + + - name: Download test datasets + run: | + hictk/test/fuzzer/scripts/download_test_datasets.py \ + hictk/test/fuzzer/test_files.json \ + . \ + --format cool "${{ matrix.format }}" \ + --resolution "${{ steps.test-params.outputs.resolution }}" \ + --dataset "${{ matrix.dataset }}" \ + --nproc 2 + + - name: Run test (df) + run: | + test/scripts/fuzzer.py \ + --resolution ${{ steps.test-params.outputs.resolution }} \ + --duration '${{ steps.test-params.outputs.duration }}' \ + --normalization ${{ matrix.normalization }} \ + --nproc $(nproc) \ + --format df \ + *".${{ matrix.format }}" \ + *.cool + + - name: Run test (numpy) + run: | + test/scripts/fuzzer.py \ + --resolution ${{ steps.test-params.outputs.resolution }} \ + --duration '${{ steps.test-params.outputs.duration }}' \ + --normalization ${{ matrix.normalization }} \ + --nproc $(nproc) \ + --format numpy \ + *".${{ matrix.format }}" \ + *.cool + + - name: Run test (coo) + run: | + test/scripts/fuzzer.py \ + --resolution ${{ steps.test-params.outputs.resolution }} \ + --duration '${{ steps.test-params.outputs.duration }}' \ + --normalization ${{ matrix.normalization }} \ + --nproc $(nproc) \ + --format csr \ + *".${{ matrix.format }}" \ + *.cool + + fuzzy-testing-status-check: + name: Status Check (fuzzy-testing) + if: ${{ always() }} + runs-on: ubuntu-latest + needs: + - build-project + + steps: + - name: Collect job results + if: needs.build-project.result != 'success' + run: exit 1 diff --git a/pyproject.toml b/pyproject.toml index 5c9a56d..a5770e8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -74,6 +74,12 @@ filterwarnings = [ "ignore:(ast.Str|Attribute s|ast.NameConstant|ast.Num) is deprecated:DeprecationWarning:_pytest", "ignore:datetime\\.datetime\\.utcfromtimestamp\\(\\)*:DeprecationWarning", # https://github.com/pytest-dev/pytest/issues/11528 ] +python_files = [ + "test/test*.py" +] +testpaths = [ + "test/" +] [tool.cibuildwheel] skip = ["cp313*", "*musllinux*", "pp*"] diff --git a/src/hictkpy_pixel_selector.cpp b/src/hictkpy_pixel_selector.cpp index 73ef249..a0397e1 100644 --- a/src/hictkpy_pixel_selector.cpp +++ b/src/hictkpy_pixel_selector.cpp @@ -344,10 +344,10 @@ hictk::internal::NumericVariant PixelSelector::parse_count_type(std::string_view if (type == "int64") { return {std::int64_t{}}; } - if (type == "float32" || type == "float") { + if (type == "float32") { return {float{}}; } - if (type == "float64" || type == "double") { + if (type == "float64" || type == "float" || type == "double") { return {double{}}; } diff --git a/test/scripts/fuzzer.py b/test/scripts/fuzzer.py new file mode 100755 index 0000000..fe7de27 --- /dev/null +++ b/test/scripts/fuzzer.py @@ -0,0 +1,546 @@ +#!/usr/bin/env python3 + +# Copyright (C) 2024 Roberto Rossini +# +# SPDX-License-Identifier: MIT + +import argparse +import itertools +import logging +import multiprocessing as mp +import pathlib +import random +import sys +import time +from typing import Any, Dict, List, Tuple + +import cooler +import numpy as np +import numpy.typing as npt +import pandas as pd +import scipy.sparse as ss + +import hictkpy + + +def make_cli() -> argparse.ArgumentParser: + def nproc() -> int: + return mp.cpu_count() + + def positive_int(arg) -> int: + if (n := int(arg)) > 0: + return n + + raise ValueError("Not a positive integer") + + def non_negative_int(arg) -> int: + if (n := int(arg)) >= 0: + return n + + raise ValueError("Not a non-negative integer") + + def valid_fraction(arg) -> float: + if (n := float(arg)) >= 0 and n <= 1: + return n + + raise ValueError("Not a number between 0 and 1") + + def valid_nproc(arg) -> int: + if 1 <= (n := int(arg)) <= nproc(): + return n + + raise ValueError(f"Not a number between 1 and {nproc()}") + + cli = argparse.ArgumentParser(description="Fuzzer for hictkpy.") + + cli.add_argument( + "test-uri", + type=pathlib.Path, + help="Path to a .cool, .mcool or hic file to be used as test file.", + ) + cli.add_argument( + "reference-uri", + type=pathlib.Path, + help="Path to a .cool or .mcool file to be used as reference.", + ) + cli.add_argument( + "--resolution", + type=non_negative_int, + help="Matrix resolution.\n" + "Required when one or both of test-uri and reference-uri are multi-resolution files.", + ) + cli.add_argument( + "--1d-to-2d-query-ratio", + type=valid_fraction, + default=0.33, + help="Ratio of 1D to 2D queries. Use 0 or 1 to only test 1D or 2D queries.", + ) + cli.add_argument( + "--duration", + type=positive_int, + default=60, + help="Duration in seconds.", + ) + cli.add_argument( + "--format", + type=str, + choices={"df", "numpy", "csr"}, + default="df", + help="Format used to fetch pixels.", + ) + cli.add_argument( + "--query-length-avg", + type=positive_int, + default=1_000_000, + help="Average query size.", + ) + cli.add_argument( + "--query-length-std", + type=non_negative_int, + default=250_000, + help="Standard deviation for query size.", + ) + cli.add_argument( + "--normalization", + type=str, + default="NONE", + help="Name of the dataset to use for balancing.", + ) + cli.add_argument( + "--join", + action="store_true", + default=False, + help="Fetch pixels in BG2 format.\n" "Ignored when --format is not df.", + ) + cli.add_argument( + "--seed", + type=int, + help="Seed used for PRNG.", + ) + cli.add_argument( + "--nproc", + type=valid_nproc, + default=nproc(), + help="Number of test processes to run in parallel.", + ) + return cli + + +def postproc_df(df: pd.DataFrame) -> pd.DataFrame: + if "balanced" in df: + df["count"] = df["balanced"] + + df["chrom1"] = df["chrom1"].astype(str) + df["chrom2"] = df["chrom2"].astype(str) + return df.set_index(["chrom1", "start1", "end1", "chrom2", "start2", "end2"])[["count"]] + + +def cooler_dump(selector, query1: str, query2: str) -> pd.DataFrame | ss.coo_matrix | npt.NDArray: + logging.debug("[cooler] running query for %s, %s...", query1, query2) + data = selector.fetch(query1, query2) + if isinstance(data, pd.DataFrame): + return postproc_df(data) + + return data + + +def hictk_dump( + file, + query1: str, + query2: str, + normalization: str = "NONE", + query_type: str = "df", +) -> pd.DataFrame | npt.NDArray | ss.csr_matrix | List[Any]: + logging.debug("[hictkpy] running query for %s, %s...", query1, query2) + if query_type == "df": + return postproc_df(file.fetch(query1, query2, normalization, join=True).to_df()) + if query_type == "csr": + return file.fetch(query1, query2, normalization).to_csr(query_span="full") + if query_type == "numpy": + return file.fetch(query1, query2, normalization).to_numpy(query_span="full") + + raise NotImplementedError + + +def read_chrom_sizes_cooler(path_to_cooler_file: str) -> Dict[str, int]: + return cooler.Cooler(path_to_cooler_file).chromsizes.to_dict() + + +def generate_query_1d(chroms, weights: npt.NDArray, mean_length: float, stddev_length: float) -> str: + chrom_name, chrom_size = random.choices(chroms, weights=weights, k=1)[0] + + query_length = max(2.0, random.gauss(mu=mean_length, sigma=stddev_length)) + + center_pos = random.randint(0, chrom_size) + start_pos = max(0.0, center_pos - (query_length / 2)) + end_pos = min(chrom_size, start_pos + query_length) + + return f"{chrom_name}:{start_pos:.0f}-{end_pos:.0f}" + + +def generate_query_2d( + chroms, + weights: npt.NDArray, + ranks: Dict[str, int], + mean_length: float, + stddev_length: float, +) -> Tuple[str, str]: + q1 = generate_query_1d(chroms, weights, mean_length, stddev_length) + q2 = generate_query_1d(chroms, weights, mean_length, stddev_length) + + chrom1, _, coord1 = q1.partition(":") + chrom2, _, coord2 = q2.partition(":") + + if ranks[chrom1] > ranks[chrom2]: + q1, q2 = q2, q1 + + if chrom1 == chrom2: + start1, _, _ = coord1.partition("-") + start2, _, _ = coord2.partition("-") + if int(start1) > int(start2): + q1, q2 = q2, q1 + + return q1, q2 + + +def find_differences_df(df1: pd.DataFrame, df2: pd.DataFrame) -> pd.DataFrame: + df = df1.merge( + df2, + how="outer", + left_index=True, + right_index=True, + suffixes=("1", "2"), + ) + # We're mapping False to None so that we can more easily drop identical rows with dropna() + df["count_close_enough"] = pd.Series(np.isclose(df["count1"], df["count2"])).map({False: None}) + + # We're dropping the counts to avoid incorrectly flagging rows with nan as counts + return df.drop(columns=["count1", "count2"]).dropna() + + +def compare_dfs(worker_id: int, q1: str, q2: str, expected: pd.DataFrame, found: pd.DataFrame) -> bool: + if len(expected) != len(found): + logging.warning( + "[%d] %s, %s: FAIL! Expected %d nnz, found %d!", + worker_id, + q1, + q2, + len(expected), + len(found), + ) + return False + + if len(expected) != 0: + diff = find_differences_df(expected, found) + if len(diff) != 0: + logging.warning( + "[%d] %s, %s (%d nnz): FAIL! Found %d differences!", + worker_id, + q1, + q2, + len(expected), + len(diff), + ) + return False + + logging.debug("[%d] %s, %s (%d nnz): OK!", worker_id, q1, q2, len(expected)) + return True + + +def compare_numpy( + worker_id: int, q1: str, q2: str, expected: npt.NDArray, found: npt.NDArray, rtol: float = 1.0e-5 +) -> bool: + nnz = np.sum(expected != 0) + if expected.shape != found.shape: + logging.warning( + "[%d] %s, %s (%d nnz): FAIL! Numpy matrices have different shapes! Expected %s, found %s", + worker_id, + q1, + q2, + nnz, + expected.shape, + found.shape, + ) + return False + + num_differences = (~np.isclose(expected, found, rtol=rtol, equal_nan=True)).sum() + if num_differences != 0: + logging.warning( + "[%d] %s, %s (%d nnz): FAIL! Found %d differences!", + worker_id, + q1, + q2, + nnz, + num_differences, + ) + return False + + logging.debug("[%d] %s, %s (%d nnz): OK!", worker_id, q1, q2, np.sum(expected != 0)) + return True + + +def compare_csr( + worker_id: int, q1: str, q2: str, expected: ss.csr_matrix, found: ss.csr_matrix, rtol: float = 1.0e-5 +) -> bool: + if expected.shape != found.shape: + logging.warning( + "[%d] %s, %s (%d nnz): FAIL! CSR matrices have different shapes! Expected %s, found %s", + worker_id, + q1, + q2, + expected.nnz, + expected.shape, + found.shape, + ) + return False + + if expected.nnz != found.nnz: + logging.warning( + "[%d] %s, %s (%d nnz): FAIL! CSR matrices have different nnz! Expected %d, found %d", + worker_id, + q1, + q2, + expected.nnz, + expected.nnz, + found.nnz, + ) + return False + + expected.sort_indices() + found.sort_indices() + + num_differences = (expected.indices != found.indices).sum() + if num_differences != 0: + logging.warning( + "[%d] %s, %s (%d nnz): FAIL! Found %d differences in CSR matrix.indices!", + worker_id, + q1, + q2, + expected.nnz, + num_differences, + ) + return False + + num_differences = (expected.indptr != found.indptr).sum() + if num_differences != 0: + logging.warning( + "[%d] %s, %s (%d nnz): FAIL! Found %d differences in CSR matrix.indptr!", + worker_id, + q1, + q2, + expected.nnz, + num_differences, + ) + return False + + num_differences = (~np.isclose(expected.data, found.data, rtol=rtol, equal_nan=True)).sum() + if num_differences != 0: + logging.warning( + "[%d] %s, %s (%d nnz): FAIL! Found %d differences!", + worker_id, + q1, + q2, + expected.nnz, + num_differences, + ) + return False + + logging.debug("[%d] %s, %s (%d nnz): OK!", worker_id, q1, q2, expected.nnz) + return True + + +def results_are_identical(worker_id: int, q1: str, q2: str, expected, found) -> bool: + if isinstance(found, pd.DataFrame): + assert isinstance(expected, type(found)) + return compare_dfs(worker_id, q1, q2, expected, found) + if isinstance(found, (np.ndarray, np.generic)): + assert isinstance(expected, type(found)) + return compare_numpy(worker_id, q1, q2, expected, found) + if isinstance(found, ss.csr_matrix): + return compare_csr(worker_id, q1, q2, expected.tocsr(), found) + + raise NotImplementedError + + +def seed_prng(worker_id: int, seed): + seed = hash(tuple([worker_id, seed])) + logging.info("[%d] seed: %d", worker_id, seed) + random.seed(seed) + + +def worker( + path_to_file: pathlib.Path, + path_to_reference_file: pathlib.Path, + resolution: int, + chroms_flat, + chrom_ranks, + query_type: str, + query_length_mu: float, + query_length_std: float, + _1d_to_2d_query_ratio: float, + balance: str, + seed: int | None, + worker_id: int, + end_time, + early_return, +) -> Tuple[int, int]: + setup_logger(logging.INFO) + + if seed is None: + seed = random.randint(0, 2**64) + + num_failures = 0 + num_queries = 0 + + clr_matrix_args = { + "balance": balance if balance != "NONE" else False, + } + + try: + if query_type == "df": + clr_matrix_args["as_pixels"] = True + clr_matrix_args["join"] = True + elif query_type == "csr": + clr_matrix_args["sparse"] = True + elif query_type == "numpy": + pass + else: + raise NotImplementedError + + seed_prng(worker_id, seed) + + chrom_sizes = np.array([n for _, n in chroms_flat], dtype=int) + weights = chrom_sizes / chrom_sizes.sum() + + clr = cooler.Cooler(str(path_to_reference_file)) + sel = clr.matrix(**clr_matrix_args) + + f = hictkpy.File(str(path_to_file), resolution) + + while time.time() < end_time: + if early_return.value: + logging.debug( + "[%d] early return signal received. Returning immediately!", + worker_id, + ) + break + + if _1d_to_2d_query_ratio <= random.random(): + q1, q2 = generate_query_2d( + chroms_flat, + weights, + chrom_ranks, + mean_length=query_length_mu, + stddev_length=query_length_std, + ) + else: + q1 = generate_query_1d( + chroms_flat, + weights, + mean_length=query_length_mu, + stddev_length=query_length_std, + ) + q2 = q1 + + num_queries += 1 + + expected = cooler_dump(sel, q1, q2) + found = hictk_dump(f, q1, q2, balance, query_type) + + if not results_are_identical(worker_id, q1, q2, expected, found): + num_failures += 1 + + except: # noqa + logging.debug( + "[%d] exception raised in worker process. Sending early return signal!", + worker_id, + ) + early_return.value = True + raise + + return num_queries, num_failures + + +def main() -> int: + args = vars(make_cli().parse_args()) + + if cooler.fileops.is_multires_file(str(args["reference-uri"])) and args["resolution"] is None: + raise RuntimeError("--resolution is required when test-uri or reference-uri are multi-resolution files.") + + reference_uri = str(args["reference-uri"]) + resolution = args["resolution"] + + if cooler.fileops.is_multires_file(reference_uri): + if resolution is None: + raise RuntimeError( + "--resolution is a mandatory option when one or both of test-uri and reference-uri are multi-resolution files." + ) + reference_uri = f"{reference_uri}::/resolutions/{resolution}" + + if cooler.fileops.is_cooler(reference_uri) and resolution is not None: + found_res = cooler.Cooler(reference_uri).binsize + if found_res is None: + found_res = 0 + if found_res != resolution: + raise RuntimeError( + f'Cooler at "{reference_uri}" has an unexpected resolution: expected {resolution}, found {found_res}.' + ) + + chroms = read_chrom_sizes_cooler(reference_uri) + + chrom_ranks = {chrom: i for i, chrom in enumerate(chroms.keys())} + chroms_flat = list(chroms.items()) + + end_time = time.time() + args["duration"] + + with mp.Pool(args["nproc"]) as pool, mp.Manager() as manager: + early_return = manager.Value(bool, False) + results = pool.starmap( + worker, + zip( + itertools.repeat(args["test-uri"]), + itertools.repeat(reference_uri), + itertools.repeat(resolution), + itertools.repeat(chroms_flat), + itertools.repeat(chrom_ranks), + itertools.repeat(args["format"]), + itertools.repeat(args["query_length_avg"]), + itertools.repeat(args["query_length_std"]), + itertools.repeat(args["1d_to_2d_query_ratio"]), + itertools.repeat(args["normalization"]), + itertools.repeat(args["seed"]), + range(1, args["nproc"] + 1), + itertools.repeat(end_time), + itertools.repeat(early_return), + ), + chunksize=1, + ) + + num_queries = sum((n for n, _ in results)) + num_failures = sum((n for _, n in results)) + num_passes = num_queries - num_failures + if num_failures == 0: + lvl = logging.INFO + else: + lvl = logging.WARN + + logging.log( + lvl, + "Score: %.4g/100 (%d success and %d failures).", + 100 * num_passes / num_queries, + num_passes, + num_failures, + ) + + return num_failures != 0 + + +def setup_logger(level=logging.INFO): + fmt = "[%(asctime)s] %(levelname)s: %(message)s" + logging.basicConfig(format=fmt) + logging.getLogger().setLevel(level) + + +if __name__ == "__main__": + setup_logger() + sys.exit(main()) diff --git a/test/test_fetch_dense.py b/test/test_fetch_dense.py index df2e6df..c4e5b76 100644 --- a/test/test_fetch_dense.py +++ b/test/test_fetch_dense.py @@ -40,7 +40,7 @@ def test_cis(self, file, resolution): assert m.dtype == np.int32 m = f.fetch("chr2R:10,000,000-15,000,000", count_type="float").to_numpy() - assert m.dtype == np.float32 + assert m.dtype == np.float64 m = f.fetch("chr2R\t10000000\t15000000", query_type="BED").to_numpy() assert m.shape == (50, 50) @@ -67,7 +67,7 @@ def test_trans(self, file, resolution): assert m.dtype == np.int32 m = f.fetch("chr2R:10,000,000-15,000,000", "chrX:0-10,000,000", count_type="float").to_numpy() - assert m.dtype == np.float32 + assert m.dtype == np.float64 m = f.fetch("chr2R\t10000000\t15000000", "chrX\t0\t10000000", query_type="BED").to_numpy() assert m.shape == (50, 100) diff --git a/test/test_fetch_df.py b/test/test_fetch_df.py index a592071..20247e3 100644 --- a/test/test_fetch_df.py +++ b/test/test_fetch_df.py @@ -46,7 +46,7 @@ def test_cis(self, file, resolution): assert df["count"].dtype == np.int32 df = f.fetch("chr2R:10,000,000-15,000,000", count_type="float").to_df() - assert df["count"].dtype == np.float32 + assert df["count"].dtype == np.float64 df = f.fetch("chr2R\t10000000\t15000000", query_type="BED").to_df() assert len(df) == 1275 @@ -77,7 +77,7 @@ def test_trans(self, file, resolution): assert df["count"].dtype == np.int32 df = f.fetch("chr2R:10,000,000-15,000,000", "chrX:0-10,000,000", count_type="float").to_df() - assert df["count"].dtype == np.float32 + assert df["count"].dtype == np.float64 df = f.fetch("chr2R\t10000000\t15000000", "chrX\t0\t10000000", query_type="BED").to_df() assert len(df) == 4995 diff --git a/test/test_fetch_sparse.py b/test/test_fetch_sparse.py index 6a681c9..b098356 100644 --- a/test/test_fetch_sparse.py +++ b/test/test_fetch_sparse.py @@ -39,7 +39,7 @@ def test_cis(self, file, resolution): assert m.dtype == np.int32 m = f.fetch("chr2R:10,000,000-15,000,000", count_type="float").to_coo() - assert m.dtype == np.float32 + assert m.dtype == np.float64 m = f.fetch("chr2R\t10000000\t15000000", query_type="BED").to_coo() assert m.shape == (50, 50) @@ -63,7 +63,7 @@ def test_trans(self, file, resolution): assert m.dtype == np.int32 m = f.fetch("chr2R:10,000,000-15,000,000", "chrX:0-10,000,000", count_type="float").to_coo() - assert m.dtype == np.float32 + assert m.dtype == np.float64 m = f.fetch("chr2R\t10000000\t15000000", "chrX\t0\t10000000", query_type="BED").to_coo() assert m.shape == (50, 100)