Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 22 additions & 1 deletion .github/workflows/pypi-release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -67,11 +67,32 @@ jobs:

- name: Build distributions
if: steps.version_check.outputs.should_publish == 'true'
env:
DAISY_PORTABLE_BUILD: "1"
run: python -m build

# Keep these steps in step with release-check.yml, or the dry-run stops
# rehearsing what actually gets uploaded.
- name: Repair the Linux wheel
if: steps.version_check.outputs.should_publish == 'true'
run: |
set -euo pipefail
pip install auditwheel patchelf
mkdir -p wheelhouse
for wheel in dist/*.whl; do
auditwheel repair "$wheel" --wheel-dir wheelhouse
done
rm dist/*.whl
mv wheelhouse/*.whl dist/
ls -l dist/

- name: Verify distributions
if: steps.version_check.outputs.should_publish == 'true'
run: twine check dist/*
run: twine check --strict dist/*

- name: Verify artifacts are publishable
if: steps.version_check.outputs.should_publish == 'true'
run: python scripts/check_release_artifacts.py --dist-dir dist

- name: Publish to PyPI
if: steps.version_check.outputs.should_publish == 'true'
Expand Down
167 changes: 167 additions & 0 deletions .github/workflows/release-check.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
name: Release dry-run

# Rehearses the PyPI release on every pull request, so a release that would be
# rejected is caught here instead of on a push to main. It mirrors the
# environment and the build steps of pypi-release.yml exactly, but stops short
# of `twine upload`.
#
# Note that this covers ground ci.yml does not: ci.yml builds via CMake with
# -DBUILD_SING=OFF, while setup.py always compiles every source in the package.
# A file CMake skips can therefore break the release build while CI stays green.
on:
pull_request:
branches: [main]
push:
branches: [main]
workflow_dispatch: {}

jobs:
dry-run:
runs-on: ubuntu-latest
permissions:
contents: read

steps:
- name: Checkout code
uses: actions/checkout@v4
with:
submodules: recursive

# Kept in step with pypi-release.yml: the release publishes a single
# cp312 wheel, so that is the interpreter the rehearsal has to use.
- name: Setup Python
uses: actions/setup-python@v4
with:
python-version: "3.12"

- name: Install system dependencies
run: |
sudo apt-get update
sudo apt-get install -y build-essential cmake libomp-dev

- name: Install Python build dependencies
run: |
python -m pip install --upgrade pip
pip install -r requirements_DaiSy.txt

# The step that would have caught the current breakage: `python -m build`
# compiles the full extension, including sources CMake leaves out.
# The output is teed to a file so a compiler error stays downloadable as
# an artifact; the raw job log needs a GitHub token to read back.
- name: Build distributions
env:
DAISY_PORTABLE_BUILD: "1"
run: |
set -o pipefail
python -m build 2>&1 | tee build.log

# `python -m build` tags a Linux wheel linux_x86_64, which PyPI refuses.
# auditwheel rewrites the tag to the glibc the wheel actually needs and
# vendors in the shared libraries it links, libgomp among them.
- name: Repair the Linux wheel
run: |
set -euo pipefail
# auditwheel shells out to patchelf and wants >= 0.14; the PyPI
# package ships a current binary, so we do not depend on whatever
# the runner image happens to carry.
pip install auditwheel patchelf
mkdir -p wheelhouse
for wheel in dist/*.whl; do
auditwheel repair "$wheel" --wheel-dir wheelhouse
done
rm dist/*.whl
mv wheelhouse/*.whl dist/
ls -l dist/

# gcc prints one error and then many follow-on lines, so surface the
# first errors in the job summary instead of making people scroll.
- name: Summarise build errors
if: failure()
run: |
{
echo "### First errors from \`python -m build\`"
echo '```'
grep -E -m 20 '(^|:)[0-9]+:[0-9]+: error:|^error:|^ERROR' build.log || echo "no error lines matched; see the build-log artifact"
echo '```'
} >> "$GITHUB_STEP_SUMMARY"

- name: Verify distribution metadata
run: twine check --strict dist/*

- name: Verify artifacts are publishable
run: python scripts/check_release_artifacts.py --dist-dir dist --check-pypi

# Installing the built wheel rather than the source tree is what makes
# this a packaging test: it catches a package that compiles but ships
# without its Python files or its compiled extension.
- name: Install the built wheel into a clean environment
run: |
python -m venv /tmp/wheel-test
/tmp/wheel-test/bin/pip install --upgrade pip
/tmp/wheel-test/bin/pip install dist/*.whl

# daisy/__init__.py turns a failed extension load into a warning, and the
# next import of daisy._core then reports "already registered" rather than
# the original cause. Exec the .so on its own, in its own process, so a
# broken extension reports the error it actually raised.
- name: Check the extension initialises
run: |
cd /tmp
/tmp/wheel-test/bin/python - <<'PY'
import glob
import importlib.util
import os

# find_spec locates the package without running its __init__.py.
spec = importlib.util.find_spec("daisy")
assert spec is not None and spec.origin, "daisy is not importable at all"

matches = glob.glob(os.path.join(os.path.dirname(spec.origin), "_core*.so"))
assert matches, "the wheel installed no _core extension"

core_spec = importlib.util.spec_from_file_location("_core", matches[0])
module = importlib.util.module_from_spec(core_spec)
core_spec.loader.exec_module(module)

print("extension initialised:", matches[0])
print("exported:", sorted(n for n in dir(module) if n[0].isupper()))
PY

- name: Smoke-test the installed package
run: |
cd /tmp
/tmp/wheel-test/bin/python - <<'PY'
import numpy as np

import daisy
from daisy import _core

print("daisy imported from", daisy.__file__)
print("extension imported from", _core.__file__)

rng = np.random.default_rng(0)
database = rng.random((256, 64), dtype=np.float32)
# searchIndex takes a 2D batch of queries, one row per query.
query = database[7:8].copy()

search = daisy.BruteForceSearch(daisy.DistanceType.L2_SQUARED)
search.buildIndex(database)
indices, distances = search.searchIndex(query, 5)

print("top-5 indices:", indices[0])
print("top-5 distances:", distances[0])

assert indices[0][0] == 7, "nearest neighbour of a database row must be itself, got %r" % (indices[0][0],)
assert distances[0][0] < 1e-4, "self-distance should be ~0, got %r" % (distances[0][0],)
print("smoke test passed")
PY

- name: Upload distributions and build log for inspection
if: always()
uses: actions/upload-artifact@v4
with:
name: release-check-${{ github.sha }}
path: |
dist/
build.log
if-no-files-found: warn
6 changes: 3 additions & 3 deletions MANIFEST.in
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
include README.md
include requirements_DaiSy.txt
recursive-include daisy *.py
recursive-include lib *.hpp *.cpp
recursive-include commons *.hpp *.cpp
recursive-include pybinds *.cpp
recursive-include lib *.h *.hpp *.cpp
recursive-include commons *.h *.hpp *.cpp
recursive-include pybinds *.h *.hpp *.cpp
recursive-exclude build *
recursive-exclude DaiSy_env *
recursive-exclude faiss_env *
Expand Down
2 changes: 1 addition & 1 deletion daisy/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
(distance-r) search via searchIndex(query, SearchConfig(type=QueryType.RANGE, r=r)).
"""

__version__ = "1.0.0"
__version__ = "1.0.9"
__author__ = ""

try:
Expand Down
34 changes: 33 additions & 1 deletion lib/algos/Sing.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,11 @@ namespace daisy
#else
void LBDfloatstreamGPU(sax_type *gsaxarray, float *positionmap, ts_type *paa, float *gqts, float bsf,
unsigned long size, float *gpositionmap, int paa_segments, float mindist_sqrt);
void LBDshortstreamGPUinsidedynamicratev2dtw(sax_type *gsaxarray, short *positionmap,
ts_type *paaU, ts_type *paaL, float *gqtsU, float *gqtsL,
float bsf, unsigned long size, short *gpositionmap,
int paa_segments, float mindist_sqrt, unsigned long *gpuoffset,
float shortrate, int chunknumber, bool *activechunk);
#endif
first_buffer_layer2 *initialize_simrec(int initial_buffer_size, int number_of_buffers,
int max_total_buffers_size, isax_index *index);
Expand Down Expand Up @@ -1363,7 +1368,34 @@ namespace daisy
(void)gpositionmap;
(void)paa_segments;
(void)mindist_sqrt;


}

// The DTW search stages this on the GPU between two barriers, so the CPU
// build has to leave gpuoffset alone: the worker threads are already
// running and hold a pointer to it.
void LBDshortstreamGPUinsidedynamicratev2dtw(sax_type *gsaxarray, short *positionmap,
ts_type *paaU, ts_type *paaL, float *gqtsU, float *gqtsL,
float bsf, unsigned long size, short *gpositionmap,
int paa_segments, float mindist_sqrt, unsigned long *gpuoffset,
float shortrate, int chunknumber, bool *activechunk)
{
(void)gsaxarray;
(void)positionmap;
(void)paaU;
(void)paaL;
(void)gqtsU;
(void)gqtsL;
(void)bsf;
(void)size;
(void)gpositionmap;
(void)paa_segments;
(void)mindist_sqrt;
(void)gpuoffset;
(void)shortrate;
(void)chunknumber;
(void)activechunk;

}
#endif

Expand Down
6 changes: 4 additions & 2 deletions lib/utils/SIMD.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,10 @@
#include <stdexcept>
#include <string>

// Detect AVX support
#if defined(__AVX__) || defined(_M_AVX)
// Detect AVX2 support. The guarded code uses AVX2 intrinsics (_mm256_srlv_epi32,
// _mm256_cvtepu8_epi16 and friends), so testing for plain __AVX__ turned a CPU
// with AVX but no AVX2 into a compile error instead of the scalar fallback.
#if defined(__AVX2__)
#define DAISY_SIMD_AVAILABLE 1
#include <immintrin.h>
#else
Expand Down
7 changes: 5 additions & 2 deletions pybinds/setup.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -721,7 +721,10 @@ PYBIND11_MODULE(_core, m)
.def_readwrite("flush_buffer_size", &daisy::HerculesConfig::flush_buffer_size)
.def_readwrite("index_dir", &daisy::HerculesConfig::index_dir);

pybind11::class_<daisy::Hercules, daisy::SimilaritySearchAlgorithm>(m, "Hercules", "Hercules hierarchical time series similarity index")
// Bound without SimilaritySearchAlgorithm: the base is abstract and never
// registered, and naming it here makes module init fail with "referenced
// unknown base type". Every other algorithm is bound the same way.
pybind11::class_<daisy::Hercules>(m, "Hercules", "Hercules hierarchical time series similarity index")
.def(pybind11::init<daisy::DistanceType>(), "Create a new Hercules with the given distance metric")
.def(pybind11::init<daisy::DistanceType, daisy::HerculesConfig>(), "Create a new Hercules with the given distance metric and configuration")
.def("setNumThreads", &daisy::Hercules::setNumThreads, "Set the number of query threads")
Expand Down Expand Up @@ -770,7 +773,7 @@ PYBIND11_MODULE(_core, m)
.def_readwrite("fill_lower", &daisy::DumpyOSConfig::fill_lower)
.def_readwrite("fill_upper", &daisy::DumpyOSConfig::fill_upper);

pybind11::class_<daisy::DumpyOS, daisy::SimilaritySearchAlgorithm>(m, "DumpyOS", "DumpyOS iSAX-based multi-ary adaptive time series similarity index")
pybind11::class_<daisy::DumpyOS>(m, "DumpyOS", "DumpyOS iSAX-based multi-ary adaptive time series similarity index")
.def(pybind11::init<daisy::DistanceType>(), "Create a new DumpyOS with the given distance metric")
.def(pybind11::init<daisy::DistanceType, daisy::DumpyOSConfig>(), "Create a new DumpyOS with the given distance metric and configuration")
.def("setNumThreads", &daisy::DumpyOS::setNumThreads, "Set the number of threads")
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ build-backend = "setuptools.build_meta"

[project]
name = "daisy-exact-search"
version = "1.0.8"
version = "1.0.9"
description = "DaiSy: A Library for Scalable Data Series Similarity Search"
readme = "README.md"
requires-python = ">=3.10"
Expand Down
Loading
Loading