├── .bumpversion.cfg ├── .github └── workflows │ └── ci.yml ├── .gitignore ├── .readthedocs.yml ├── HISTORY.rst ├── MANIFEST.in ├── Makefile ├── README.rst ├── docs ├── Makefile ├── conf.py ├── img │ └── logo.png ├── index.rst ├── installation.rst ├── make.bat ├── reference.rst ├── requirements.txt └── usage.rst ├── jakteristics ├── __about__.py ├── __init__.py ├── __main__.py ├── ckdtree │ ├── README.rst │ ├── __init__.py │ ├── _lib │ │ └── _c99compat.h │ ├── ckdtree.pxd │ ├── ckdtree.pyx │ └── ckdtree │ │ └── src │ │ ├── build.cxx │ │ ├── ckdtree_decl.h │ │ ├── coo_entries.h │ │ ├── count_neighbors.cxx │ │ ├── distance.h │ │ ├── distance_base.h │ │ ├── ordered_pair.h │ │ ├── partial_sort.h │ │ ├── query.cxx │ │ ├── query_ball_point.cxx │ │ ├── query_ball_tree.cxx │ │ ├── query_pairs.cxx │ │ ├── rectangle.h │ │ └── sparse_distances.cxx ├── constants.py ├── extension.pyx ├── las_utils.py ├── main.py ├── utils.pxd └── utils.pyx ├── pyproject.toml ├── requirements-dev.txt ├── requirements.txt ├── setup.py └── tests ├── conftest.py ├── data └── test_0.02_seconde.las ├── test_cli.py ├── test_features.py └── test_las_utils.py /.bumpversion.cfg: -------------------------------------------------------------------------------- 1 | [bumpversion] 2 | current_version = 0.6.2 3 | commit = True 4 | tag = True 5 | tag_name = {new_version} 6 | 7 | [bumpversion:file:jakteristics/__about__.py] 8 | 9 | [bumpversion:file:HISTORY.rst] 10 | search = History 11 | ------- 12 | 13 | Unreleased 14 | ---------- 15 | replace = History 16 | ------- 17 | 18 | Unreleased 19 | ---------- 20 | 21 | 22 | {new_version} ({now:%Y-%m-%d}) 23 | ------------------ 24 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: ["*"] 6 | tags: ["*"] 7 | pull_request: 8 | branches: ["*"] 9 | 10 | jobs: 11 | build_and_push: 12 | name: Build for ${{ matrix.os }} 13 | runs-on: ${{ matrix.os }} 14 | strategy: 15 | matrix: 16 | os: [ubuntu-22.04, windows-2022] 17 | 18 | steps: 19 | - uses: actions/checkout@v4 20 | 21 | - uses: actions/setup-python@v5 22 | 23 | - name: Install cibuildwheel 24 | run: python -m pip install --user cibuildwheel==2.18.1 25 | 26 | - name: Build wheels 27 | run: python -m cibuildwheel --output-dir wheelhouse 28 | env: 29 | CIBW_BEFORE_TEST: pip install -r requirements-dev.txt 30 | CIBW_TEST_COMMAND: pytest {project}/tests 31 | CIBW_BUILD: cp3{8,9,10,11,12}-{manylinux_x86_64,win_amd64} 32 | 33 | - uses: actions/upload-artifact@v4 34 | if: github.event_name == 'push' && startsWith(github.event.ref, 'refs/tags') 35 | with: 36 | name: wheels-${{ runner.os }} 37 | path: wheelhouse/*.whl 38 | 39 | publish: 40 | name: Publish to PyPI 41 | needs: build_and_push 42 | runs-on: ubuntu-20.04 43 | if: github.event_name == 'push' && startsWith(github.event.ref, 'refs/tags') 44 | steps: 45 | - uses: actions/checkout@v4 46 | 47 | - uses: actions/setup-python@v5 48 | 49 | - uses: actions/download-artifact@v4 50 | with: 51 | path: wheelhouse 52 | merge-multiple: true 53 | 54 | - name: Publish to PyPI 55 | env: 56 | TWINE_USERNAME: __token__ 57 | TWINE_PASSWORD: ${{ secrets.PYPI_TOKEN }} 58 | run: | 59 | pip install -r requirements.txt 60 | pip install twine 61 | python setup.py sdist 62 | twine upload dist/* 63 | twine upload wheelhouse/*.whl 64 | 65 | - name: Create GitHub Release 66 | uses: softprops/action-gh-release@v2 67 | with: 68 | files: | 69 | dist/* 70 | wheelhouse/*.whl 71 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /data 2 | run_main.py 3 | jakteristics/**/*.html 4 | jakteristics/**/*.c 5 | jakteristics/**/*.cpp 6 | 7 | .eggs 8 | 9 | # Byte-compiled / optimized / DLL files 10 | __pycache__/ 11 | *.py[cod] 12 | *$py.class 13 | nilearn_cache/ 14 | .pytest_cache/ 15 | .vscode/ 16 | .idea/ 17 | 18 | # C extensions 19 | *.so 20 | 21 | # Packages 22 | *.egg 23 | *.egg-info 24 | dist 25 | build 26 | eggs 27 | parts 28 | bin 29 | var 30 | sdist 31 | develop-eggs 32 | .installed.cfg 33 | lib 34 | lib64 35 | .cache 36 | 37 | # Installer logs 38 | pip-log.txt 39 | 40 | # Unit test / coverage reports 41 | .coverage 42 | .tox 43 | nosetests.xml 44 | htmlcov 45 | 46 | # Translations 47 | *.mo 48 | 49 | # Mr Developer 50 | .mr.developer.cfg 51 | .project 52 | .pydevproject 53 | 54 | # Complexity 55 | output/*.html 56 | output/*/index.html 57 | 58 | # Sphinx 59 | docs/_build 60 | -------------------------------------------------------------------------------- /.readthedocs.yml: -------------------------------------------------------------------------------- 1 | # .readthedocs.yml 2 | # Read the Docs configuration file 3 | # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details 4 | 5 | # Required 6 | version: 2 7 | 8 | # Build documentation in the docs/ directory with Sphinx 9 | sphinx: 10 | configuration: docs/conf.py 11 | 12 | # Optionally build your docs in additional formats such as PDF and ePub 13 | formats: all 14 | 15 | # Optionally set the version of Python and requirements required to build your docs 16 | python: 17 | version: 3.7 18 | install: 19 | - requirements: docs/requirements.txt 20 | - requirements: requirements.txt 21 | - method: pip 22 | path: . 23 | -------------------------------------------------------------------------------- /HISTORY.rst: -------------------------------------------------------------------------------- 1 | .. :changelog: 2 | 3 | History 4 | ------- 5 | 6 | Unreleased 7 | ---------- 8 | 9 | 10 | 0.6.2 (2024-07-22) 11 | ------------------ 12 | 13 | 14 | 0.6.1 (2024-06-04) 15 | ------------------ 16 | 17 | 18 | 0.6.0 (2023-04-20) 19 | ------------------ 20 | 21 | * add: number_of_neighbors feature 22 | * add: eigenvalues and eigenvectors features 23 | 24 | 25 | 0.5.1 (2023-04-11) 26 | ------------------ 27 | 28 | * fix: computing features when kdtree is not built from the same points for which we want to compute the features 29 | * drop python 3.6, add wheels for python 3.7-3.11 on linux and windows 30 | 31 | 0.5.0 (2022-01-26) 32 | ------------------ 33 | 34 | * fix: compatibility with latest laspy version (>= 2.1.1, (2.1.0 has a bug)) 35 | 36 | 37 | 0.4.3 (2020-09-24) 38 | ------------------ 39 | 40 | * the default value when features can't be computed should be NaN 41 | 42 | 43 | 0.4.2 (2020-04-20) 44 | ------------------ 45 | 46 | * fix extension import statement 47 | 48 | 49 | 0.4.1 (2020-04-17) 50 | ------------------ 51 | 52 | * fix: create parent directories for output file 53 | * fix: rename --num_threads to --num-threads 54 | * fix: require laspy 1.7 for upper case names in extra dimensions 55 | 56 | 57 | 0.4.0 (2020-04-16) 58 | ------------------ 59 | 60 | * first pypi release 61 | * add github actions 62 | 63 | 64 | 0.3.0 (2020-04-14) 65 | ------------------ 66 | 67 | * add feature-names parameter to compute specific features 68 | 69 | 70 | 0.2.0 (2020-04-10) 71 | ------------------ 72 | 73 | * fix windows compilation with openmp 74 | * add example cloudcompare script 75 | * add num_threads cli parameter and help documentation 76 | * write extra dimensions in the correct order 77 | 78 | 79 | 0.1.2 (2020-04-10) 80 | ------------------ 81 | 82 | * Fix tests 83 | 84 | 85 | 0.1.1 (2020-04-10) 86 | ------------------ 87 | 88 | * Fix bug where single precision was used for intermediate variables 89 | 90 | 91 | 0.1.0 (2020-04-10) 92 | ------------------ 93 | 94 | * First release 95 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include pyproject.toml 2 | include HISTORY.rst 3 | include README.rst 4 | include requirements.txt 5 | recursive-include jakteristics/ckdtree * 6 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: help build install install-dev test clean 2 | 3 | .DEFAULT: help 4 | help: 5 | @echo "make build" 6 | @echo " build extensions in place" 7 | @echo "make install" 8 | @echo " install in the current python environment" 9 | @echo "make install-dev" 10 | @echo " install in development mode with dev dependencies" 11 | @echo "make test" 12 | @echo " run tests" 13 | @echo "make clean" 14 | @echo " remove build artifacts and __pycache__" 15 | 16 | build: 17 | python setup.py build_ext -i 18 | 19 | install: 20 | pip install . 21 | 22 | install-dev: 23 | pip install -r requirements-dev.txt 24 | pip install -e . 25 | 26 | test: 27 | python setup.py pytest 28 | 29 | clean: 30 | rm -rf ./**/__pycache__ 31 | rm -rf build 32 | rm -rf *.egg-info 33 | find . -wholename "./jakteristics/*.cpp" -type f -delete 34 | find . -wholename "./jakteristics/*.c" -type f -delete 35 | find . -wholename "./jakteristics/*.html" -type f -delete 36 | find . -wholename "./jakteristics/*.so" -type f -delete 37 | find . -wholename "./jakteristics/*.pyd" -type f -delete 38 | 39 | tag: 40 | @if [ -z $(tag) ]; then\ 41 | echo "Please provide the 'tag' variable.";\ 42 | else\ 43 | git tag -a $(tag) -m "$(tag)";\ 44 | git push --delete upstream latest;\ 45 | git tag -d latest;\ 46 | git tag -a -m "latest" latest;\ 47 | fi 48 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | 2 | Jakteristics 3 | ~~~~~~~~~~~~ 4 | 5 | * **Documentation**: https://jakteristics.readthedocs.io 6 | * **Github**: https://github.com/jakarto3d/jakteristics 7 | 8 | Jakteristics is a python package to compute point cloud geometric features. 9 | 10 | A **geometric feature** is a description of the geometric shape around a point based on its 11 | neighborhood. For example, a point located on a wall will have a high *planarity*. 12 | 13 | The features used in this package are described in the paper 14 | `Contour detection in unstructured 3D point clouds`_. 15 | They are computed based on the eigenvalues and eigenvectors: 16 | 17 | * Eigenvalue sum 18 | * Omnivariance 19 | * Eigenentropy 20 | * Anisotropy 21 | * Planarity 22 | * Linearity 23 | * PCA1 24 | * PCA2 25 | * Surface Variation 26 | * Sphericity 27 | * Verticality 28 | * Nx, Ny, Nz (The normal vector) 29 | 30 | It's inspired from a similar tool in `CloudCompare `_. 31 | 32 | It's implemented in cython using the BLAS and LAPACK scipy wrappers. It can use multiple cpus, 33 | and the performance is quite good (at least twice as fast as CloudCompare). 34 | 35 | .. _`Contour detection in unstructured 3D point clouds`: https://ethz.ch/content/dam/ethz/special-interest/baug/igp/photogrammetry-remote-sensing-dam/documents/pdf/timo-jan-cvpr2016.pdf 36 | 37 | 38 | Installation 39 | ============ 40 | 41 | .. code:: bash 42 | 43 | python -m pip install jakteristics 44 | 45 | 46 | Usage 47 | ===== 48 | 49 | Refer to the `documentation `_ for more details. 50 | 51 | 52 | From python 53 | ----------- 54 | 55 | .. code:: python 56 | 57 | from jakteristics import compute_features 58 | 59 | features = compute_features(xyz, search_radius=0.15, feature_names=['planarity', 'linearity']) 60 | 61 | 62 | CLI 63 | --- 64 | 65 | Once the package is installed, you can use the `jakteristics` command: 66 | 67 | .. code:: bash 68 | 69 | jakteristics input/las/file.las output/file.las --search-radius 0.15 --num-threads 4 70 | 71 | 72 | Run tests 73 | ========= 74 | 75 | .. code:: bash 76 | 77 | python -m pip install -r requirements-dev.txt 78 | python setup.py pytest 79 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line, and also 5 | # from the environment for the first two. 6 | SPHINXOPTS ?= 7 | SPHINXBUILD ?= sphinx-build 8 | SOURCEDIR = . 9 | BUILDDIR = _build 10 | 11 | # Put it first so that "make" without argument is like "make help". 12 | help: 13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 14 | 15 | .PHONY: help Makefile 16 | 17 | # Catch-all target: route all unknown targets to Sphinx using the new 18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 19 | %: Makefile 20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 21 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # Configuration file for the Sphinx documentation builder. 2 | # 3 | # This file only contains a selection of the most common options. For a full 4 | # list see the documentation: 5 | # https://www.sphinx-doc.org/en/master/usage/configuration.html 6 | 7 | # -- Path setup -------------------------------------------------------------- 8 | 9 | # If extensions (or modules to document with autodoc) are in another directory, 10 | # add these directories to sys.path here. If the directory is relative to the 11 | # documentation root, use os.path.abspath to make it absolute, like shown here. 12 | # 13 | import os 14 | import sys 15 | 16 | try: 17 | import jakteristics 18 | except ImportError: 19 | # if package is not installed, add the parent path 20 | sys.path.insert(0, os.path.abspath("..")) 21 | 22 | 23 | # -- Project information ----------------------------------------------------- 24 | 25 | project = "jakteristics" 26 | copyright = "2020, David Caron" 27 | author = "David Caron" 28 | 29 | 30 | # -- General configuration --------------------------------------------------- 31 | 32 | master_doc = "index" 33 | 34 | # Add any Sphinx extension module names here, as strings. They can be 35 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 36 | # ones. 37 | extensions = [ 38 | "sphinx.ext.autodoc", 39 | "sphinx.ext.napoleon", 40 | "sphinx_autodoc_typehints", 41 | "sphinx_rtd_theme", 42 | "sphinx_click.ext", 43 | ] 44 | 45 | # Add any paths that contain templates here, relative to this directory. 46 | templates_path = ["_templates"] 47 | 48 | # List of patterns, relative to source directory, that match files and 49 | # directories to ignore when looking for source files. 50 | # This pattern also affects html_static_path and html_extra_path. 51 | exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"] 52 | 53 | autodoc_default_options = { 54 | "undoc-members": True, 55 | } 56 | 57 | # -- Options for HTML output ------------------------------------------------- 58 | 59 | # The theme to use for HTML and HTML Help pages. See the documentation for 60 | # a list of builtin themes. 61 | # 62 | html_theme = "sphinx_rtd_theme" 63 | html_logo = "img/logo.png" 64 | 65 | # Add any paths that contain custom static files (such as style sheets) here, 66 | # relative to this directory. They are copied after the builtin static files, 67 | # so a file named "default.css" will overwrite the builtin "default.css". 68 | html_static_path = ["_static"] 69 | -------------------------------------------------------------------------------- /docs/img/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jakarto3d/jakteristics/6a22e0072a42cf4b41a04a6e214fd7ef3df5ec67/docs/img/logo.png -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | Jakteristics 2 | ============ 3 | 4 | Jakteristics is a python package to compute point cloud geometric features. 5 | 6 | A **geometric feature** is a description of the geometric shape around a point based on its 7 | neighborhood. For example, a point located on a wall will have a high *planarity*. 8 | 9 | The features used in this package are described in the paper 10 | `Contour detection in unstructured 3D point clouds`_. 11 | They are based on the eigenvalues *λ1*, *λ2* and *λ3* and the eigenvectors *e1*, *e2* and *e3*. 12 | 13 | 14 | * Eigenvalue sum : :math:`λ1 + λ2 + λ3` 15 | * Omnivariance: :math:`(λ1 \cdot λ2 \cdot λ3) ^ {1 / 3}` 16 | * Eigenentropy: :math:`-∑_{i=1}^3 λi \cdot \ln(λi)` 17 | * Anisotropy: :math:`(λ1 − λ3)/λ1` 18 | * Planarity: :math:`(λ2−λ3)/λ1` 19 | * Linearity: :math:`(λ1−λ2)/λ1` 20 | * PCA1: :math:`λ1/(λ1 + λ2 + λ3)` 21 | * PCA2: :math:`λ2/(λ1 + λ2 + λ3)` 22 | * Surface Variation: :math:`λ3/(λ1+λ2+λ3)` 23 | * Sphericity: :math:`λ3/λ1` 24 | * Verticality: :math:`1-|e3[2]|` 25 | * Nx, Ny, Nz: The normal vector 26 | 27 | .. _`Contour detection in unstructured 3D point clouds`: https://ethz.ch/content/dam/ethz/special-interest/baug/igp/photogrammetry-remote-sensing-dam/documents/pdf/timo-jan-cvpr2016.pdf 28 | 29 | .. toctree:: 30 | :maxdepth: 2 31 | 32 | installation.rst 33 | usage.rst 34 | reference.rst 35 | -------------------------------------------------------------------------------- /docs/installation.rst: -------------------------------------------------------------------------------- 1 | Installation 2 | ============ 3 | 4 | 5 | Pypi 6 | ---- 7 | 8 | To install from pypi, run:: 9 | 10 | pip install jakteristics 11 | 12 | 13 | Building 14 | -------- 15 | 16 | To build the package from source, you need to have a compiler to build the cython extensions. 17 | 18 | On Windows, you need `Microsoft C++ Build Tools `_ 19 | 20 | Then, run:: 21 | 22 | python -m pip install . 23 | 24 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | pushd %~dp0 4 | 5 | REM Command file for Sphinx documentation 6 | 7 | if "%SPHINXBUILD%" == "" ( 8 | set SPHINXBUILD=sphinx-build 9 | ) 10 | set SOURCEDIR=. 11 | set BUILDDIR=_build 12 | 13 | if "%1" == "" goto help 14 | 15 | %SPHINXBUILD% >NUL 2>NUL 16 | if errorlevel 9009 ( 17 | echo. 18 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 19 | echo.installed, then set the SPHINXBUILD environment variable to point 20 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 21 | echo.may add the Sphinx directory to PATH. 22 | echo. 23 | echo.If you don't have Sphinx installed, grab it from 24 | echo.http://sphinx-doc.org/ 25 | exit /b 1 26 | ) 27 | 28 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 29 | goto end 30 | 31 | :help 32 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 33 | 34 | :end 35 | popd 36 | -------------------------------------------------------------------------------- /docs/reference.rst: -------------------------------------------------------------------------------- 1 | Reference 2 | ========= 3 | 4 | jakteristics 5 | ------------ 6 | 7 | .. autofunction:: jakteristics.compute_features 8 | 9 | jakteristics.las_utils 10 | ---------------------- 11 | 12 | .. autofunction:: jakteristics.las_utils.read_las_xyz 13 | 14 | .. autofunction:: jakteristics.las_utils.write_with_extra_dims 15 | -------------------------------------------------------------------------------- /docs/requirements.txt: -------------------------------------------------------------------------------- 1 | sphinx 2 | sphinx-autodoc-typehints 3 | sphinx-rtd-theme 4 | sphinx-click 5 | -------------------------------------------------------------------------------- /docs/usage.rst: -------------------------------------------------------------------------------- 1 | Usage 2 | ===== 3 | 4 | From python 5 | ----------- 6 | 7 | If your input is a las file, you can use the 8 | :py:func:`jakteristics.las_utils.read_las_xyz` function to read the xyz coordinates 9 | from your input file as a numpy array. 10 | 11 | If your input is already a numpy array, you can use the :py:func:`jakteristics.compute_features` 12 | directly. By default, every geometry feature will be computed. In a future version, 13 | the :py:arg:`feature_names` argument will be required. 14 | 15 | Once you have the features, you can use them like any numpy array, or write them 16 | to a las file using :py:func:`jakteristics.write_with_extra_dims`. 17 | 18 | Example: 19 | 20 | .. code-block:: python 21 | 22 | from jakteristics import las_utils, compute_features, FEATURE_NAMES 23 | 24 | input_path = "/path/to/a/las_file.las" 25 | xyz = las_utils.read_las_xyz(input_path) 26 | 27 | features = compute_features(xyz, search_radius=0.15, feature_names=FEATURE_NAMES) 28 | 29 | output_path = "/path/to/output_file.las" 30 | las_utils.write_with_extra_dims(input_path, output_path, features, FEATURE_NAMES) 31 | 32 | # or for a specific feature: 33 | omnivariance = compute_features(xyz, search_radius=0.15, feature_names=["omnivariance"]) 34 | output_omnivariance = "/path/to/output_omnivariance.las" 35 | las_utils.write_with_extra_dims(input_path, output_omnivariance, omnivariance, ["omnivariance"]) 36 | 37 | 38 | If you want to to all of these steps (read a las file, compute certain features 39 | and write it back to disk), you can use the command line: 40 | 41 | 42 | .. code:: bash 43 | 44 | jakteristics input/las/file.las output/file.las --search-radius 0.15 --num-threads 4 45 | 46 | 47 | CLI 48 | --- 49 | 50 | .. click:: jakteristics.__main__:click_command 51 | :prog: jakteristics 52 | -------------------------------------------------------------------------------- /jakteristics/__about__.py: -------------------------------------------------------------------------------- 1 | __author__ = "David Caron" 2 | __email__ = "david.caron@jakarto.com" 3 | __version__ = "0.6.2" 4 | -------------------------------------------------------------------------------- /jakteristics/__init__.py: -------------------------------------------------------------------------------- 1 | from .__about__ import __author__, __email__, __version__ 2 | 3 | from .main import compute_features 4 | from .constants import FEATURE_NAMES 5 | from .ckdtree.ckdtree import cKDTree 6 | -------------------------------------------------------------------------------- /jakteristics/__main__.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | from pathlib import Path 3 | from time import perf_counter 4 | from typing import List 5 | 6 | import typer 7 | from typer import Option, Argument 8 | 9 | from . import extension, las_utils 10 | from .constants import FEATURE_NAMES 11 | 12 | 13 | app = typer.Typer() 14 | 15 | 16 | def show_features_callback(value): 17 | if value: 18 | for name in FEATURE_NAMES: 19 | typer.echo(name) 20 | raise typer.Exit() 21 | 22 | 23 | @app.command() 24 | def typer_main( 25 | las_input: Path = Argument(...), 26 | output: Path = Argument(...), 27 | search_radius: float = Option( 28 | ..., 29 | "--search-radius", 30 | "-s", 31 | help="The search radius to use to query neighbors.", 32 | ), 33 | num_threads: int = Option( 34 | -1, 35 | "--num-threads", 36 | "-t", 37 | help=( 38 | "The number of threads to use for computation. " 39 | "Defaults to the number of cpu on the machine." 40 | ), 41 | ), 42 | feature_names: List[str] = Option( 43 | FEATURE_NAMES, 44 | "--feature", 45 | "-f", 46 | help=( 47 | "The feature names to compute. Repeat this parameter " 48 | "to compute multiple features. " 49 | "Use --show-features to see the list of possible choices. " 50 | "Default: All features." 51 | ), 52 | ), 53 | manhattan_distance: bool = Option( 54 | False, 55 | "--manhattan-distance", 56 | "-m", 57 | is_flag=True, 58 | help=( 59 | "How to compute the distance between 2 points. " 60 | "If provided, the sum-of-absolute-values is used ('Manhattan' distance)." 61 | "By default, the standard Euclidean distance is used. " 62 | ), 63 | ), 64 | eps: float = Option( 65 | 0, 66 | "--eps", 67 | "-e", 68 | show_default=True, 69 | help=( 70 | "Return approximate nearest neighbors; the k-th returned value " 71 | "is guaranteed to be no further than (1+eps) times the " 72 | "distance to the real k-th nearest neighbor." 73 | ), 74 | ), 75 | show_features: bool = typer.Option( 76 | None, 77 | "--show-features", 78 | help="Show a list of possible feature names and exit.", 79 | callback=show_features_callback, 80 | is_eager=True, 81 | ), 82 | ): 83 | t = perf_counter() 84 | typer.echo("Computing geometric features...") 85 | 86 | xyz = las_utils.read_las_xyz(las_input) 87 | feature_names_str = [] 88 | for name in feature_names: 89 | if name not in FEATURE_NAMES: 90 | choices = ", ".join(FEATURE_NAMES) 91 | raise typer.BadParameter(f"invalid choice: {name}. (choose from {choices})") 92 | feature_names_str.append(name) 93 | 94 | output.parent.mkdir(parents=True, exist_ok=True) 95 | 96 | features = extension.compute_features( 97 | xyz, 98 | search_radius, 99 | num_threads=num_threads, 100 | euclidean_distance=not manhattan_distance, 101 | eps=eps, 102 | feature_names=feature_names_str, 103 | ) 104 | 105 | las_utils.write_with_extra_dims(las_input, output, features, feature_names_str) 106 | typer.echo(f"Done in {perf_counter() - t:0.2f} seconds.") 107 | 108 | 109 | # used for documentation 110 | click_command = typer.main.get_command(app) 111 | 112 | if __name__ == "__main__": # pragma: no cover 113 | app() 114 | -------------------------------------------------------------------------------- /jakteristics/ckdtree/README.rst: -------------------------------------------------------------------------------- 1 | 2 | Vendorized from scipy at [scipy.spatial.cKDTree](https://github.com/scipy/scipy/tree/master/scipy/spatial) 3 | 4 | We had to do this, because there is no ckdtree.pxd file in scipy. 5 | -------------------------------------------------------------------------------- /jakteristics/ckdtree/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | ============================================================= 3 | Spatial algorithms and data structures (:mod:`scipy.spatial`) 4 | ============================================================= 5 | 6 | .. currentmodule:: scipy.spatial 7 | 8 | Spatial transformations 9 | ======================= 10 | 11 | These are contained in the `scipy.spatial.transform` submodule. 12 | 13 | Nearest-neighbor queries 14 | ======================== 15 | .. autosummary:: 16 | :toctree: generated/ 17 | 18 | KDTree -- class for efficient nearest-neighbor queries 19 | cKDTree -- class for efficient nearest-neighbor queries (faster implementation) 20 | Rectangle 21 | 22 | Distance metrics are contained in the :mod:`scipy.spatial.distance` submodule. 23 | 24 | Delaunay triangulation, convex hulls, and Voronoi diagrams 25 | ========================================================== 26 | 27 | .. autosummary:: 28 | :toctree: generated/ 29 | 30 | Delaunay -- compute Delaunay triangulation of input points 31 | ConvexHull -- compute a convex hull for input points 32 | Voronoi -- compute a Voronoi diagram hull from input points 33 | SphericalVoronoi -- compute a Voronoi diagram from input points on the surface of a sphere 34 | HalfspaceIntersection -- compute the intersection points of input halfspaces 35 | 36 | Plotting helpers 37 | ================ 38 | 39 | .. autosummary:: 40 | :toctree: generated/ 41 | 42 | delaunay_plot_2d -- plot 2-D triangulation 43 | convex_hull_plot_2d -- plot 2-D convex hull 44 | voronoi_plot_2d -- plot 2-D Voronoi diagram 45 | 46 | .. seealso:: :ref:`Tutorial ` 47 | 48 | 49 | Simplex representation 50 | ====================== 51 | The simplices (triangles, tetrahedra, etc.) appearing in the Delaunay 52 | tessellation (N-D simplices), convex hull facets, and Voronoi ridges 53 | (N-1-D simplices) are represented in the following scheme:: 54 | 55 | tess = Delaunay(points) 56 | hull = ConvexHull(points) 57 | voro = Voronoi(points) 58 | 59 | # coordinates of the jth vertex of the ith simplex 60 | tess.points[tess.simplices[i, j], :] # tessellation element 61 | hull.points[hull.simplices[i, j], :] # convex hull facet 62 | voro.vertices[voro.ridge_vertices[i, j], :] # ridge between Voronoi cells 63 | 64 | For Delaunay triangulations and convex hulls, the neighborhood 65 | structure of the simplices satisfies the condition: 66 | ``tess.neighbors[i,j]`` is the neighboring simplex of the ith 67 | simplex, opposite to the ``j``-vertex. It is -1 in case of no neighbor. 68 | 69 | Convex hull facets also define a hyperplane equation:: 70 | 71 | (hull.equations[i,:-1] * coord).sum() + hull.equations[i,-1] == 0 72 | 73 | Similar hyperplane equations for the Delaunay triangulation correspond 74 | to the convex hull facets on the corresponding N+1-D 75 | paraboloid. 76 | 77 | The Delaunay triangulation objects offer a method for locating the 78 | simplex containing a given point, and barycentric coordinate 79 | computations. 80 | 81 | Functions 82 | --------- 83 | 84 | .. autosummary:: 85 | :toctree: generated/ 86 | 87 | tsearch 88 | distance_matrix 89 | minkowski_distance 90 | minkowski_distance_p 91 | procrustes 92 | geometric_slerp 93 | 94 | """ 95 | 96 | # from .kdtree import * 97 | # from .ckdtree import cKDTree 98 | # from .qhull import * 99 | # from ._spherical_voronoi import SphericalVoronoi 100 | # from ._plotutils import * 101 | # from ._procrustes import procrustes 102 | # from ._geometric_slerp import geometric_slerp 103 | 104 | # __all__ = [s for s in dir() if not s.startswith('_')] 105 | # __all__ += ['distance', 'transform'] 106 | 107 | # from . import distance, transform 108 | 109 | # from scipy._lib._testutils import PytestTester 110 | # test = PytestTester(__name__) 111 | # del PytestTester 112 | -------------------------------------------------------------------------------- /jakteristics/ckdtree/_lib/_c99compat.h: -------------------------------------------------------------------------------- 1 | /* 2 | C99 compatibility for SciPy 3 | 4 | The Rules: 5 | - Every function definition must have compiler guard so that it is not defined twice. 6 | - This file should only affect compilers that do not support C99 natively. 7 | - All functions should be defined as "static" to limit linker conflicts. 8 | */ 9 | #ifndef SCIPY_C99_COMPAT 10 | #define SCIPY_C99_COMPAT 11 | #include 12 | 13 | #if defined(_MSC_VER) && _MSC_VER <= 1600 14 | /* MSVC 2008 and MSVC 2010 */ 15 | #ifndef isnan 16 | #define isnan(x) _isnan((x)) 17 | #endif 18 | 19 | #ifndef signbit 20 | static int signbit(double x) 21 | { 22 | return x > 0; 23 | } 24 | #endif 25 | 26 | #ifndef copysign 27 | static double copysign(double x, double y) 28 | { 29 | if (x >= 0) { 30 | if (y >= 0) { 31 | return x; 32 | } 33 | else { 34 | return -x; 35 | } 36 | } 37 | else { 38 | if (y >= 0) { 39 | return -x; 40 | } 41 | else { 42 | return x; 43 | } 44 | } 45 | } 46 | #endif 47 | 48 | #ifndef fmax 49 | static double fmax(double x, double y) 50 | { 51 | /* z > nan for z != nan is required by C the standard */ 52 | int xnan = isnan(x), ynan = isnan(y); 53 | 54 | if (xnan || ynan) 55 | { 56 | if (xnan && !ynan) 57 | return y; 58 | if (!xnan && ynan) 59 | return x; 60 | return x; 61 | } 62 | 63 | /* +0 > -0 is preferred by C the standard */ 64 | if (x == 0 && y == 0) 65 | { 66 | int xs = signbit(x), ys = signbit(y); 67 | if (xs && !ys) 68 | return y; 69 | if (!xs && ys) 70 | return x; 71 | return x; 72 | } 73 | 74 | if (x > y) { 75 | return x; 76 | } 77 | else { 78 | return y; 79 | } 80 | } 81 | #endif 82 | 83 | #ifndef fmin 84 | static double fmin(double x, double y) 85 | { 86 | /* z > nan for z != nan is required by C the standard */ 87 | int xnan = isnan(x), ynan = isnan(y); 88 | 89 | if (xnan || ynan) 90 | { 91 | if (xnan && !ynan) 92 | return y; 93 | if (!xnan && ynan) 94 | return x; 95 | return x; 96 | } 97 | 98 | /* +0 > -0 is preferred by C the standard */ 99 | if (x == 0 && y == 0) 100 | { 101 | int xs = signbit(x), ys = signbit(y); 102 | if (xs && !ys) 103 | return x; 104 | if (!xs && ys) 105 | return y; 106 | return y; 107 | } 108 | 109 | if (x > y) { 110 | return y; 111 | } 112 | else { 113 | return x; 114 | } 115 | } 116 | #endif 117 | #endif 118 | 119 | #if (__STDC_VERSION__ < 199901L) 120 | /* Hopefully fail in fewer cases */ 121 | 122 | /* For compilers which aren't MSVC and haven't defined isnan */ 123 | #ifndef isnan 124 | #define isnan(x) ((x) != (x)) 125 | #endif 126 | 127 | #ifndef isfinite 128 | #ifdef _MSC_VER 129 | /* MSVC 2015 and newer still don't have everything */ 130 | #define isfinite(x) _finite((x)) 131 | #else 132 | #define isfinite(x) !isnan((x) + (-x)) 133 | #endif 134 | #endif 135 | 136 | #ifndef isinf 137 | #define isinf(x) (!isfinite(x) && !isnan(x)) 138 | #endif 139 | 140 | #ifndef fma 141 | #define fma(x, y, z) ((x)*(y) + (z)) 142 | #endif 143 | #endif 144 | 145 | /* 146 | * portable isnan/isinf; in cmath only available in C++11 and npy_math 147 | * versions don't work well (they get undef'd by cmath, see gh-5689) 148 | * Implementation based on npy_math.h 149 | */ 150 | #ifndef sc_isnan 151 | #define sc_isnan isnan 152 | #endif 153 | #ifndef sc_isinf 154 | #define sc_isinf isinf 155 | #endif 156 | #ifndef sc_isfinite 157 | #define sc_isfinite isfinite 158 | #endif 159 | #ifndef sc_fma 160 | #define sc_fma fma 161 | #endif 162 | #endif 163 | -------------------------------------------------------------------------------- /jakteristics/ckdtree/ckdtree.pxd: -------------------------------------------------------------------------------- 1 | 2 | from libcpp.vector cimport vector 3 | cimport numpy as np 4 | 5 | cdef extern from "ckdtree_decl.h": 6 | struct ckdtreenode: 7 | np.intp_t split_dim 8 | np.intp_t children 9 | np.float64_t split 10 | np.intp_t start_idx 11 | np.intp_t end_idx 12 | ckdtreenode *less 13 | ckdtreenode *greater 14 | np.intp_t _less 15 | np.intp_t _greater 16 | 17 | struct ckdtree: 18 | vector[ckdtreenode] *tree_buffer 19 | ckdtreenode *ctree 20 | np.float64_t *raw_data 21 | np.intp_t n 22 | np.intp_t m 23 | np.intp_t leafsize 24 | np.float64_t *raw_maxes 25 | np.float64_t *raw_mins 26 | np.intp_t *raw_indices 27 | np.float64_t *raw_boxsize_data 28 | np.intp_t size 29 | 30 | int query_ball_point(const ckdtree *tree, 31 | const np.float64_t *x, 32 | const np.float64_t *r, 33 | const np.float64_t p, 34 | const np.float64_t eps, 35 | const np.intp_t n_queries, 36 | vector[np.intp_t] **results, 37 | const int return_length) nogil except + 38 | 39 | cdef class cKDTreeNode: 40 | cdef: 41 | readonly np.intp_t level 42 | readonly np.intp_t split_dim 43 | readonly np.intp_t children 44 | readonly np.float64_t split 45 | ckdtreenode *_node 46 | np.ndarray _data 47 | np.ndarray _indices 48 | cdef void _setup(cKDTreeNode self) 49 | 50 | cdef class cKDTree: 51 | cdef: 52 | ckdtree * cself 53 | readonly cKDTreeNode tree 54 | readonly np.ndarray data 55 | readonly np.ndarray maxes 56 | readonly np.ndarray mins 57 | readonly np.ndarray indices 58 | readonly object boxsize 59 | np.ndarray boxsize_data 60 | 61 | cdef _pre_init(cKDTree self) 62 | cdef _post_init(cKDTree self) 63 | cdef _post_init_traverse(cKDTree self, ckdtreenode *node) 64 | -------------------------------------------------------------------------------- /jakteristics/ckdtree/ckdtree/src/build.cxx: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | 13 | #include "ckdtree_decl.h" 14 | #include "ordered_pair.h" 15 | #include "partial_sort.h" 16 | 17 | #define tree_buffer_root(buf) (&(buf)[0][0]) 18 | 19 | static ckdtree_intp_t 20 | build(ckdtree *self, ckdtree_intp_t start_idx, intptr_t end_idx, 21 | double *maxes, double *mins, 22 | const int _median, const int _compact) 23 | { 24 | 25 | const ckdtree_intp_t m = self->m; 26 | const double *data = self->raw_data; 27 | ckdtree_intp_t *indices = (intptr_t *)(self->raw_indices); 28 | 29 | ckdtreenode new_node, *n, *root; 30 | ckdtree_intp_t node_index, _less, _greater; 31 | ckdtree_intp_t i, j, p, q, d; 32 | double size, split, minval, maxval; 33 | 34 | /* put a new node into the node stack */ 35 | self->tree_buffer->push_back(new_node); 36 | node_index = self->tree_buffer->size() - 1; 37 | root = tree_buffer_root(self->tree_buffer); 38 | n = root + node_index; 39 | memset(n, 0, sizeof(n[0])); 40 | 41 | n->start_idx = start_idx; 42 | n->end_idx = end_idx; 43 | n->children = end_idx - start_idx; 44 | 45 | if (end_idx-start_idx <= self->leafsize) { 46 | /* below brute force limit, return leafnode */ 47 | n->split_dim = -1; 48 | return node_index; 49 | } 50 | else { 51 | 52 | if (CKDTREE_LIKELY(_compact)) { 53 | /* Recompute hyperrectangle bounds. This should lead to a more 54 | * compact kd-tree but comes at the expense of larger construction 55 | * time. However, construction time is usually dwarfed by the 56 | * query time by orders of magnitude. 57 | */ 58 | const double *tmp_data_point; 59 | tmp_data_point = data + indices[start_idx] * m; 60 | for(i=0; i tmp ? maxes[i] : tmp; 69 | mins[i] = mins[i] < tmp ? mins[i] : tmp; 70 | } 71 | } 72 | } 73 | 74 | /* split on the dimension with largest spread */ 75 | d = 0; 76 | size = 0; 77 | for (i=0; i size) { 79 | d = i; 80 | size = maxes[i] - mins[i]; 81 | } 82 | } 83 | maxval = maxes[d]; 84 | minval = mins[d]; 85 | if (maxval == minval) { 86 | /* all points are identical; warn user? 87 | * return leafnode 88 | */ 89 | n->split_dim = -1; 90 | return node_index; 91 | } 92 | 93 | /* construct new inner node */ 94 | 95 | if (CKDTREE_LIKELY(_median)) { 96 | /* split on median to create a balanced tree 97 | * adopted from scikit-learn 98 | */ 99 | i = (end_idx - start_idx) / 2; 100 | partition_node_indices(data, indices + start_idx, d, i, m, 101 | end_idx - start_idx); 102 | p = start_idx + i; 103 | split = data[indices[p]*m+d]; 104 | } 105 | else { 106 | /* split with the sliding midpoint rule */ 107 | split = (maxval + minval) / 2; 108 | } 109 | 110 | p = start_idx; 111 | q = end_idx - 1; 112 | while (p <= q) { 113 | if (data[indices[p] * m + d] < split) 114 | ++p; 115 | else if (data[indices[q] * m + d] >= split) 116 | --q; 117 | else { 118 | ckdtree_intp_t t = indices[p]; 119 | indices[p] = indices[q]; 120 | indices[q] = t; 121 | ++p; 122 | --q; 123 | } 124 | } 125 | /* slide midpoint if necessary */ 126 | if (p == start_idx) { 127 | /* no points less than split */ 128 | j = start_idx; 129 | split = data[indices[j] * m + d]; 130 | for (i = start_idx+1; i < end_idx; ++i) { 131 | if (data[indices[i] * m + d] < split) { 132 | j = i; 133 | split = data[indices[j] * m + d]; 134 | } 135 | } 136 | ckdtree_intp_t t = indices[start_idx]; 137 | indices[start_idx] = indices[j]; 138 | indices[j] = t; 139 | p = start_idx + 1; 140 | q = start_idx; 141 | } 142 | else if (p == end_idx) { 143 | /* no points greater than split */ 144 | j = end_idx - 1; 145 | split = data[indices[j] * m + d]; 146 | for (i = start_idx; i < end_idx-1; ++i) { 147 | if (data[indices[i] * m + d] > split) { 148 | j = i; 149 | split = data[indices[j] * m + d]; 150 | } 151 | } 152 | ckdtree_intp_t t = indices[end_idx-1]; 153 | indices[end_idx-1] = indices[j]; 154 | indices[j] = t; 155 | p = end_idx - 1; 156 | q = end_idx - 2; 157 | } 158 | 159 | if (CKDTREE_LIKELY(_compact)) { 160 | _less = build(self, start_idx, p, maxes, mins, _median, _compact); 161 | _greater = build(self, p, end_idx, maxes, mins, _median, _compact); 162 | } 163 | else 164 | { 165 | std::vector tmp(m); 166 | double *mids = &tmp[0]; 167 | 168 | for (i=0; itree_buffer); 181 | n = root + node_index; 182 | /* fill in entries */ 183 | n->_less = _less; 184 | n->_greater = _greater; 185 | n->less = root + _less; 186 | n->greater = root + _greater; 187 | n->split_dim = d; 188 | n->split = split; 189 | 190 | return node_index; 191 | } 192 | } 193 | 194 | 195 | 196 | int build_ckdtree(ckdtree *self, ckdtree_intp_t start_idx, intptr_t end_idx, 197 | double *maxes, double *mins, int _median, int _compact) 198 | 199 | { 200 | build(self, start_idx, end_idx, maxes, mins, _median, _compact); 201 | return 0; 202 | } 203 | 204 | static double 205 | add_weights(ckdtree *self, 206 | double *node_weights, 207 | ckdtree_intp_t node_index, 208 | double *weights) 209 | { 210 | 211 | ckdtree_intp_t *indices = (intptr_t *)(self->raw_indices); 212 | 213 | ckdtreenode *n, *root; 214 | 215 | root = tree_buffer_root(self->tree_buffer); 216 | 217 | n = root + node_index; 218 | 219 | double sum = 0; 220 | 221 | if (n->split_dim != -1) { 222 | /* internal nodes; recursively calculate the total weight */ 223 | double left, right; 224 | left = add_weights(self, node_weights, n->_less, weights); 225 | right = add_weights(self, node_weights, n->_greater, weights); 226 | sum = left + right; 227 | } else { 228 | ckdtree_intp_t i; 229 | 230 | /* Leaf nodes */ 231 | for (i = n->start_idx; i < n->end_idx; ++i) { 232 | sum += weights[indices[i]]; 233 | } 234 | } 235 | 236 | node_weights[node_index] = sum; 237 | return sum; 238 | } 239 | 240 | int 241 | build_weights (ckdtree *self, double *node_weights, double *weights) 242 | { 243 | 244 | add_weights(self, node_weights, 0, weights); 245 | return 0; 246 | } 247 | 248 | -------------------------------------------------------------------------------- /jakteristics/ckdtree/ckdtree/src/ckdtree_decl.h: -------------------------------------------------------------------------------- 1 | #ifndef CKDTREE_CPP_DECL 2 | #define CKDTREE_CPP_DECL 3 | 4 | /* 5 | * Use numpy to provide some platform independency. 6 | * Define these functions for your platform 7 | * */ 8 | #include /* needed for isinf / sc_inf from c99compat under CLANG */ 9 | #include "_c99compat.h" 10 | #include 11 | #define CKDTREE_LIKELY(x) NPY_LIKELY(x) 12 | #define CKDTREE_UNLIKELY(x) NPY_UNLIKELY(x) 13 | #define CKDTREE_PREFETCH(x, rw, loc) NPY_PREFETCH(x, rw, loc) 14 | 15 | #define ckdtree_intp_t npy_intp 16 | #define ckdtree_isinf(x) sc_isinf(x) 17 | #define ckdtree_fmin(x, y) fmin(x, y) 18 | #define ckdtree_fmax(x, y) fmax(x, y) 19 | #define ckdtree_fabs(x) fabs(x) 20 | 21 | #include "ordered_pair.h" 22 | #include "coo_entries.h" 23 | 24 | struct ckdtreenode { 25 | ckdtree_intp_t split_dim; 26 | ckdtree_intp_t children; 27 | double split; 28 | ckdtree_intp_t start_idx; 29 | ckdtree_intp_t end_idx; 30 | ckdtreenode *less; 31 | ckdtreenode *greater; 32 | ckdtree_intp_t _less; 33 | ckdtree_intp_t _greater; 34 | }; 35 | 36 | struct ckdtree { 37 | // tree structure 38 | std::vector *tree_buffer; 39 | ckdtreenode *ctree; 40 | // meta data 41 | double *raw_data; 42 | ckdtree_intp_t n; 43 | ckdtree_intp_t m; 44 | ckdtree_intp_t leafsize; 45 | double *raw_maxes; 46 | double *raw_mins; 47 | ckdtree_intp_t *raw_indices; 48 | double *raw_boxsize_data; 49 | ckdtree_intp_t size; 50 | }; 51 | 52 | /* Build methods in C++ for better speed and GIL release */ 53 | 54 | int 55 | build_ckdtree(ckdtree *self, ckdtree_intp_t start_idx, intptr_t end_idx, 56 | double *maxes, double *mins, int _median, int _compact); 57 | 58 | int 59 | build_weights (ckdtree *self, double *node_weights, double *weights); 60 | 61 | /* Query methods in C++ for better speed and GIL release */ 62 | 63 | int 64 | query_knn(const ckdtree *self, 65 | double *dd, 66 | ckdtree_intp_t *ii, 67 | const double *xx, 68 | const ckdtree_intp_t n, 69 | const ckdtree_intp_t *k, 70 | const ckdtree_intp_t nk, 71 | const ckdtree_intp_t kmax, 72 | const double eps, 73 | const double p, 74 | const double distance_upper_bound); 75 | 76 | int 77 | query_pairs(const ckdtree *self, 78 | const double r, 79 | const double p, 80 | const double eps, 81 | std::vector *results); 82 | 83 | int 84 | count_neighbors_unweighted(const ckdtree *self, 85 | const ckdtree *other, 86 | ckdtree_intp_t n_queries, 87 | double *real_r, 88 | ckdtree_intp_t *results, 89 | const double p, 90 | int cumulative); 91 | 92 | int 93 | count_neighbors_weighted(const ckdtree *self, 94 | const ckdtree *other, 95 | double *self_weights, 96 | double *other_weights, 97 | double *self_node_weights, 98 | double *other_node_weights, 99 | ckdtree_intp_t n_queries, 100 | double *real_r, 101 | double *results, 102 | const double p, 103 | int cumulative); 104 | 105 | int 106 | query_ball_point(const ckdtree *self, 107 | const double *x, 108 | const double *r, 109 | const double p, 110 | const double eps, 111 | const ckdtree_intp_t n_queries, 112 | std::vector **results, 113 | const int return_length); 114 | 115 | int 116 | query_ball_tree(const ckdtree *self, 117 | const ckdtree *other, 118 | const double r, 119 | const double p, 120 | const double eps, 121 | std::vector **results 122 | ); 123 | 124 | int 125 | sparse_distance_matrix(const ckdtree *self, 126 | const ckdtree *other, 127 | const double p, 128 | const double max_distance, 129 | std::vector *results); 130 | 131 | 132 | #endif 133 | -------------------------------------------------------------------------------- /jakteristics/ckdtree/ckdtree/src/coo_entries.h: -------------------------------------------------------------------------------- 1 | #ifndef CKDTREE_COO_ENTRIES 2 | #define CKDTREE_COO_ENTRIES 3 | 4 | struct coo_entry { 5 | ckdtree_intp_t i; 6 | ckdtree_intp_t j; 7 | double v; 8 | }; 9 | 10 | #endif 11 | 12 | -------------------------------------------------------------------------------- /jakteristics/ckdtree/ckdtree/src/count_neighbors.cxx: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | 14 | #include "ckdtree_decl.h" 15 | #include "rectangle.h" 16 | 17 | struct WeightedTree { 18 | const ckdtree *tree; 19 | double *weights; 20 | double *node_weights; 21 | }; 22 | 23 | struct CNBParams 24 | { 25 | double *r; 26 | void * results; /* will be casted inside */ 27 | WeightedTree self, other; 28 | int cumulative; 29 | }; 30 | 31 | template static void 32 | traverse( 33 | RectRectDistanceTracker *tracker, 34 | const CNBParams *params, 35 | double *start, double *end, 36 | const ckdtreenode *node1, 37 | const ckdtreenode *node2) 38 | { 39 | static void (* const next)(RectRectDistanceTracker *tracker, 40 | const CNBParams *params, 41 | double *start, double *end, 42 | const ckdtreenode *node1, 43 | const ckdtreenode *node2) = traverse; 44 | 45 | ResultType *results = (ResultType*) params->results; 46 | 47 | /* 48 | * Speed through pairs of nodes all of whose children are close 49 | * and see if any work remains to be done 50 | */ 51 | 52 | double * new_start = std::lower_bound(start, end, tracker->min_distance); 53 | double * new_end = std::lower_bound(start, end, tracker->max_distance); 54 | 55 | 56 | /* since max_distance >= min_distance, end < start never happens */ 57 | if (params->cumulative) { 58 | double * i; 59 | if (new_end != end) { 60 | ResultType nn = WeightType::get_weight(¶ms->self, node1) 61 | * WeightType::get_weight(¶ms->other, node2); 62 | 63 | for (i = new_end; i < end; ++i) { 64 | results[i - params->r] += nn; 65 | } 66 | } 67 | /* any bins larger than end have been correctly counted, thus 68 | * thus we can truncate the queries in future of this branch of the traversal*/ 69 | start = new_start; 70 | end = new_end; 71 | } else { 72 | start = new_start; 73 | end = new_end; 74 | 75 | if (end == start) { 76 | ResultType nn = WeightType::get_weight(¶ms->self, node1) 77 | * WeightType::get_weight(¶ms->other, node2); 78 | results[start - params->r] += nn; 79 | } 80 | } 81 | 82 | if (end == start) { 83 | /* this pair falls into exactly one bin, no need to probe deeper. */ 84 | return; 85 | } 86 | 87 | /* OK, need to probe a bit deeper */ 88 | if (node1->split_dim == -1) { /* 1 is leaf node */ 89 | if (node2->split_dim == -1) { /* 1 & 2 are leaves */ 90 | ckdtree_intp_t i, j; 91 | const double p = tracker->p; 92 | const double tmd = tracker->max_distance; 93 | const double *sdata = params->self.tree->raw_data; 94 | const ckdtree_intp_t *sindices = params->self.tree->raw_indices; 95 | const double *odata = params->other.tree->raw_data; 96 | const ckdtree_intp_t *oindices = params->other.tree->raw_indices; 97 | const ckdtree_intp_t m = params->self.tree->m; 98 | const ckdtree_intp_t start1 = node1->start_idx; 99 | const ckdtree_intp_t start2 = node2->start_idx; 100 | const ckdtree_intp_t end1 = node1->end_idx; 101 | const ckdtree_intp_t end2 = node2->end_idx; 102 | 103 | CKDTREE_PREFETCH(sdata + sindices[start1] * m, 0, m); 104 | 105 | if (start1 < end1 - 1) 106 | CKDTREE_PREFETCH(sdata + sindices[start1+1] * m, 0, m); 107 | 108 | /* brute-force */ 109 | for (i = start1; i < end1; ++i) { 110 | 111 | if (i < end1 - 2) 112 | CKDTREE_PREFETCH(sdata + sindices[i+2] * m, 0, m); 113 | 114 | CKDTREE_PREFETCH(odata + oindices[start2] * m, 0, m); 115 | 116 | if (start2 < end2 - 1) 117 | CKDTREE_PREFETCH(odata + oindices[start2+1] * m, 0, m); 118 | 119 | for (j = start2; j < end2; ++j) { 120 | 121 | if (j < end2 - 2) 122 | CKDTREE_PREFETCH(odata + oindices[j+2] * m, 0, m); 123 | 124 | double d = MinMaxDist::point_point_p(params->self.tree, 125 | sdata + sindices[i] * m, 126 | odata + oindices[j] * m, 127 | p, m, tmd); 128 | 129 | if (params->cumulative) { 130 | /* 131 | * I think it's usually cheaper to test d against all 132 | * r's than to generate a distance array, sort it, then 133 | * search for all r's via binary search 134 | */ 135 | double * l; 136 | for (l = start; l < end; ++l) { 137 | if (d <= *l) { 138 | results[l - params->r] += WeightType::get_weight(¶ms->self, sindices[i]) 139 | * WeightType::get_weight(¶ms->other, sindices[j]); 140 | } 141 | } 142 | } else { 143 | const double *l = std::lower_bound(start, end, d); 144 | results[l - params->r] += WeightType::get_weight(¶ms->self, sindices[i]) 145 | * WeightType::get_weight(¶ms->other, sindices[j]); 146 | } 147 | } 148 | } 149 | } 150 | else { /* 1 is a leaf node, 2 is inner node */ 151 | tracker->push_less_of(2, node2); 152 | next(tracker, params, start, end, node1, node2->less); 153 | tracker->pop(); 154 | 155 | tracker->push_greater_of(2, node2); 156 | next(tracker, params, start, end, node1, node2->greater); 157 | tracker->pop(); 158 | } 159 | } 160 | else { /* 1 is an inner node */ 161 | if (node2->split_dim == -1) { 162 | /* 1 is an inner node, 2 is a leaf node */ 163 | tracker->push_less_of(1, node1); 164 | next(tracker, params, start, end, node1->less, node2); 165 | tracker->pop(); 166 | 167 | tracker->push_greater_of(1, node1); 168 | next(tracker, params, start, end, node1->greater, node2); 169 | tracker->pop(); 170 | } 171 | else { /* 1 and 2 are inner nodes */ 172 | tracker->push_less_of(1, node1); 173 | tracker->push_less_of(2, node2); 174 | next(tracker, params, start, end, node1->less, node2->less); 175 | tracker->pop(); 176 | 177 | tracker->push_greater_of(2, node2); 178 | next(tracker, params, start, end, node1->less, node2->greater); 179 | tracker->pop(); 180 | tracker->pop(); 181 | 182 | tracker->push_greater_of(1, node1); 183 | tracker->push_less_of(2, node2); 184 | next(tracker, params, start, end, node1->greater, node2->less); 185 | tracker->pop(); 186 | 187 | tracker->push_greater_of(2, node2); 188 | next(tracker, params, start, end, node1->greater, node2->greater); 189 | tracker->pop(); 190 | tracker->pop(); 191 | } 192 | } 193 | } 194 | 195 | template void 196 | count_neighbors(struct CNBParams *params, 197 | ckdtree_intp_t n_queries, const double p) 198 | { 199 | 200 | const ckdtree *self = params->self.tree; 201 | const ckdtree *other = params->other.tree; 202 | 203 | #define HANDLE(cond, kls) \ 204 | if (cond) { \ 205 | RectRectDistanceTracker tracker(self, r1, r2, p, 0.0, 0.0);\ 206 | traverse(&tracker, params, params->r, params->r+n_queries, \ 207 | self->ctree, other->ctree); \ 208 | } else 209 | 210 | Rectangle r1(self->m, self->raw_mins, self->raw_maxes); 211 | Rectangle r2(other->m, other->raw_mins, other->raw_maxes); 212 | 213 | if (CKDTREE_LIKELY(self->raw_boxsize_data == NULL)) { 214 | HANDLE(CKDTREE_LIKELY(p == 2), MinkowskiDistP2) 215 | HANDLE(p == 1, MinkowskiDistP1) 216 | HANDLE(ckdtree_isinf(p), MinkowskiDistPinf) 217 | HANDLE(1, MinkowskiDistPp) 218 | {} 219 | } else { 220 | HANDLE(CKDTREE_LIKELY(p == 2), BoxMinkowskiDistP2) 221 | HANDLE(p == 1, BoxMinkowskiDistP1) 222 | HANDLE(ckdtree_isinf(p), BoxMinkowskiDistPinf) 223 | HANDLE(1, BoxMinkowskiDistPp) 224 | {} 225 | } 226 | } 227 | 228 | struct Unweighted { 229 | /* the interface for accessing weights of unweighted data. */ 230 | static inline ckdtree_intp_t 231 | get_weight(const WeightedTree *wt, const ckdtreenode * node) 232 | { 233 | return node->children; 234 | } 235 | static inline ckdtree_intp_t 236 | get_weight(const WeightedTree *wt, const ckdtree_intp_t i) 237 | { 238 | return 1; 239 | } 240 | }; 241 | 242 | 243 | int 244 | count_neighbors_unweighted(const ckdtree *self, const ckdtree *other, 245 | ckdtree_intp_t n_queries, double *real_r, intptr_t *results, 246 | const double p, int cumulative) { 247 | 248 | CNBParams params = {0}; 249 | 250 | params.r = real_r; 251 | params.results = (void*) results; 252 | params.self.tree = self; 253 | params.other.tree = other; 254 | params.cumulative = cumulative; 255 | 256 | count_neighbors(¶ms, n_queries, p); 257 | 258 | return 0; 259 | } 260 | 261 | struct Weighted { 262 | /* the interface for accessing weights of weighted data. */ 263 | static inline double 264 | get_weight(const WeightedTree *wt, const ckdtreenode * node) 265 | { 266 | return (wt->weights != NULL) 267 | ? wt->node_weights[node - wt->tree->ctree] 268 | : node->children; 269 | } 270 | static inline double 271 | get_weight(const WeightedTree *wt, const ckdtree_intp_t i) 272 | { 273 | return (wt->weights != NULL)?wt->weights[i]:1; 274 | } 275 | }; 276 | 277 | int 278 | count_neighbors_weighted(const ckdtree *self, const ckdtree *other, 279 | double *self_weights, double *other_weights, 280 | double *self_node_weights, double *other_node_weights, 281 | ckdtree_intp_t n_queries, double *real_r, double *results, 282 | const double p, int cumulative) 283 | { 284 | 285 | CNBParams params = {0}; 286 | 287 | params.r = real_r; 288 | params.results = (void*) results; 289 | params.cumulative = cumulative; 290 | 291 | params.self.tree = self; 292 | params.other.tree = other; 293 | if (self_weights) { 294 | params.self.weights = self_weights; 295 | params.self.node_weights = self_node_weights; 296 | } 297 | if (other_weights) { 298 | params.other.weights = other_weights; 299 | params.other.node_weights = other_node_weights; 300 | } 301 | 302 | count_neighbors(¶ms, n_queries, p); 303 | 304 | return 0; 305 | } 306 | 307 | -------------------------------------------------------------------------------- /jakteristics/ckdtree/ckdtree/src/distance.h: -------------------------------------------------------------------------------- 1 | #include "distance_base.h" 2 | 3 | 4 | struct PlainDist1D { 5 | static inline const double side_distance_from_min_max( 6 | const ckdtree * tree, const double x, 7 | const double min, 8 | const double max, 9 | const ckdtree_intp_t k 10 | ) 11 | { 12 | double s, t; 13 | s = 0; 14 | t = x - max; 15 | if (t > s) { 16 | s = t; 17 | } else { 18 | t = min - x; 19 | if (t > s) s = t; 20 | } 21 | return s; 22 | } 23 | static inline void 24 | interval_interval(const ckdtree * tree, 25 | const Rectangle& rect1, const Rectangle& rect2, 26 | const ckdtree_intp_t k, 27 | double *min, double *max) 28 | { 29 | /* Compute the minimum/maximum distance along dimension k between points in 30 | * two hyperrectangles. 31 | */ 32 | *min = ckdtree_fmax(0., fmax(rect1.mins()[k] - rect2.maxes()[k], 33 | rect2.mins()[k] - rect1.maxes()[k])); 34 | *max = ckdtree_fmax(rect1.maxes()[k] - rect2.mins()[k], 35 | rect2.maxes()[k] - rect1.mins()[k]); 36 | } 37 | 38 | static inline double 39 | point_point(const ckdtree * tree, 40 | const double *x, const double *y, 41 | const ckdtree_intp_t k) { 42 | return ckdtree_fabs(x[k] - y[k]); 43 | } 44 | }; 45 | 46 | typedef BaseMinkowskiDistPp MinkowskiDistPp; 47 | typedef BaseMinkowskiDistPinf MinkowskiDistPinf; 48 | typedef BaseMinkowskiDistP1 MinkowskiDistP1; 49 | typedef BaseMinkowskiDistP2 NonOptimizedMinkowskiDistP2; 50 | 51 | /* 52 | * Measuring distances 53 | * =================== 54 | */ 55 | inline double 56 | sqeuclidean_distance_double(const double *u, const double *v, ckdtree_intp_t n) 57 | { 58 | double s; 59 | ckdtree_intp_t i; 60 | // manually unrolled loop, might be vectorized 61 | double acc[4] = {0., 0., 0., 0.}; 62 | for (i = 0; i < n/4; i += 4) { 63 | double _u[4] = {u[i], u[i + 1], u[i + 2], u[i + 3]}; 64 | double _v[4] = {v[i], v[i + 1], v[i + 2], v[i + 3]}; 65 | double diff[4] = {_u[0] - _v[0], 66 | _u[1] - _v[1], 67 | _u[2] - _v[2], 68 | _u[3] - _v[3]}; 69 | acc[0] += diff[0] * diff[0]; 70 | acc[1] += diff[1] * diff[1]; 71 | acc[2] += diff[2] * diff[2]; 72 | acc[3] += diff[3] * diff[3]; 73 | } 74 | s = acc[0] + acc[1] + acc[2] + acc[3]; 75 | if (i < n) { 76 | for(; i= 0) { 124 | /* do not pass though 0 */ 125 | min = ckdtree_fabs(min); 126 | max = ckdtree_fabs(max); 127 | if(min < max) { 128 | *realmin = min; 129 | *realmax = max; 130 | } else { 131 | *realmin = max; 132 | *realmax = min; 133 | } 134 | } else { 135 | min = ckdtree_fabs(min); 136 | max = ckdtree_fabs(max); 137 | *realmax = ckdtree_fmax(max, min); 138 | *realmin = 0; 139 | } 140 | /* done with non-periodic dimension */ 141 | return; 142 | } 143 | if(max <= 0 || min >= 0) { 144 | /* do not pass through 0 */ 145 | min = ckdtree_fabs(min); 146 | max = ckdtree_fabs(max); 147 | if(min > max) { 148 | double t = min; 149 | min = max; 150 | max = t; 151 | } 152 | if(max < half) { 153 | /* all below half*/ 154 | *realmin = min; 155 | *realmax = max; 156 | } else if(min > half) { 157 | /* all above half */ 158 | *realmax = full - min; 159 | *realmin = full - max; 160 | } else { 161 | /* min below, max above */ 162 | *realmax = half; 163 | *realmin = ckdtree_fmin(min, full - max); 164 | } 165 | } else { 166 | /* pass though 0 */ 167 | min = -min; 168 | if(min > max) max = min; 169 | if(max > half) max = half; 170 | *realmax = max; 171 | *realmin = 0; 172 | } 173 | } 174 | static inline void 175 | interval_interval(const ckdtree * tree, 176 | const Rectangle& rect1, const Rectangle& rect2, 177 | const ckdtree_intp_t k, 178 | double *min, double *max) 179 | { 180 | /* Compute the minimum/maximum distance along dimension k between points in 181 | * two hyperrectangles. 182 | */ 183 | _interval_interval_1d(rect1.mins()[k] - rect2.maxes()[k], 184 | rect1.maxes()[k] - rect2.mins()[k], min, max, 185 | tree->raw_boxsize_data[k], tree->raw_boxsize_data[k + rect1.m]); 186 | } 187 | 188 | static inline double 189 | point_point(const ckdtree * tree, 190 | const double *x, const double *y, 191 | const ckdtree_intp_t k) 192 | { 193 | double r1; 194 | r1 = wrap_distance(x[k] - y[k], tree->raw_boxsize_data[k + tree->m], tree->raw_boxsize_data[k]); 195 | r1 = ckdtree_fabs(r1); 196 | return r1; 197 | } 198 | 199 | static inline const double 200 | wrap_position(const double x, const double boxsize) 201 | { 202 | if (boxsize <= 0) return x; 203 | const double r = std::floor(x / boxsize); 204 | double x1 = x - r * boxsize; 205 | /* ensure result is within the box. */ 206 | while(x1 >= boxsize) x1 -= boxsize; 207 | while(x1 < 0) x1 += boxsize; 208 | return x1; 209 | } 210 | 211 | static inline const double side_distance_from_min_max( 212 | const ckdtree * tree, const double x, 213 | const double min, 214 | const double max, 215 | const ckdtree_intp_t k 216 | ) 217 | { 218 | double s, t, tmin, tmax; 219 | double fb = tree->raw_boxsize_data[k]; 220 | double hb = tree->raw_boxsize_data[k + tree->m]; 221 | 222 | if (fb <= 0) { 223 | /* non-periodic dimension */ 224 | s = PlainDist1D::side_distance_from_min_max(tree, x, min, max, k); 225 | return s; 226 | } 227 | 228 | /* periodic */ 229 | s = 0; 230 | tmax = x - max; 231 | tmin = x - min; 232 | /* is the test point in this range */ 233 | if(CKDTREE_LIKELY(tmax < 0 && tmin > 0)) { 234 | /* yes. min distance is 0 */ 235 | return 0; 236 | } 237 | 238 | /* no */ 239 | tmax = ckdtree_fabs(tmax); 240 | tmin = ckdtree_fabs(tmin); 241 | 242 | /* make tmin the closer edge */ 243 | if(tmin > tmax) { t = tmin; tmin = tmax; tmax = t; } 244 | 245 | /* both edges are less than half a box. */ 246 | /* no wrapping, use the closer edge */ 247 | if(tmax < hb) return tmin; 248 | 249 | /* both edge are more than half a box. */ 250 | /* wrapping on both edge, use the 251 | * wrapped further edge */ 252 | if(tmin > hb) return fb - tmax; 253 | 254 | /* the further side is wrapped */ 255 | tmax = fb - tmax; 256 | if(tmin > tmax) return tmax; 257 | return tmin; 258 | } 259 | 260 | private: 261 | static inline double 262 | wrap_distance(const double x, const double hb, const double fb) 263 | { 264 | double x1; 265 | if (CKDTREE_UNLIKELY(x < -hb)) x1 = fb + x; 266 | else if (CKDTREE_UNLIKELY(x > hb)) x1 = x - fb; 267 | else x1 = x; 268 | #if 0 269 | printf("ckdtree_fabs_b x : %g x1 %g\n", x, x1); 270 | #endif 271 | return x1; 272 | } 273 | 274 | 275 | }; 276 | 277 | 278 | typedef BaseMinkowskiDistPp BoxMinkowskiDistPp; 279 | typedef BaseMinkowskiDistPinf BoxMinkowskiDistPinf; 280 | typedef BaseMinkowskiDistP1 BoxMinkowskiDistP1; 281 | typedef BaseMinkowskiDistP2 BoxMinkowskiDistP2; 282 | 283 | -------------------------------------------------------------------------------- /jakteristics/ckdtree/ckdtree/src/distance_base.h: -------------------------------------------------------------------------------- 1 | template 2 | struct BaseMinkowskiDistPp { 3 | /* 1-d pieces 4 | * These should only be used if p != infinity 5 | */ 6 | 7 | static inline void 8 | interval_interval_p(const ckdtree * tree, 9 | const Rectangle& rect1, const Rectangle& rect2, 10 | const ckdtree_intp_t k, const double p, 11 | double *min, double *max) 12 | { 13 | /* Compute the minimum/maximum distance along dimension k between points in 14 | * two hyperrectangles. 15 | */ 16 | Dist1D::interval_interval(tree, rect1, rect2, k, min, max); 17 | *min = std::pow(*min, p); 18 | *max = std::pow(*max, p); 19 | } 20 | 21 | static inline void 22 | rect_rect_p(const ckdtree * tree, 23 | const Rectangle& rect1, const Rectangle& rect2, 24 | const double p, 25 | double *min, double *max) 26 | { 27 | *min = 0.; 28 | *max = 0.; 29 | for(ckdtree_intp_t i=0; iupperbound) 60 | return r; 61 | } 62 | return r; 63 | } 64 | 65 | static inline double 66 | distance_p(const double s, const double p) 67 | { 68 | return std::pow(s,p); 69 | } 70 | }; 71 | 72 | template 73 | struct BaseMinkowskiDistP1 : public BaseMinkowskiDistPp { 74 | 75 | static inline void 76 | interval_interval_p(const ckdtree * tree, 77 | const Rectangle& rect1, const Rectangle& rect2, 78 | const ckdtree_intp_t k, const double p, 79 | double *min, double *max) 80 | { 81 | /* Compute the minimum/maximum distance along dimension k between points in 82 | * two hyperrectangles. 83 | */ 84 | Dist1D::interval_interval(tree, rect1, rect2, k, min, max); 85 | } 86 | 87 | static inline void 88 | rect_rect_p(const ckdtree * tree, 89 | const Rectangle& rect1, const Rectangle& rect2, 90 | const double p, 91 | double *min, double *max) 92 | { 93 | *min = 0.; 94 | *max = 0.; 95 | for(ckdtree_intp_t i=0; iupperbound) 117 | return r; 118 | } 119 | return r; 120 | } 121 | 122 | static inline double 123 | distance_p(const double s, const double p) 124 | { 125 | return s; 126 | } 127 | }; 128 | 129 | template 130 | struct BaseMinkowskiDistPinf : public BaseMinkowskiDistPp { 131 | static inline void 132 | interval_interval_p(const ckdtree * tree, 133 | const Rectangle& rect1, const Rectangle& rect2, 134 | const ckdtree_intp_t k, double p, 135 | double *min, double *max) 136 | { 137 | return rect_rect_p(tree, rect1, rect2, p, min, max); 138 | } 139 | 140 | static inline void 141 | rect_rect_p(const ckdtree * tree, 142 | const Rectangle& rect1, const Rectangle& rect2, 143 | const double p, 144 | double *min, double *max) 145 | { 146 | *min = 0.; 147 | *max = 0.; 148 | for(ckdtree_intp_t i=0; iupperbound) 170 | return r; 171 | } 172 | return r; 173 | } 174 | static inline double 175 | distance_p(const double s, const double p) 176 | { 177 | return s; 178 | } 179 | }; 180 | 181 | template 182 | struct BaseMinkowskiDistP2 : public BaseMinkowskiDistPp { 183 | static inline void 184 | interval_interval_p(const ckdtree * tree, 185 | const Rectangle& rect1, const Rectangle& rect2, 186 | const ckdtree_intp_t k, const double p, 187 | double *min, double *max) 188 | { 189 | /* Compute the minimum/maximum distance along dimension k between points in 190 | * two hyperrectangles. 191 | */ 192 | Dist1D::interval_interval(tree, rect1, rect2, k, min, max); 193 | *min *= *min; 194 | *max *= *max; 195 | } 196 | 197 | static inline void 198 | rect_rect_p(const ckdtree * tree, 199 | const Rectangle& rect1, const Rectangle& rect2, 200 | const double p, 201 | double *min, double *max) 202 | { 203 | *min = 0.; 204 | *max = 0.; 205 | for(ckdtree_intp_t i=0; iupperbound) 230 | return r; 231 | } 232 | return r; 233 | } 234 | static inline double 235 | distance_p(const double s, const double p) 236 | { 237 | return s * s; 238 | } 239 | }; 240 | 241 | -------------------------------------------------------------------------------- /jakteristics/ckdtree/ckdtree/src/ordered_pair.h: -------------------------------------------------------------------------------- 1 | 2 | #ifndef CKDTREE_ORDERED_PAIR 3 | #define CKDTREE_ORDERED_PAIR 4 | 5 | struct ordered_pair { 6 | ckdtree_intp_t i; 7 | ckdtree_intp_t j; 8 | }; 9 | 10 | inline void 11 | add_ordered_pair(std::vector *results, 12 | const ckdtree_intp_t i, const intptr_t j) 13 | { 14 | if (i > j) { 15 | ordered_pair p = {j,i}; 16 | results->push_back(p); 17 | } 18 | else { 19 | ordered_pair p = {i,j}; 20 | results->push_back(p);; 21 | } 22 | } 23 | 24 | #endif 25 | 26 | 27 | -------------------------------------------------------------------------------- /jakteristics/ckdtree/ckdtree/src/partial_sort.h: -------------------------------------------------------------------------------- 1 | #ifndef CKDTREE_PARTIAL_SORT 2 | #define CKDTREE_PARTIAL_SORT 3 | 4 | /* Adapted version of the code originally 5 | * written by @jiefangxuanyan for scikit-learn. 6 | * 7 | */ 8 | 9 | #include "ckdtree_decl.h" 10 | #include 11 | 12 | struct IndexComparator { 13 | 14 | const double *data; 15 | ckdtree_intp_t split_dim; 16 | ckdtree_intp_t n_dims; 17 | 18 | IndexComparator(const double *data, 19 | ckdtree_intp_t split_dim, 20 | ckdtree_intp_t n_dims) : 21 | data(data), 22 | split_dim(split_dim), 23 | n_dims(n_dims) {}; 24 | 25 | inline bool operator()(ckdtree_intp_t a, ckdtree_intp_t b) { 26 | const double point_a = data[a * n_dims + split_dim]; 27 | const double point_b = data[b * n_dims + split_dim]; 28 | 29 | if CKDTREE_UNLIKELY (point_a == point_b) { 30 | return a < b; 31 | } else { 32 | return point_a < point_b; 33 | } 34 | } 35 | }; 36 | 37 | /* 38 | * Partition points in the node into two groups. 39 | * 40 | */ 41 | 42 | static int 43 | partition_node_indices(const double *data, 44 | ckdtree_intp_t *node_indices, 45 | ckdtree_intp_t split_dim, 46 | ckdtree_intp_t split_index, 47 | ckdtree_intp_t n_dims, 48 | ckdtree_intp_t n_points) { 49 | 50 | /* Upon return, the values in node_indices will be rearranged such that 51 | * (assuming numpy-style indexing): 52 | * 53 | * data[node_indices[0:split_index], split_dim] 54 | * <= data[node_indices[split_index], split_dim] 55 | * 56 | * and 57 | * 58 | * data[node_indices[split_index], split_dim] 59 | * <= data[node_indices[split_index:n_points], split_dim] 60 | * 61 | * This is eassentially a wrapper around the standard C++ function 62 | * ``std::nth_element``. 63 | * 64 | * 65 | * Parameters 66 | * ---------- 67 | * data : double pointer 68 | * Pointer to a 2D array of the training data, of shape [N, n_dims]. 69 | * N must be greater than any of the values in node_indices. 70 | * node_indices : int pointer 71 | * Pointer to a 1D array of length n_points. This lists the indices of 72 | * each of the points within the current node. This will be modified 73 | * in-place. 74 | * split_dim : int 75 | * the dimension on which to split. This will usually be computed via 76 | * the routine ``find_node_split_dim`` 77 | * split_index : int 78 | * the index within node_indices around which to split the points. 79 | * 80 | * Returns 81 | * ------- 82 | * status : int 83 | * integer exit status. On return, the contents of node_indices are 84 | * modified as noted above. 85 | */ 86 | 87 | IndexComparator index_comparator(data, split_dim, n_dims); 88 | 89 | std::nth_element(node_indices, 90 | node_indices + split_index, 91 | node_indices + n_points, 92 | index_comparator); 93 | 94 | return 0; 95 | } 96 | 97 | #endif 98 | -------------------------------------------------------------------------------- /jakteristics/ckdtree/ckdtree/src/query.cxx: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | 13 | #include "ckdtree_decl.h" 14 | #include "ordered_pair.h" 15 | #include "rectangle.h" 16 | 17 | /* 18 | * Priority queue 19 | * ============== 20 | */ 21 | 22 | union heapcontents { 23 | ckdtree_intp_t intdata; 24 | void *ptrdata; 25 | }; 26 | 27 | struct heapitem { 28 | double priority; 29 | heapcontents contents; 30 | }; 31 | 32 | struct heap { 33 | 34 | std::vector _heap; 35 | ckdtree_intp_t n; 36 | ckdtree_intp_t space; 37 | 38 | heap (ckdtree_intp_t initial_size) : _heap(initial_size) { 39 | space = initial_size; 40 | n = 0; 41 | } 42 | 43 | inline void push(heapitem &item) { 44 | ckdtree_intp_t i; 45 | heapitem t; 46 | n++; 47 | 48 | if (n > space) _heap.resize(2*space+1); 49 | space = _heap.size(); 50 | 51 | i = n-1; 52 | _heap[i] = item; 53 | while ((i > 0) && (_heap[i].priority < _heap[(i-1)/2].priority)) { 54 | t = _heap[(i-1)/2]; 55 | _heap[(i-1)/2] = _heap[i]; 56 | _heap[i] = t; 57 | i = (i-1)/2; 58 | } 59 | } 60 | 61 | inline heapitem peek() {return _heap[0];} 62 | 63 | inline void remove() { 64 | heapitem t; 65 | ckdtree_intp_t i, j, k, l, nn; 66 | _heap[0] = _heap[n-1]; 67 | n--; 68 | /* 69 | * No point in freeing up space as the heap empties. 70 | * The whole heap gets deallocated at the end of any 71 | * query below. Just keep the space to avoid unnecessary 72 | * reallocs. 73 | */ 74 | nn = n; 75 | i=0; 76 | j=1; 77 | k=2; 78 | while (((j _heap[j].priority)) || 79 | ((k _heap[k].priority))) { 80 | if ((k_heap[k].priority)) 81 | l = k; 82 | else 83 | l = j; 84 | t = _heap[l]; 85 | _heap[l] = _heap[i]; 86 | _heap[i] = t; 87 | i = l; 88 | j = 2*i+1; 89 | k = 2*i+2; 90 | } 91 | } 92 | 93 | inline heapitem pop() { 94 | heapitem it = _heap[0]; 95 | remove(); 96 | return it; 97 | } 98 | }; 99 | 100 | 101 | /* 102 | * nodeinfo 103 | * ======== 104 | */ 105 | 106 | struct nodeinfo { 107 | const ckdtreenode *node; 108 | ckdtree_intp_t m; 109 | double min_distance; /* full min distance */ 110 | double buf[1]; // the good old struct hack 111 | /* accessors to 'packed' attributes */ 112 | inline double * const side_distances() { 113 | /* min distance to the query per side; we 114 | * update this as the query is proceeded */ 115 | return buf; 116 | } 117 | inline double * const maxes() { 118 | return buf + m; 119 | } 120 | inline double * const mins() { 121 | return buf + 2 * m; 122 | } 123 | 124 | inline void init_box(const struct nodeinfo * from) { 125 | std::memcpy(buf, from->buf, sizeof(double) * (3 * m)); 126 | min_distance = from->min_distance; 127 | } 128 | 129 | inline void init_plain(const struct nodeinfo * from) { 130 | /* skip copying min and max, because we only need side_distance array in this case. */ 131 | std::memcpy(buf, from->buf, sizeof(double) * m); 132 | min_distance = from->min_distance; 133 | } 134 | 135 | inline void update_side_distance(const int d, const double new_side_distance, const double p) { 136 | if (CKDTREE_UNLIKELY(ckdtree_isinf(p))) { 137 | min_distance = ckdtree_fmax(min_distance, new_side_distance); 138 | } else { 139 | min_distance += new_side_distance - side_distances()[d]; 140 | } 141 | side_distances()[d] = new_side_distance; 142 | } 143 | }; 144 | 145 | /* 146 | * Memory pool for nodeinfo structs 147 | * ================================ 148 | */ 149 | 150 | struct nodeinfo_pool { 151 | 152 | std::vector pool; 153 | 154 | ckdtree_intp_t alloc_size; 155 | ckdtree_intp_t arena_size; 156 | ckdtree_intp_t m; 157 | char *arena; 158 | char *arena_ptr; 159 | 160 | nodeinfo_pool(ckdtree_intp_t m) { 161 | alloc_size = sizeof(nodeinfo) + (3 * m -1)*sizeof(double); 162 | alloc_size = 64*(alloc_size/64)+64; 163 | arena_size = 4096*((64*alloc_size)/4096)+4096; 164 | arena = new char[arena_size]; 165 | arena_ptr = arena; 166 | pool.push_back(arena); 167 | this->m = m; 168 | } 169 | 170 | ~nodeinfo_pool() { 171 | for (ckdtree_intp_t i = pool.size()-1; i >= 0; --i) 172 | delete [] pool[i]; 173 | } 174 | 175 | inline nodeinfo *allocate() { 176 | nodeinfo *ni1; 177 | ckdtree_intp_t m1 = (ckdtree_intp_t)arena_ptr; 178 | ckdtree_intp_t m0 = (ckdtree_intp_t)arena; 179 | if ((arena_size-(ckdtree_intp_t)(m1-m0))m = m; 186 | arena_ptr += alloc_size; 187 | return ni1; 188 | } 189 | }; 190 | 191 | /* k-nearest neighbor search for a single point x */ 192 | template 193 | static void 194 | query_single_point(const ckdtree *self, 195 | double *result_distances, 196 | ckdtree_intp_t *result_indices, 197 | const double *x, 198 | const ckdtree_intp_t *k, 199 | const ckdtree_intp_t nk, 200 | const ckdtree_intp_t kmax, 201 | const double eps, 202 | const double p, 203 | double distance_upper_bound) 204 | { 205 | static double inf = strtod("INF", NULL); 206 | 207 | /* memory pool to allocate and automatically reclaim nodeinfo structs */ 208 | nodeinfo_pool nipool(self->m); 209 | 210 | /* 211 | * priority queue for chasing nodes 212 | * entries are: 213 | * - minimum distance between the cell and the target 214 | * - distances between the nearest side of the cell and the target 215 | * the head node of the cell 216 | */ 217 | heap q(12); 218 | 219 | /* 220 | * priority queue for chasing nodes 221 | * entries are: 222 | * - minimum distance between the cell and the target 223 | * - distances between the nearest side of the cell and the target 224 | * the head node of the cell 225 | */ 226 | heap neighbors(kmax); 227 | 228 | ckdtree_intp_t i; 229 | const ckdtree_intp_t m = self->m; 230 | nodeinfo *ni1; 231 | nodeinfo *ni2; 232 | double d; 233 | double epsfac; 234 | heapitem it, it2, neighbor; 235 | const ckdtreenode *node; 236 | const ckdtreenode *inode; 237 | 238 | /* set up first nodeifo */ 239 | ni1 = nipool.allocate(); 240 | ni1->node = self->ctree; 241 | 242 | /* initialize first node, update min_distance */ 243 | ni1->min_distance = 0; 244 | 245 | for (i=0; imins()[i] = self->raw_mins[i]; 247 | ni1->maxes()[i] = self->raw_maxes[i]; 248 | 249 | double side_distance; 250 | if(self->raw_boxsize_data != NULL) { 251 | side_distance = BoxDist1D::side_distance_from_min_max( 252 | self, x[i], self->raw_mins[i], self->raw_maxes[i], i); 253 | } else { 254 | side_distance = PlainDist1D::side_distance_from_min_max( 255 | self, x[i], self->raw_mins[i], self->raw_maxes[i], i); 256 | } 257 | side_distance = MinMaxDist::distance_p(side_distance, p); 258 | 259 | ni1->side_distances()[i] = 0; 260 | ni1->update_side_distance(i, side_distance, p); 261 | } 262 | 263 | /* fiddle approximation factor */ 264 | if (CKDTREE_LIKELY(p == 2.0)) { 265 | double tmp = 1. + eps; 266 | epsfac = 1. / (tmp*tmp); 267 | } 268 | else if (eps == 0.) 269 | epsfac = 1.; 270 | else if (ckdtree_isinf(p)) 271 | epsfac = 1. / (1. + eps); 272 | else 273 | epsfac = 1. / std::pow((1. + eps), p); 274 | 275 | /* internally we represent all distances as distance**p */ 276 | if (CKDTREE_LIKELY(p == 2.0)) { 277 | double tmp = distance_upper_bound; 278 | distance_upper_bound = tmp*tmp; 279 | } 280 | else if ((!ckdtree_isinf(p)) && (!isinf(distance_upper_bound))) 281 | distance_upper_bound = std::pow(distance_upper_bound,p); 282 | 283 | for(;;) { 284 | if (ni1->node->split_dim == -1) { 285 | 286 | node = ni1->node; 287 | 288 | /* brute-force */ 289 | { 290 | const ckdtree_intp_t start_idx = node->start_idx; 291 | const ckdtree_intp_t end_idx = node->end_idx; 292 | const double *data = self->raw_data; 293 | const ckdtree_intp_t *indices = self->raw_indices; 294 | 295 | CKDTREE_PREFETCH(data+indices[start_idx]*m, 0, m); 296 | if (start_idx < end_idx - 1) 297 | CKDTREE_PREFETCH(data+indices[start_idx+1]*m, 0, m); 298 | 299 | for (i=start_idx; inode; 332 | const ckdtree_intp_t split_dim = inode->split_dim; 333 | const double split = inode->split; 334 | 335 | /* 336 | * we don't push cells that are too far onto the queue at all, 337 | * but since the distance_upper_bound decreases, we might get 338 | * here even if the cell's too far 339 | */ 340 | if (ni1->min_distance > distance_upper_bound*epsfac) { 341 | /* since this is the nearest cell, we're done, bail out */ 342 | break; 343 | } 344 | // set up children for searching 345 | // ni2 will be pushed to the queue 346 | 347 | ni2 = nipool.allocate(); 348 | 349 | if (CKDTREE_LIKELY(self->raw_boxsize_data == NULL)) { 350 | /* 351 | * non periodic : the 'near' node is know from the 352 | * relative distance to the split, and 353 | * has the same distance as the parent node. 354 | * 355 | * we set ni1 to 'near', and set ni2 to 'far'. 356 | * we only recalculate the distance of 'far' later. 357 | * 358 | * This code branch doesn't use min and max. 359 | */ 360 | ni2->init_plain(ni1); 361 | 362 | double side_distance; 363 | 364 | if (x[split_dim] < split) { 365 | ni1->node = inode->less; 366 | ni2->node = inode->greater; 367 | side_distance = split - x[split_dim]; 368 | } else { 369 | ni1->node = inode->greater; 370 | ni2->node = inode->less; 371 | side_distance = x[split_dim] - split; 372 | } 373 | 374 | side_distance = MinMaxDist::distance_p(side_distance, p); 375 | 376 | ni2->update_side_distance(split_dim, side_distance, p); 377 | } else { 378 | /* 379 | * for periodic queries, we do not know which node is closer. 380 | * thus re-calculate ni1 and ni2 both. 381 | * 382 | * this branch we need to keep track of mins and maxes; 383 | */ 384 | ni2->init_box(ni1); 385 | 386 | double side_distance; 387 | 388 | ni1->maxes()[split_dim] = split; 389 | ni1->node = inode->less; 390 | 391 | side_distance = BoxDist1D::side_distance_from_min_max( 392 | self, 393 | x[split_dim], 394 | ni1->mins()[split_dim], 395 | ni1->maxes()[split_dim], split_dim); 396 | side_distance = MinMaxDist::distance_p(side_distance, p); 397 | 398 | ni1->update_side_distance(split_dim, side_distance, p); 399 | 400 | ni2->mins()[split_dim] = split; 401 | ni2->node = inode->greater; 402 | 403 | side_distance = BoxDist1D::side_distance_from_min_max( 404 | self, 405 | x[split_dim], 406 | ni2->mins()[split_dim], 407 | ni2->maxes()[split_dim], split_dim); 408 | side_distance = MinMaxDist::distance_p(side_distance, p); 409 | 410 | ni2->update_side_distance(split_dim, side_distance, p); 411 | } 412 | 413 | /* Ensure ni1 is closer than ni2 */ 414 | if (ni1->min_distance > ni2->min_distance) { 415 | { 416 | nodeinfo *tmp; 417 | tmp = ni1; 418 | ni1 = ni2; 419 | ni2 = tmp; 420 | } 421 | } 422 | 423 | /* 424 | * near child is at the same or closer than the distance as the current node 425 | * we're going here next, so no point pushing it on the queue 426 | * no need to recompute the distance or the side_distances 427 | */ 428 | 429 | /* 430 | * far child can be further 431 | * push it on the queue if it's near enough 432 | */ 433 | 434 | if (ni2->min_distance<=distance_upper_bound*epsfac) { 435 | it2.priority = ni2->min_distance; 436 | it2.contents.ptrdata = (void*) ni2; 437 | q.push(it2); 438 | } 439 | 440 | } 441 | } 442 | 443 | /* heapsort */ 444 | std::vector sorted_neighbors(kmax); 445 | ckdtree_intp_t nnb = neighbors.n; 446 | for(i = neighbors.n - 1; i >=0; --i) { 447 | sorted_neighbors[i] = neighbors.pop(); 448 | } 449 | 450 | /* fill output arrays with sorted neighbors */ 451 | for (i = 0; i < nk; ++i) { 452 | if(CKDTREE_UNLIKELY(k[i] - 1 >= nnb)) { 453 | result_indices[i] = self->n; 454 | result_distances[i] = inf; 455 | } else { 456 | neighbor = sorted_neighbors[k[i] - 1]; 457 | result_indices[i] = neighbor.contents.intdata; 458 | if (CKDTREE_LIKELY(p == 2.0)) 459 | result_distances[i] = std::sqrt(-neighbor.priority); 460 | else if ((p == 1.) || (ckdtree_isinf(p))) 461 | result_distances[i] = -neighbor.priority; 462 | else 463 | result_distances[i] = std::pow((-neighbor.priority),(1./p)); 464 | } 465 | } 466 | } 467 | 468 | /* Query n points for their k nearest neighbors */ 469 | 470 | int 471 | query_knn(const ckdtree *self, 472 | double *dd, 473 | ckdtree_intp_t *ii, 474 | const double *xx, 475 | const ckdtree_intp_t n, 476 | const ckdtree_intp_t* k, 477 | const ckdtree_intp_t nk, 478 | const ckdtree_intp_t kmax, 479 | const double eps, 480 | const double p, 481 | const double distance_upper_bound) 482 | { 483 | #define HANDLE(cond, kls) \ 484 | if(cond) { \ 485 | query_single_point(self, dd_row, ii_row, xx_row, k, nk, kmax, eps, p, distance_upper_bound); \ 486 | } else 487 | 488 | ckdtree_intp_t m = self->m; 489 | ckdtree_intp_t i; 490 | 491 | if(CKDTREE_LIKELY(!self->raw_boxsize_data)) { 492 | for (i=0; i row(m); 504 | double * xx_row = &row[0]; 505 | int j; 506 | for (i=0; iraw_boxsize_data[j]); 512 | } 513 | HANDLE(CKDTREE_LIKELY(p == 2), BoxMinkowskiDistP2) 514 | HANDLE(p == 1, BoxMinkowskiDistP1) 515 | HANDLE(ckdtree_isinf(p), BoxMinkowskiDistPinf) 516 | HANDLE(1, BoxMinkowskiDistPp) {} 517 | } 518 | 519 | } 520 | return 0; 521 | } 522 | 523 | -------------------------------------------------------------------------------- /jakteristics/ckdtree/ckdtree/src/query_ball_point.cxx: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | 13 | #include "ckdtree_decl.h" 14 | #include "rectangle.h" 15 | 16 | 17 | static void 18 | traverse_no_checking(const ckdtree *self, 19 | const int return_length, 20 | std::vector *results, 21 | const ckdtreenode *node) 22 | { 23 | const ckdtree_intp_t *indices = self->raw_indices; 24 | const ckdtreenode *lnode; 25 | ckdtree_intp_t i; 26 | 27 | if (node->split_dim == -1) { /* leaf node */ 28 | lnode = node; 29 | const ckdtree_intp_t start = lnode->start_idx; 30 | const ckdtree_intp_t end = lnode->end_idx; 31 | for (i = start; i < end; ++i) { 32 | if (return_length) { 33 | (*results)[0] ++; 34 | } else { 35 | results->push_back(indices[i]); 36 | } 37 | } 38 | } 39 | else { 40 | traverse_no_checking(self, return_length, results, node->less); 41 | traverse_no_checking(self, return_length, results, node->greater); 42 | } 43 | } 44 | 45 | 46 | template static void 47 | traverse_checking(const ckdtree *self, 48 | const int return_length, 49 | std::vector *results, 50 | const ckdtreenode *node, 51 | RectRectDistanceTracker *tracker 52 | ) 53 | { 54 | const ckdtreenode *lnode; 55 | double d; 56 | ckdtree_intp_t i; 57 | 58 | if (tracker->min_distance > tracker->upper_bound * tracker->epsfac) { 59 | return; 60 | } 61 | else if (tracker->max_distance < tracker->upper_bound / tracker->epsfac) { 62 | traverse_no_checking(self, return_length, results, node); 63 | } 64 | else if (node->split_dim == -1) { /* leaf node */ 65 | 66 | /* brute-force */ 67 | lnode = node; 68 | const double p = tracker->p; 69 | const double tub = tracker->upper_bound; 70 | const double *tpt = tracker->rect1.mins(); 71 | const double *data = self->raw_data; 72 | const ckdtree_intp_t *indices = self->raw_indices; 73 | const ckdtree_intp_t m = self->m; 74 | const ckdtree_intp_t start = lnode->start_idx; 75 | const ckdtree_intp_t end = lnode->end_idx; 76 | 77 | CKDTREE_PREFETCH(data + indices[start] * m, 0, m); 78 | if (start < end - 1) 79 | CKDTREE_PREFETCH(data + indices[start+1] * m, 0, m); 80 | 81 | for (i = start; i < end; ++i) { 82 | 83 | if (i < end -2 ) 84 | CKDTREE_PREFETCH(data + indices[i+2] * m, 0, m); 85 | 86 | d = MinMaxDist::point_point_p(self, data + indices[i] * m, tpt, p, m, tub); 87 | 88 | if (d <= tub) { 89 | if(return_length) { 90 | (*results)[0] ++; 91 | } else { 92 | results->push_back((ckdtree_intp_t) indices[i]); 93 | } 94 | } 95 | } 96 | } 97 | else { 98 | tracker->push_less_of(2, node); 99 | traverse_checking(self, return_length, results, node->less, tracker); 100 | tracker->pop(); 101 | 102 | tracker->push_greater_of(2, node); 103 | traverse_checking(self, return_length, results, node->greater, tracker); 104 | tracker->pop(); 105 | } 106 | } 107 | 108 | int 109 | query_ball_point(const ckdtree *self, const double *x, 110 | const double *r, const double p, const double eps, 111 | const ckdtree_intp_t n_queries, 112 | std::vector **results, const int return_length) 113 | { 114 | #define HANDLE(cond, kls) \ 115 | if(cond) { \ 116 | if(return_length) results[i]->push_back(0); \ 117 | RectRectDistanceTracker tracker(self, point, rect, p, eps, r[i]); \ 118 | traverse_checking(self, return_length, results[i], self->ctree, &tracker); \ 119 | } else 120 | 121 | for (ckdtree_intp_t i=0; i < n_queries; ++i) { 122 | const ckdtree_intp_t m = self->m; 123 | Rectangle rect(m, self->raw_mins, self->raw_maxes); 124 | if (CKDTREE_LIKELY(self->raw_boxsize_data == NULL)) { 125 | Rectangle point(m, x + i * m, x + i * m); 126 | HANDLE(CKDTREE_LIKELY(p == 2), MinkowskiDistP2) 127 | HANDLE(p == 1, MinkowskiDistP1) 128 | HANDLE(ckdtree_isinf(p), MinkowskiDistPinf) 129 | HANDLE(1, MinkowskiDistPp) 130 | {} 131 | } else { 132 | Rectangle point(m, x + i * m, x + i * m); 133 | int j; 134 | for(j=0; jraw_boxsize_data[j]); 136 | } 137 | HANDLE(CKDTREE_LIKELY(p == 2), BoxMinkowskiDistP2) 138 | HANDLE(p == 1, BoxMinkowskiDistP1) 139 | HANDLE(ckdtree_isinf(p), BoxMinkowskiDistPinf) 140 | HANDLE(1, BoxMinkowskiDistPp) 141 | {} 142 | } 143 | } 144 | return 0; 145 | } 146 | -------------------------------------------------------------------------------- /jakteristics/ckdtree/ckdtree/src/query_ball_tree.cxx: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | 13 | #include "ckdtree_decl.h" 14 | #include "rectangle.h" 15 | 16 | 17 | static void 18 | traverse_no_checking(const ckdtree *self, const ckdtree *other, 19 | std::vector **results, 20 | const ckdtreenode *node1, const ckdtreenode *node2) 21 | { 22 | const ckdtreenode *lnode1; 23 | const ckdtreenode *lnode2; 24 | const ckdtree_intp_t *sindices = self->raw_indices; 25 | const ckdtree_intp_t *oindices = other->raw_indices; 26 | std::vector *results_i; 27 | ckdtree_intp_t i, j; 28 | 29 | if (node1->split_dim == -1) { /* leaf node */ 30 | lnode1 = node1; 31 | 32 | if (node2->split_dim == -1) { /* leaf node */ 33 | lnode2 = node2; 34 | 35 | const ckdtree_intp_t start1 = lnode1->start_idx; 36 | const ckdtree_intp_t start2 = lnode2->start_idx; 37 | const ckdtree_intp_t end1 = lnode1->end_idx; 38 | const ckdtree_intp_t end2 = lnode2->end_idx; 39 | 40 | for (i = start1; i < end1; ++i) { 41 | results_i = results[sindices[i]]; 42 | for (j = start2; j < end2; ++j) 43 | results_i->push_back(oindices[j]); 44 | } 45 | } 46 | else { 47 | traverse_no_checking(self, other, results, node1, node2->less); 48 | traverse_no_checking(self, other, results, node1, node2->greater); 49 | } 50 | } 51 | else { 52 | traverse_no_checking(self, other, results, node1->less, node2); 53 | traverse_no_checking(self, other, results, node1->greater, node2); 54 | } 55 | } 56 | 57 | 58 | template static void 59 | traverse_checking(const ckdtree *self, const ckdtree *other, 60 | std::vector **results, 61 | const ckdtreenode *node1, const ckdtreenode *node2, 62 | RectRectDistanceTracker *tracker) 63 | { 64 | const ckdtreenode *lnode1; 65 | const ckdtreenode *lnode2; 66 | std::vector *results_i; 67 | double d; 68 | ckdtree_intp_t i, j; 69 | 70 | if (tracker->min_distance > tracker->upper_bound * tracker->epsfac) 71 | return; 72 | else if (tracker->max_distance < tracker->upper_bound / tracker->epsfac) 73 | traverse_no_checking(self, other, results, node1, node2); 74 | else if (node1->split_dim == -1) { /* 1 is leaf node */ 75 | lnode1 = node1; 76 | 77 | if (node2->split_dim == -1) { /* 1 & 2 are leaves */ 78 | 79 | /* brute-force */ 80 | lnode2 = node2; 81 | const double p = tracker->p; 82 | const double tub = tracker->upper_bound; 83 | const double tmd = tracker->max_distance; 84 | const double *sdata = self->raw_data; 85 | const ckdtree_intp_t *sindices = self->raw_indices; 86 | const double *odata = other->raw_data; 87 | const ckdtree_intp_t *oindices = other->raw_indices; 88 | const ckdtree_intp_t m = self->m; 89 | const ckdtree_intp_t start1 = lnode1->start_idx; 90 | const ckdtree_intp_t start2 = lnode2->start_idx; 91 | const ckdtree_intp_t end1 = lnode1->end_idx; 92 | const ckdtree_intp_t end2 = lnode2->end_idx; 93 | 94 | CKDTREE_PREFETCH(sdata + sindices[start1] * m, 0, m); 95 | 96 | if (start1 < end1 - 1) 97 | CKDTREE_PREFETCH(sdata + sindices[start1+1] * m, 0, m); 98 | 99 | for (i = start1; i < end1; ++i) { 100 | 101 | if (i < end1 - 2) 102 | CKDTREE_PREFETCH(sdata + sindices[i+2] * m, 0, m); 103 | 104 | CKDTREE_PREFETCH(odata + oindices[start2] * m, 0, m); 105 | 106 | if (start2 < end2 - 1) 107 | CKDTREE_PREFETCH(odata + oindices[start2+1] * m, 0, m); 108 | 109 | results_i = results[sindices[i]]; 110 | 111 | for (j = start2; j < end2; ++j) { 112 | 113 | if (j < end2 - 2) 114 | CKDTREE_PREFETCH(odata + oindices[j+2] * m, 0, m); 115 | 116 | d = MinMaxDist::point_point_p( 117 | self, 118 | sdata + sindices[i] * m, 119 | odata + oindices[j] * m, 120 | p, m, tmd); 121 | 122 | if (d <= tub) 123 | results_i->push_back(other->raw_indices[j]); 124 | } 125 | } 126 | 127 | } 128 | else { /* 1 is a leaf node, 2 is inner node */ 129 | 130 | tracker->push_less_of(2, node2); 131 | traverse_checking( 132 | self, other, results, node1, node2->less, tracker); 133 | tracker->pop(); 134 | 135 | tracker->push_greater_of(2, node2); 136 | traverse_checking( 137 | self, other, results, node1, node2->greater, tracker); 138 | tracker->pop(); 139 | } 140 | } 141 | else { /* 1 is an inner node */ 142 | if (node2->split_dim == -1) { /* 1 is an inner node, 2 is a leaf node */ 143 | tracker->push_less_of(1, node1); 144 | traverse_checking( 145 | self, other, results, node1->less, node2, tracker); 146 | tracker->pop(); 147 | 148 | tracker->push_greater_of(1, node1); 149 | traverse_checking( 150 | self, other, results, node1->greater, node2, tracker); 151 | tracker->pop(); 152 | } 153 | else { /* 1 & 2 are inner nodes */ 154 | 155 | tracker->push_less_of(1, node1); 156 | tracker->push_less_of(2, node2); 157 | traverse_checking( 158 | self, other, results, node1->less, node2->less, tracker); 159 | tracker->pop(); 160 | 161 | tracker->push_greater_of(2, node2); 162 | traverse_checking( 163 | self, other, results, node1->less, node2->greater, tracker); 164 | tracker->pop(); 165 | tracker->pop(); 166 | 167 | 168 | tracker->push_greater_of(1, node1); 169 | tracker->push_less_of(2, node2); 170 | traverse_checking( 171 | self, other, results, node1->greater, node2->less, tracker); 172 | tracker->pop(); 173 | 174 | tracker->push_greater_of(2, node2); 175 | traverse_checking( 176 | self, other, results, node1->greater, node2->greater, tracker); 177 | tracker->pop(); 178 | tracker->pop(); 179 | } 180 | } 181 | } 182 | 183 | int 184 | query_ball_tree(const ckdtree *self, const ckdtree *other, 185 | const double r, const double p, const double eps, 186 | std::vector **results) 187 | { 188 | 189 | #define HANDLE(cond, kls) \ 190 | if(cond) { \ 191 | RectRectDistanceTracker tracker(self, r1, r2, p, eps, r); \ 192 | traverse_checking(self, other, results, self->ctree, other->ctree, \ 193 | &tracker); \ 194 | } else 195 | 196 | Rectangle r1(self->m, self->raw_mins, self->raw_maxes); 197 | Rectangle r2(other->m, other->raw_mins, other->raw_maxes); 198 | 199 | if(CKDTREE_LIKELY(self->raw_boxsize_data == NULL)) { 200 | HANDLE(CKDTREE_LIKELY(p == 2), MinkowskiDistP2) 201 | HANDLE(p == 1, MinkowskiDistP1) 202 | HANDLE(ckdtree_isinf(p), MinkowskiDistPinf) 203 | HANDLE(1, MinkowskiDistPp) 204 | {} 205 | } else { 206 | HANDLE(CKDTREE_LIKELY(p == 2), BoxMinkowskiDistP2) 207 | HANDLE(p == 1, BoxMinkowskiDistP1) 208 | HANDLE(ckdtree_isinf(p), BoxMinkowskiDistPinf) 209 | HANDLE(1, BoxMinkowskiDistPp) 210 | {} 211 | } 212 | return 0; 213 | } 214 | -------------------------------------------------------------------------------- /jakteristics/ckdtree/ckdtree/src/query_pairs.cxx: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | 13 | #include "ckdtree_decl.h" 14 | #include "ordered_pair.h" 15 | #include "rectangle.h" 16 | 17 | 18 | static void 19 | traverse_no_checking(const ckdtree *self, 20 | std::vector *results, 21 | const ckdtreenode *node1, const ckdtreenode *node2) 22 | { 23 | const ckdtreenode *lnode1; 24 | const ckdtreenode *lnode2; 25 | ckdtree_intp_t i, j, min_j; 26 | const ckdtree_intp_t *indices = self->raw_indices; 27 | 28 | if (node1->split_dim == -1) { /* leaf node */ 29 | lnode1 = node1; 30 | 31 | if (node2->split_dim == -1) { /* leaf node */ 32 | lnode2 = node2; 33 | 34 | const ckdtree_intp_t start1 = lnode1->start_idx; 35 | const ckdtree_intp_t start2 = lnode2->start_idx; 36 | const ckdtree_intp_t end1 = lnode1->end_idx; 37 | const ckdtree_intp_t end2 = lnode2->end_idx; 38 | 39 | for (i = start1; i < end1; ++i) { 40 | 41 | /* Special care here to avoid duplicate pairs */ 42 | if (node1 == node2) 43 | min_j = i + 1; 44 | else 45 | min_j = start2; 46 | 47 | for (j = min_j; j < end2; ++j) 48 | add_ordered_pair(results, indices[i], indices[j]); 49 | } 50 | } 51 | else { 52 | traverse_no_checking(self, results, node1, node2->less); 53 | traverse_no_checking(self, results, node1, node2->greater); 54 | } 55 | } 56 | else { 57 | if (node1 == node2) { 58 | /* 59 | * Avoid traversing (node1->less, node2->greater) and 60 | * (node1->greater, node2->less) (it's the same node pair twice 61 | * over, which is the source of the complication in the 62 | * original KDTree.query_pairs) 63 | */ 64 | traverse_no_checking(self, results, node1->less, node2->less); 65 | traverse_no_checking(self, results, node1->less, node2->greater); 66 | traverse_no_checking(self, results, node1->greater, node2->greater); 67 | } 68 | else { 69 | traverse_no_checking(self, results, node1->less, node2); 70 | traverse_no_checking(self, results, node1->greater, node2); 71 | } 72 | } 73 | } 74 | 75 | 76 | template static void 77 | traverse_checking(const ckdtree *self, 78 | std::vector *results, 79 | const ckdtreenode *node1, const ckdtreenode *node2, 80 | RectRectDistanceTracker *tracker) 81 | { 82 | const ckdtreenode *lnode1; 83 | const ckdtreenode *lnode2; 84 | double d; 85 | ckdtree_intp_t i, j, min_j; 86 | 87 | if (tracker->min_distance > tracker->upper_bound * tracker->epsfac) 88 | return; 89 | else if (tracker->max_distance < tracker->upper_bound / tracker->epsfac) 90 | traverse_no_checking(self, results, node1, node2); 91 | else if (node1->split_dim == -1) { /* 1 is leaf node */ 92 | lnode1 = node1; 93 | 94 | if (node2->split_dim == -1) { /* 1 & 2 are leaves */ 95 | lnode2 = node2; 96 | 97 | /* brute-force */ 98 | const double p = tracker->p; 99 | const double tub = tracker->upper_bound; 100 | const double *data = self->raw_data; 101 | const ckdtree_intp_t *indices = self->raw_indices; 102 | const ckdtree_intp_t m = self->m; 103 | const ckdtree_intp_t start1 = lnode1->start_idx; 104 | const ckdtree_intp_t start2 = lnode2->start_idx; 105 | const ckdtree_intp_t end1 = lnode1->end_idx; 106 | const ckdtree_intp_t end2 = lnode2->end_idx; 107 | 108 | CKDTREE_PREFETCH(data+indices[start1]*m, 0, m); 109 | if (start1 < end1 - 1) 110 | CKDTREE_PREFETCH(data+indices[start1+1]*m, 0, m); 111 | 112 | for(i = start1; i < end1; ++i) { 113 | 114 | if (i < end1 - 2) 115 | CKDTREE_PREFETCH(data+indices[i+2]*m, 0, m); 116 | 117 | /* Special care here to avoid duplicate pairs */ 118 | if (node1 == node2) 119 | min_j = i + 1; 120 | else 121 | min_j = start2; 122 | 123 | if (min_j < end2) 124 | CKDTREE_PREFETCH(data+indices[min_j]*m, 0, m); 125 | if (min_j < end2 - 1) 126 | CKDTREE_PREFETCH(data+indices[min_j+1]*m, 0, m); 127 | 128 | for (j = min_j; j < end2; ++j) { 129 | 130 | if (j < end2 - 2) 131 | CKDTREE_PREFETCH(data+indices[j+2]*m, 0, m); 132 | 133 | d = MinMaxDist::point_point_p( 134 | self, 135 | data + indices[i] * m, 136 | data + indices[j] * m, 137 | p, m, tub); 138 | 139 | if (d <= tub) 140 | add_ordered_pair(results, indices[i], indices[j]); 141 | } 142 | } 143 | } 144 | else { /* 1 is a leaf node, 2 is inner node */ 145 | tracker->push_less_of(2, node2); 146 | traverse_checking(self, results, node1, node2->less, tracker); 147 | tracker->pop(); 148 | 149 | tracker->push_greater_of(2, node2); 150 | traverse_checking(self, results, node1, node2->greater, tracker); 151 | tracker->pop(); 152 | } 153 | } 154 | else { /* 1 is an inner node */ 155 | if (node2->split_dim == -1) { /* 1 is an inner node, 2 is a leaf node */ 156 | tracker->push_less_of(1, node1); 157 | traverse_checking(self, results, node1->less, node2, tracker); 158 | tracker->pop(); 159 | 160 | tracker->push_greater_of(1, node1); 161 | traverse_checking(self, results, node1->greater, node2, tracker); 162 | tracker->pop(); 163 | } 164 | else { /* 1 and 2 are inner nodes */ 165 | tracker->push_less_of(1, node1); 166 | tracker->push_less_of(2, node2); 167 | traverse_checking(self, results, node1->less, node2->less, tracker); 168 | tracker->pop(); 169 | 170 | tracker->push_greater_of(2, node2); 171 | traverse_checking(self, results, node1->less, node2->greater, 172 | tracker); 173 | tracker->pop(); 174 | tracker->pop(); 175 | 176 | tracker->push_greater_of(1, node1); 177 | if (node1 != node2) { 178 | /* 179 | * Avoid traversing (node1->less, node2->greater) and 180 | * (node1->greater, node2->less) (it's the same node pair 181 | * twice over, which is the source of the complication in 182 | * the original KDTree.query_pairs) 183 | */ 184 | tracker->push_less_of(2, node2); 185 | traverse_checking(self, results, node1->greater, node2->less, 186 | tracker); 187 | tracker->pop(); 188 | } 189 | tracker->push_greater_of(2, node2); 190 | traverse_checking(self, results, node1->greater, node2->greater, 191 | tracker); 192 | tracker->pop(); 193 | tracker->pop(); 194 | } 195 | } 196 | } 197 | 198 | 199 | #include 200 | 201 | int 202 | query_pairs(const ckdtree *self, 203 | const double r, const double p, const double eps, 204 | std::vector *results) 205 | { 206 | 207 | #define HANDLE(cond, kls) \ 208 | if(cond) { \ 209 | RectRectDistanceTracker tracker(self, r1, r2, p, eps, r);\ 210 | traverse_checking(self, results, self->ctree, self->ctree, \ 211 | &tracker); \ 212 | } else 213 | 214 | Rectangle r1(self->m, self->raw_mins, self->raw_maxes); 215 | Rectangle r2(self->m, self->raw_mins, self->raw_maxes); 216 | 217 | if(CKDTREE_LIKELY(self->raw_boxsize_data == NULL)) { 218 | HANDLE(CKDTREE_LIKELY(p == 2), MinkowskiDistP2) 219 | HANDLE(p == 1, MinkowskiDistP1) 220 | HANDLE(ckdtree_isinf(p), MinkowskiDistPinf) 221 | HANDLE(1, MinkowskiDistPp) 222 | {} 223 | } else { 224 | HANDLE(CKDTREE_LIKELY(p == 2), BoxMinkowskiDistP2) 225 | HANDLE(p == 1, BoxMinkowskiDistP1) 226 | HANDLE(ckdtree_isinf(p), BoxMinkowskiDistPinf) 227 | HANDLE(1, BoxMinkowskiDistPp) 228 | {} 229 | } 230 | 231 | return 0; 232 | } 233 | 234 | -------------------------------------------------------------------------------- /jakteristics/ckdtree/ckdtree/src/rectangle.h: -------------------------------------------------------------------------------- 1 | 2 | #ifndef CKDTREE_CPP_RECTANGLE 3 | #define CKDTREE_CPP_RECTANGLE 4 | 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | 12 | 13 | /* Interval arithmetic 14 | * =================== 15 | */ 16 | 17 | struct Rectangle { 18 | 19 | const ckdtree_intp_t m; 20 | 21 | /* the last const is to allow const Rectangle to use these functions; 22 | * also notice we had to mark buf mutable to avoid writing non const version 23 | * of the same accessors. */ 24 | double * const maxes() const { return &buf[0]; } 25 | double * const mins() const { return &buf[0] + m; } 26 | 27 | Rectangle(const ckdtree_intp_t _m, 28 | const double *_mins, 29 | const double *_maxes) : m(_m), buf(2 * m) { 30 | 31 | /* copy array data */ 32 | /* FIXME: use std::vector ? */ 33 | std::memcpy((void*)mins(), (void*)_mins, m*sizeof(double)); 34 | std::memcpy((void*)maxes(), (void*)_maxes, m*sizeof(double)); 35 | }; 36 | 37 | Rectangle(const Rectangle& rect) : m(rect.m), buf(rect.buf) {}; 38 | 39 | private: 40 | mutable std::vector buf; 41 | }; 42 | 43 | #include "distance.h" 44 | 45 | /* 46 | * Rectangle-to-rectangle distance tracker 47 | * ======================================= 48 | * 49 | * The logical unit that repeats over and over is to keep track of the 50 | * maximum and minimum distances between points in two hyperrectangles 51 | * as these rectangles are successively split. 52 | * 53 | * Example 54 | * ------- 55 | * node1 encloses points in rect1, node2 encloses those in rect2 56 | * 57 | * cdef RectRectDistanceTracker dist_tracker 58 | * dist_tracker = RectRectDistanceTracker(rect1, rect2, p) 59 | * 60 | * ... 61 | * 62 | * if dist_tracker.min_distance < ...: 63 | * ... 64 | * 65 | * dist_tracker.push_less_of(1, node1) 66 | * do_something(node1.less, dist_tracker) 67 | * dist_tracker.pop() 68 | * 69 | * dist_tracker.push_greater_of(1, node1) 70 | * do_something(node1.greater, dist_tracker) 71 | * dist_tracker.pop() 72 | * 73 | * Notice that Point is just a reduced case of Rectangle where 74 | * mins == maxes. 75 | * 76 | */ 77 | 78 | struct RR_stack_item { 79 | ckdtree_intp_t which; 80 | ckdtree_intp_t split_dim; 81 | double min_along_dim; 82 | double max_along_dim; 83 | double min_distance; 84 | double max_distance; 85 | }; 86 | 87 | const ckdtree_intp_t LESS = 1; 88 | const ckdtree_intp_t GREATER = 2; 89 | 90 | template 91 | struct RectRectDistanceTracker { 92 | 93 | const ckdtree * tree; 94 | Rectangle rect1; 95 | Rectangle rect2; 96 | double p; 97 | double epsfac; 98 | double upper_bound; 99 | double min_distance; 100 | double max_distance; 101 | 102 | ckdtree_intp_t stack_size; 103 | ckdtree_intp_t stack_max_size; 104 | std::vector stack_arr; 105 | RR_stack_item *stack; 106 | 107 | /* if min/max distance / adjustment is less than this, 108 | * we believe the incremental tracking is inaccurate */ 109 | double inaccurate_distance_limit; 110 | 111 | void _resize_stack(const ckdtree_intp_t new_max_size) { 112 | stack_arr.resize(new_max_size); 113 | stack = &stack_arr[0]; 114 | stack_max_size = new_max_size; 115 | }; 116 | 117 | RectRectDistanceTracker(const ckdtree *_tree, 118 | const Rectangle& _rect1, const Rectangle& _rect2, 119 | const double _p, const double eps, 120 | const double _upper_bound) 121 | : tree(_tree), rect1(_rect1), rect2(_rect2), stack_arr(8) { 122 | 123 | if (rect1.m != rect2.m) { 124 | const char *msg = "rect1 and rect2 have different dimensions"; 125 | throw std::invalid_argument(msg); // raises ValueError 126 | } 127 | 128 | p = _p; 129 | 130 | /* internally we represent all distances as distance ** p */ 131 | if (CKDTREE_LIKELY(p == 2.0)) 132 | upper_bound = _upper_bound * _upper_bound; 133 | else if ((!ckdtree_isinf(p)) && (!isinf(_upper_bound))) 134 | upper_bound = std::pow(_upper_bound,p); 135 | else 136 | upper_bound = _upper_bound; 137 | 138 | /* fiddle approximation factor */ 139 | if (CKDTREE_LIKELY(p == 2.0)) { 140 | double tmp = 1. + eps; 141 | epsfac = 1. / (tmp*tmp); 142 | } 143 | else if (eps == 0.) 144 | epsfac = 1.; 145 | else if (ckdtree_isinf(p)) 146 | epsfac = 1. / (1. + eps); 147 | else 148 | epsfac = 1. / std::pow((1. + eps), p); 149 | 150 | stack = &stack_arr[0]; 151 | stack_max_size = 8; 152 | stack_size = 0; 153 | 154 | /* Compute initial min and max distances */ 155 | MinMaxDist::rect_rect_p(tree, rect1, rect2, p, &min_distance, &max_distance); 156 | if(ckdtree_isinf(max_distance)) { 157 | const char *msg = "Encountering floating point overflow. " 158 | "The value of p too large for this dataset; " 159 | "For such large p, consider using the special case p=np.inf . "; 160 | throw std::invalid_argument(msg); // raises ValueError 161 | } 162 | inaccurate_distance_limit = max_distance; 163 | }; 164 | 165 | 166 | void push(const ckdtree_intp_t which, const intptr_t direction, 167 | const ckdtree_intp_t split_dim, const double split_val) { 168 | 169 | const double p = this->p; 170 | /* subnomial is 1 if round-off is expected to taint the incremental distance tracking. 171 | * in that case we always recompute the distances. 172 | * Recomputing costs more calls to pow, thus if the round-off error does not seem 173 | * to wipe out the value, then we still do the incremental update. 174 | * */ 175 | int subnomial = 0; 176 | 177 | Rectangle *rect; 178 | if (which == 1) 179 | rect = &rect1; 180 | else 181 | rect = &rect2; 182 | 183 | /* push onto stack */ 184 | if (stack_size == stack_max_size) 185 | _resize_stack(stack_max_size * 2); 186 | 187 | RR_stack_item *item = &stack[stack_size]; 188 | ++stack_size; 189 | item->which = which; 190 | item->split_dim = split_dim; 191 | item->min_distance = min_distance; 192 | item->max_distance = max_distance; 193 | item->min_along_dim = rect->mins()[split_dim]; 194 | item->max_along_dim = rect->maxes()[split_dim]; 195 | 196 | /* update min/max distances */ 197 | double min1, max1; 198 | double min2, max2; 199 | 200 | MinMaxDist::interval_interval_p(tree, rect1, rect2, split_dim, p, &min1, &max1); 201 | 202 | if (direction == LESS) 203 | rect->maxes()[split_dim] = split_val; 204 | else 205 | rect->mins()[split_dim] = split_val; 206 | 207 | MinMaxDist::interval_interval_p(tree, rect1, rect2, split_dim, p, &min2, &max2); 208 | 209 | subnomial = subnomial || (min_distance < inaccurate_distance_limit || max_distance < inaccurate_distance_limit); 210 | 211 | subnomial = subnomial || ((min1 != 0 && min1 < inaccurate_distance_limit) || max1 < inaccurate_distance_limit); 212 | subnomial = subnomial || ((min2 != 0 && min2 < inaccurate_distance_limit) || max2 < inaccurate_distance_limit); 213 | subnomial = subnomial || (min_distance < inaccurate_distance_limit || max_distance < inaccurate_distance_limit); 214 | 215 | if (CKDTREE_UNLIKELY(subnomial)) { 216 | MinMaxDist::rect_rect_p(tree, rect1, rect2, p, &min_distance, &max_distance); 217 | } else { 218 | min_distance += (min2 - min1); 219 | max_distance += (max2 - max1); 220 | } 221 | }; 222 | 223 | inline void push_less_of(const ckdtree_intp_t which, 224 | const ckdtreenode *node) { 225 | push(which, LESS, node->split_dim, node->split); 226 | }; 227 | 228 | inline void push_greater_of(const ckdtree_intp_t which, 229 | const ckdtreenode *node) { 230 | push(which, GREATER, node->split_dim, node->split); 231 | }; 232 | 233 | inline void pop() { 234 | /* pop from stack */ 235 | --stack_size; 236 | 237 | /* assert stack_size >= 0 */ 238 | if (CKDTREE_UNLIKELY(stack_size < 0)) { 239 | const char *msg = "Bad stack size. This error should never occur."; 240 | throw std::logic_error(msg); 241 | } 242 | 243 | RR_stack_item* item = &stack[stack_size]; 244 | min_distance = item->min_distance; 245 | max_distance = item->max_distance; 246 | 247 | if (item->which == 1) { 248 | rect1.mins()[item->split_dim] = item->min_along_dim; 249 | rect1.maxes()[item->split_dim] = item->max_along_dim; 250 | } 251 | else { 252 | rect2.mins()[item->split_dim] = item->min_along_dim; 253 | rect2.maxes()[item->split_dim] = item->max_along_dim; 254 | } 255 | }; 256 | 257 | }; 258 | 259 | 260 | #endif 261 | -------------------------------------------------------------------------------- /jakteristics/ckdtree/ckdtree/src/sparse_distances.cxx: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | 13 | #include "ckdtree_decl.h" 14 | #include "rectangle.h" 15 | #include "coo_entries.h" 16 | 17 | template static void 18 | traverse(const ckdtree *self, const ckdtree *other, 19 | std::vector *results, 20 | const ckdtreenode *node1, const ckdtreenode *node2, 21 | RectRectDistanceTracker *tracker) 22 | { 23 | 24 | if (tracker->min_distance > tracker->upper_bound) 25 | return; 26 | else if (node1->split_dim == -1) { /* 1 is leaf node */ 27 | 28 | if (node2->split_dim == -1) { /* 1 & 2 are leaves */ 29 | /* brute-force */ 30 | const double p = tracker->p; 31 | const double tub = tracker->upper_bound; 32 | const double *sdata = self->raw_data; 33 | const ckdtree_intp_t *sindices = self->raw_indices; 34 | const double *odata = other->raw_data; 35 | const ckdtree_intp_t *oindices = other->raw_indices; 36 | const ckdtree_intp_t m = self->m; 37 | const ckdtree_intp_t start1 = node1->start_idx; 38 | const ckdtree_intp_t start2 = node2->start_idx; 39 | const ckdtree_intp_t end1 = node1->end_idx; 40 | const ckdtree_intp_t end2 = node2->end_idx; 41 | 42 | CKDTREE_PREFETCH(sdata + sindices[start1] * m, 0, m); 43 | if (start1 < end1 - 1) 44 | CKDTREE_PREFETCH(sdata + sindices[start1+1] * m, 0, m); 45 | 46 | for (ckdtree_intp_t i = start1; i < end1; ++i) { 47 | 48 | if (i < end1 - 2) 49 | CKDTREE_PREFETCH(sdata + sindices[i+2] * m, 0, m); 50 | 51 | CKDTREE_PREFETCH(odata + oindices[start2] * m, 0, m); 52 | if (start2 < end2 - 1) 53 | CKDTREE_PREFETCH(sdata + oindices[start2+1] * m, 0, m); 54 | 55 | for (ckdtree_intp_t j = start2; j < end2; ++j) { 56 | 57 | if (j < end2 - 2) 58 | CKDTREE_PREFETCH(odata + oindices[j+2] * m, 0, m); 59 | 60 | double d = MinMaxDist::point_point_p( 61 | self, 62 | sdata + sindices[i] * m, 63 | odata + oindices[j] * m, 64 | p, m, tub); 65 | 66 | if (d <= tub) { 67 | if (CKDTREE_LIKELY(p == 2.0)) 68 | d = std::sqrt(d); 69 | else if ((p != 1) && (!ckdtree_isinf(p))) 70 | d = std::pow(d, 1. / p); 71 | 72 | coo_entry e = {sindices[i], oindices[j], d}; 73 | results->push_back(e); 74 | } 75 | } 76 | } 77 | } 78 | else { /* 1 is a leaf node, 2 is inner node */ 79 | tracker->push_less_of(2, node2); 80 | traverse(self, other, results, node1, node2->less, tracker); 81 | tracker->pop(); 82 | 83 | tracker->push_greater_of(2, node2); 84 | traverse(self, other, results, node1, node2->greater, tracker); 85 | tracker->pop(); 86 | } 87 | } 88 | else { /* 1 is an inner node */ 89 | if (node2->split_dim == -1) { 90 | /* 1 is an inner node, 2 is a leaf node*/ 91 | tracker->push_less_of(1, node1); 92 | traverse(self, other, results, node1->less, node2, tracker); 93 | tracker->pop(); 94 | 95 | tracker->push_greater_of(1, node1); 96 | traverse(self, other, results, node1->greater, node2, tracker); 97 | tracker->pop(); 98 | } 99 | else { /* 1 and 2 are inner nodes */ 100 | tracker->push_less_of(1, node1); 101 | tracker->push_less_of(2, node2); 102 | traverse(self, other, results, node1->less, node2->less, tracker); 103 | tracker->pop(); 104 | 105 | tracker->push_greater_of(2, node2); 106 | traverse(self, other, results, node1->less, node2->greater, tracker); 107 | tracker->pop(); 108 | tracker->pop(); 109 | 110 | tracker->push_greater_of(1, node1); 111 | tracker->push_less_of(2, node2); 112 | traverse(self, other, results, node1->greater, node2->less, tracker); 113 | tracker->pop(); 114 | 115 | tracker->push_greater_of(2, node2); 116 | traverse(self, other, results, node1->greater, node2->greater, 117 | tracker); 118 | tracker->pop(); 119 | tracker->pop(); 120 | } 121 | } 122 | } 123 | 124 | 125 | int 126 | sparse_distance_matrix(const ckdtree *self, const ckdtree *other, 127 | const double p, 128 | const double max_distance, 129 | std::vector *results) 130 | { 131 | #define HANDLE(cond, kls) \ 132 | if(cond) { \ 133 | RectRectDistanceTracker tracker(self, r1, r2, p, 0, max_distance);\ 134 | traverse(self, other, results, self->ctree, other->ctree, &tracker); \ 135 | } else 136 | 137 | Rectangle r1(self->m, self->raw_mins, self->raw_maxes); 138 | Rectangle r2(other->m, other->raw_mins, other->raw_maxes); 139 | if(CKDTREE_LIKELY(self->raw_boxsize_data == NULL)) { 140 | HANDLE(CKDTREE_LIKELY(p == 2), MinkowskiDistP2) 141 | HANDLE(p == 1, MinkowskiDistP1) 142 | HANDLE(ckdtree_isinf(p), MinkowskiDistPinf) 143 | HANDLE(1, MinkowskiDistPp) 144 | {} 145 | } else { 146 | HANDLE(CKDTREE_LIKELY(p == 2), BoxMinkowskiDistP2) 147 | HANDLE(p == 1, BoxMinkowskiDistP1) 148 | HANDLE(ckdtree_isinf(p), BoxMinkowskiDistPinf) 149 | HANDLE(1, BoxMinkowskiDistPp) 150 | {} 151 | } 152 | 153 | return 0; 154 | } 155 | -------------------------------------------------------------------------------- /jakteristics/constants.py: -------------------------------------------------------------------------------- 1 | FEATURE_NAMES = [ 2 | "eigenvalue_sum", 3 | "omnivariance", 4 | "eigenentropy", 5 | "anisotropy", 6 | "planarity", 7 | "linearity", 8 | "PCA1", 9 | "PCA2", 10 | "surface_variation", 11 | "sphericity", 12 | "verticality", 13 | "nx", 14 | "ny", 15 | "nz", 16 | "number_of_neighbors", 17 | "eigenvalue1", 18 | "eigenvalue2", 19 | "eigenvalue3", 20 | "eigenvector1x", 21 | "eigenvector1y", 22 | "eigenvector1z", 23 | "eigenvector2x", 24 | "eigenvector2y", 25 | "eigenvector2z", 26 | "eigenvector3x", 27 | "eigenvector3y", 28 | "eigenvector3z", 29 | ] 30 | -------------------------------------------------------------------------------- /jakteristics/extension.pyx: -------------------------------------------------------------------------------- 1 | # cython: language_level=3 2 | # distutils: language = c++ 3 | 4 | import numpy as np 5 | import multiprocessing 6 | 7 | from cpython.mem cimport PyMem_Malloc, PyMem_Realloc, PyMem_Free 8 | from libc.string cimport memset 9 | from libcpp cimport bool 10 | cimport openmp 11 | cimport cython 12 | from cython.parallel import prange, parallel 13 | from libc.math cimport fabs, pow, log, sqrt 14 | cimport numpy as np 15 | from libcpp.vector cimport vector 16 | from libcpp.map cimport map as cppmap 17 | from libcpp.string cimport string 18 | from libc.stdint cimport uintptr_t, uint32_t, int8_t, uint8_t, int64_t 19 | 20 | from .ckdtree.ckdtree cimport cKDTree, ckdtree, query_ball_point 21 | from . cimport utils 22 | from .constants import FEATURE_NAMES 23 | 24 | 25 | @cython.boundscheck(False) 26 | @cython.wraparound(False) 27 | @cython.initializedcheck(False) 28 | @cython.cdivision(True) 29 | def compute_features( 30 | double [:, ::1] points, 31 | float search_radius, 32 | *, 33 | cKDTree kdtree=None, 34 | int num_threads=-1, 35 | int max_k_neighbors=50000, 36 | bint euclidean_distance=True, 37 | feature_names=FEATURE_NAMES, 38 | float eps=0.0, 39 | ): 40 | cdef: 41 | cppmap [string, uint8_t] features_map 42 | 43 | int64_t n_points = points.shape[0] 44 | double [::1, :] neighbor_points 45 | double [::1, :] eigenvectors 46 | double [:] eigenvalues 47 | 48 | int i, j, k 49 | uint32_t neighbor_id 50 | uint32_t n_neighbors_at_id 51 | int thread_id 52 | int number_of_neighbors 53 | 54 | float [:, :] features = np.full((n_points, len(feature_names)), float("NaN"), dtype=np.float32) 55 | 56 | const np.float64_t[:, ::1] radius_vector 57 | np.float64_t p = 2 if euclidean_distance else 1 58 | np.float64_t eps_scipy = 0.0 59 | vector[np.intp_t] *** threaded_vvres 60 | int return_length = False 61 | 62 | if not points.shape[1] == 3: 63 | raise ValueError("You must provide an (n x 3) numpy array.") 64 | 65 | if num_threads == -1: 66 | num_threads = multiprocessing.cpu_count() 67 | 68 | if kdtree is None: 69 | kdtree = cKDTree(points) 70 | 71 | for n, name in enumerate(feature_names): 72 | if name not in FEATURE_NAMES: 73 | raise ValueError(f"Unknown feature name: {name}") 74 | features_map[name.encode()] = n 75 | 76 | radius_vector = np.full((num_threads, 3), fill_value=search_radius) 77 | neighbor_points = np.zeros([3, max_k_neighbors * num_threads], dtype=np.float64, order="F") 78 | eigenvectors = np.zeros([3, 3 * num_threads], dtype=np.float64, order="F") 79 | eigenvalues = np.zeros(3 * num_threads, dtype=np.float64) 80 | 81 | threaded_vvres = init_result_vectors(num_threads) 82 | 83 | try: 84 | for i in prange(n_points, nogil=True, num_threads=num_threads): 85 | thread_id = openmp.omp_get_thread_num() 86 | 87 | threaded_vvres[thread_id][0].clear() 88 | query_ball_point( 89 | kdtree.cself, 90 | &points[i, 0], 91 | &radius_vector[thread_id, 0], 92 | p, 93 | eps_scipy, 94 | 1, 95 | threaded_vvres[thread_id], 96 | return_length, 97 | ) 98 | 99 | n_neighbors_at_id = threaded_vvres[thread_id][0].size() 100 | number_of_neighbors = n_neighbors_at_id 101 | 102 | if n_neighbors_at_id > max_k_neighbors: 103 | n_neighbors_at_id = max_k_neighbors 104 | elif n_neighbors_at_id == 0: 105 | continue 106 | 107 | for j in range(n_neighbors_at_id): 108 | neighbor_id = threaded_vvres[thread_id][0][0][j] 109 | for k in range(3): 110 | neighbor_points[k, thread_id * max_k_neighbors + j] = kdtree.cself.raw_data[neighbor_id * 3 + k] 111 | 112 | utils.c_covariance( 113 | neighbor_points[:, thread_id * max_k_neighbors:thread_id * max_k_neighbors + n_neighbors_at_id], 114 | eigenvectors[:, thread_id * 3:(thread_id + 1) * 3], 115 | ) 116 | utils.c_eigenvectors( 117 | eigenvectors[:, thread_id * 3:(thread_id + 1) * 3], 118 | eigenvalues[thread_id * 3:(thread_id + 1) * 3], 119 | ) 120 | 121 | compute_features_from_eigenvectors( 122 | number_of_neighbors, 123 | eigenvalues[thread_id * 3 : thread_id * 3 + 3], 124 | eigenvectors[:, thread_id * 3 : thread_id * 3 + 3], 125 | features[i, :], 126 | features_map, 127 | ) 128 | 129 | finally: 130 | free_result_vectors(threaded_vvres, num_threads) 131 | 132 | return np.asarray(features) 133 | 134 | @cython.boundscheck(False) 135 | @cython.wraparound(False) 136 | @cython.cdivision(True) 137 | cdef inline void compute_features_from_eigenvectors( 138 | int number_of_neighbors, 139 | double [:] eigenvalues, 140 | double [:, :] eigenvectors, 141 | float [:] out, 142 | cppmap [string, uint8_t] & out_map, 143 | ) nogil: 144 | cdef: 145 | float l1, l2, l3 146 | float eigenvalue_sum 147 | float n0, n1, n2 148 | float norm 149 | 150 | l1 = eigenvalues[0] 151 | l2 = eigenvalues[1] 152 | l3 = eigenvalues[2] 153 | 154 | # Those features are inspired from cloud compare implementation (https://github.com/CloudCompare/CloudCompare/blob/master/CC/src/Neighbourhood.cpp#L871) 155 | # Those features are also implemented in CGAL (https://doc.cgal.org/4.12/Classification/group__PkgClassificationFeatures.html) 156 | 157 | # Sum of eigenvalues equals the original variance of the data 158 | eigenvalue_sum = l1 + l2 + l3 159 | 160 | if out_map.count(b"eigenvalue1"): 161 | out[out_map.at(b"eigenvalue1")] = l1 162 | if out_map.count(b"eigenvalue2"): 163 | out[out_map.at(b"eigenvalue2")] = l2 164 | if out_map.count(b"eigenvalue3"): 165 | out[out_map.at(b"eigenvalue3")] = l3 166 | 167 | if out_map.count(b"number_of_neighbors"): 168 | out[out_map.at(b"number_of_neighbors")] = number_of_neighbors 169 | 170 | if out_map.count(b"eigenvalue_sum"): 171 | out[out_map.at(b"eigenvalue_sum")] = eigenvalue_sum 172 | 173 | if out_map.count(b"omnivariance"): 174 | out[out_map.at(b"omnivariance")] = pow(l1 * l2 * l3, 1.0 / 3.0) 175 | 176 | if out_map.count(b"eigenentropy"): 177 | out[out_map.at(b"eigenentropy")] = -(l1 * log(l1) + l2 * log(l2) + l3 * log(l3)) 178 | 179 | # Anisotropy is the difference between the most principal direction of the point subset. 180 | # Divided by l1 allows to keep this difference in a ratio between 0 and 1 181 | # a difference close to zero (l3 close to l1) means that the subset of points are equally spread in the 3 principal directions 182 | # If the anisotropy is close to 1 (mean l3 close to zero), the subset of points is strongly related only in the first principal component. It depends mainly on one direction. 183 | if out_map.count(b"anisotropy"): 184 | out[out_map.at(b"anisotropy")] = (l1 - l3) / l1 185 | if out_map.count(b"planarity"): 186 | out[out_map.at(b"planarity")] = (l2 - l3) / l1 187 | if out_map.count(b"linearity"): 188 | out[out_map.at(b"linearity")] = (l1 - l2) / l1 189 | if out_map.count(b"PCA1"): 190 | out[out_map.at(b"PCA1")] = l1 / eigenvalue_sum 191 | if out_map.count(b"PCA2"): 192 | out[out_map.at(b"PCA2")] = l2 / eigenvalue_sum 193 | # Surface variance is how the third component contributes to the sum of the eigenvalues 194 | if out_map.count(b"surface_variation"): 195 | out[out_map.at(b"surface_variation")] = l3 / eigenvalue_sum 196 | if out_map.count(b"sphericity"): 197 | out[out_map.at(b"sphericity")] = l3 / l1 198 | 199 | if out_map.count(b"verticality"): 200 | out[out_map.at(b"verticality")] = 1.0 - fabs(eigenvectors[2, 2]) 201 | 202 | # eigenvectors is col-major 203 | if out_map.count(b"nx") or out_map.count(b"ny") or out_map.count(b"nz"): 204 | n0 = eigenvectors[0, 1] * eigenvectors[1, 2] - eigenvectors[0, 2] * eigenvectors[1, 1] 205 | n1 = eigenvectors[0, 2] * eigenvectors[1, 0] - eigenvectors[0, 0] * eigenvectors[1, 2] 206 | n2 = eigenvectors[0, 0] * eigenvectors[1, 1] - eigenvectors[0, 1] * eigenvectors[1, 0] 207 | norm = sqrt(n0 * n0 + n1 * n1 + n2 * n2) 208 | if out_map.count(b"nx"): 209 | out[out_map.at(b"nx")] = n0 / norm 210 | if out_map.count(b"ny"): 211 | out[out_map.at(b"ny")] = n1 / norm 212 | if out_map.count(b"nz"): 213 | out[out_map.at(b"nz")] = n2 / norm 214 | 215 | if out_map.count(b"eigenvector1x"): 216 | out[out_map.at(b"eigenvector1x")] = eigenvectors[0, 0] 217 | if out_map.count(b"eigenvector1y"): 218 | out[out_map.at(b"eigenvector1y")] = eigenvectors[0, 1] 219 | if out_map.count(b"eigenvector1z"): 220 | out[out_map.at(b"eigenvector1z")] = eigenvectors[0, 2] 221 | 222 | if out_map.count(b"eigenvector2x"): 223 | out[out_map.at(b"eigenvector2x")] = eigenvectors[1, 0] 224 | if out_map.count(b"eigenvector2y"): 225 | out[out_map.at(b"eigenvector2y")] = eigenvectors[1, 1] 226 | if out_map.count(b"eigenvector2z"): 227 | out[out_map.at(b"eigenvector2z")] = eigenvectors[1, 2] 228 | 229 | if out_map.count(b"eigenvector3x"): 230 | out[out_map.at(b"eigenvector3x")] = eigenvectors[2, 0] 231 | if out_map.count(b"eigenvector3y"): 232 | out[out_map.at(b"eigenvector3y")] = eigenvectors[2, 1] 233 | if out_map.count(b"eigenvector3z"): 234 | out[out_map.at(b"eigenvector3z")] = eigenvectors[2, 2] 235 | 236 | 237 | cdef vector[np.intp_t] *** init_result_vectors(int num_threads): 238 | """Allocate memory for result vectors, based on thread count""" 239 | threaded_vvres = PyMem_Malloc(num_threads * sizeof(void*)) 240 | if not threaded_vvres: 241 | raise MemoryError() 242 | memset( threaded_vvres, 0, num_threads * sizeof(void*)) 243 | for i in range(num_threads): 244 | threaded_vvres[i] = PyMem_Malloc(sizeof(void*)) 245 | if not threaded_vvres[i]: 246 | raise MemoryError() 247 | memset( threaded_vvres[i], 0, sizeof(void*)) 248 | threaded_vvres[i][0] = new vector[np.intp_t]() 249 | return threaded_vvres 250 | 251 | 252 | cdef void free_result_vectors(vector[np.intp_t] *** threaded_vvres, int num_threads): 253 | """Free memory for result vectors""" 254 | if threaded_vvres != NULL: 255 | for i in range(num_threads): 256 | if threaded_vvres[i] != NULL: 257 | del threaded_vvres[i][0] 258 | PyMem_Free(threaded_vvres[i]) 259 | PyMem_Free(threaded_vvres) 260 | 261 | -------------------------------------------------------------------------------- /jakteristics/las_utils.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | from typing import List, Tuple, Union 3 | 4 | 5 | import numpy as np 6 | import laspy 7 | 8 | 9 | def read_las_xyz( 10 | filename: Union[str, Path], with_offset: bool = False 11 | ) -> Union[np.array, Tuple[np.ndarray, List[float]]]: 12 | """Reads xyz coordinates of a las file, optionally as single precision floating point. 13 | 14 | Arguments: 15 | filename: 16 | The las file to read 17 | with_offset: 18 | If True, returns a tuple of a float32 array of coordinates, and the las header offset 19 | If False, returns only float64 coordinates 20 | Default: False 21 | Returns: 22 | Depending on the `with_offset` parameter, either an (n x 3) array, or 23 | a tuple of an (n x 3) array and the file offset. 24 | """ 25 | with laspy.open(filename, mode="r") as las: 26 | las_data = las.read() 27 | if with_offset: 28 | offset = las.header.offset 29 | points = np.ascontiguousarray(las_data.xyz - offset, dtype="f") 30 | return points, offset 31 | else: 32 | points = np.ascontiguousarray(las_data.xyz) 33 | return points 34 | 35 | 36 | def write_with_extra_dims( 37 | input_path: Path, 38 | output_path: Path, 39 | extra_dims: np.array, 40 | extra_dims_names: List, 41 | ): 42 | """From an existing las file, create a new las file with extra dimensions 43 | 44 | Arguments: 45 | input_path: The input las file. 46 | output_path: The output las file. 47 | extra_dims: The numpy array containing geometric features. 48 | extra_dims_names: A list of names corresponding to each column of `extra_dims`. 49 | """ 50 | if input_path == output_path: 51 | raise ValueError("Paths must not be the same") 52 | 53 | with laspy.open(input_path, mode="r") as in_las: 54 | header = in_las.header 55 | if extra_dims.shape[0] != header.point_count: 56 | raise ValueError( 57 | f"The features and point counts should be equal " 58 | f"{extra_dims.shape[0]} != {header.point_count}" 59 | ) 60 | 61 | data = [(name, extra_dims[:, i]) for i, name in enumerate(extra_dims_names)] 62 | new_dimensions = [ 63 | laspy.ExtraBytesParams(name=name, type=extra_dims.dtype, description=name) 64 | for name in extra_dims_names 65 | ] 66 | 67 | # insert new data in previous pointcloud PackedPointRecord 68 | las_data = in_las.read() 69 | las_data.add_extra_dims(new_dimensions) 70 | las_data.update_header() 71 | for name, array in data: 72 | setattr(las_data, name, array) 73 | 74 | with laspy.open(output_path, mode="w", header=las_data.header) as out_las: 75 | out_las.write_points(las_data.points) 76 | -------------------------------------------------------------------------------- /jakteristics/main.py: -------------------------------------------------------------------------------- 1 | from typing import List, Optional 2 | import warnings 3 | 4 | import numpy as np 5 | 6 | import jakteristics.extension 7 | from jakteristics.ckdtree import ckdtree 8 | 9 | from .constants import FEATURE_NAMES 10 | 11 | 12 | def compute_features( 13 | points: np.ndarray, 14 | search_radius: float, 15 | *, 16 | kdtree: ckdtree.cKDTree = None, 17 | num_threads: int = -1, 18 | max_k_neighbors: int = 50000, 19 | euclidean_distance: bool = True, 20 | feature_names: Optional[List[str]] = None, 21 | eps: float = 0.0, 22 | ) -> np.ndarray: 23 | """ 24 | Compute features for a set of points. 25 | 26 | Parameters: 27 | points: 28 | A contiguous (n, 3) array of xyz coordinates to query. 29 | search_radius: 30 | The radius to query neighbors at each point. 31 | kdtree: 32 | If None, the kdtree is computed from the list of points. 33 | Must be an instance of `jakteristics.cKDTree` 34 | (and not `scipy.spatial.cKDTree`). 35 | num_threads: 36 | The number of threads (OpenMP) to use when doing the computation. 37 | Default: The number of cores on the machine. 38 | max_k_neighbors: 39 | The maximum number of neighbors to query 40 | Larger number will use more memory, but the neighbor points are not 41 | all kept at the same time in memory. 42 | Note: if this number is smaller, the neighbor search will not be faster. 43 | The radius is used to do the query, and the neighbors are then removed 44 | according to this parameter. 45 | euclidean_distance: 46 | How to compute the distance between 2 points. 47 | If true, the Euclidean distance is used. 48 | If false, the sum-of-absolute-values is used ("Manhattan" distance). 49 | feature_names: 50 | The feature names to compute (see `constants.FEATURE_NAMES` for possible values) 51 | Default: all features 52 | eps: 53 | Return approximate nearest neighbors; the k-th returned value 54 | is guaranteed to be no further than (1+eps) times the 55 | distance to the real k-th nearest neighbor. 56 | 57 | Returns: 58 | The computed features, one row per query point, and one column 59 | per requested feature. 60 | """ 61 | 62 | if feature_names is None: 63 | warnings.warn( 64 | "The `feature_names` argument of `compute_features` will be required " 65 | "in a future version of jakteristics." 66 | ) 67 | feature_names = FEATURE_NAMES 68 | 69 | points = np.ascontiguousarray(points) 70 | 71 | return jakteristics.extension.compute_features( 72 | points, 73 | search_radius, 74 | kdtree=kdtree, 75 | num_threads=num_threads, 76 | max_k_neighbors=max_k_neighbors, 77 | euclidean_distance=euclidean_distance, 78 | feature_names=feature_names, 79 | eps=eps, 80 | ) 81 | -------------------------------------------------------------------------------- /jakteristics/utils.pxd: -------------------------------------------------------------------------------- 1 | # cython: language_level=3 2 | # distutils: language = c++ 3 | 4 | cdef void c_matmul_transposed(double[::1, :] a, double[::1, :] out, double alpha) nogil 5 | cdef void c_covariance(double[::1, :] a, double[::1, :] out) nogil 6 | cdef void c_eigenvectors(double[::1, :] a, double[:] eigenvalues) nogil 7 | -------------------------------------------------------------------------------- /jakteristics/utils.pyx: -------------------------------------------------------------------------------- 1 | # cython: language_level=3 2 | # distutils: language = c++ 3 | 4 | import numpy as np 5 | cimport numpy as np 6 | 7 | cimport cython 8 | from libc.math cimport fabs 9 | from scipy.linalg.cython_blas cimport dsyrk 10 | from scipy.linalg.cython_lapack cimport dsyev 11 | 12 | cdef extern from "" namespace "std" nogil: 13 | void sort[Iter](Iter first, Iter last) 14 | 15 | 16 | @cython.boundscheck(False) 17 | @cython.wraparound(False) 18 | @cython.cdivision(True) 19 | cdef inline void c_matmul_transposed(double[::1, :] a, double[::1, :] out, double alpha) nogil: 20 | cdef: 21 | char *uplo = 'U' 22 | char *trans = 'N' 23 | int n = a.shape[0] 24 | int k = a.shape[1] 25 | int lda = a.strides[1] // sizeof(double) # for column major 26 | int ldc = out.strides[1] // sizeof(double) # for column major 27 | double beta = 0.0 28 | 29 | dsyrk(uplo, trans, &n, &k, &alpha, &a[0,0], &lda, &beta, &out[0,0], &ldc) 30 | 31 | cdef int i, j 32 | 33 | # dsyrk computes results for the upper matrix only 34 | for i in range(n): 35 | for j in range(n): 36 | if i > j: 37 | out[i, j] = out[j, i] 38 | 39 | 40 | def py_matmul_transposed(a): 41 | out = np.empty((a.shape[0], a.shape[0]), dtype=np.float64, order="F") 42 | c_matmul_transposed(np.asfortranarray(a), out, 1.0) 43 | return out 44 | 45 | 46 | @cython.boundscheck(False) 47 | @cython.wraparound(False) 48 | @cython.cdivision(True) 49 | cpdef inline void substract_mean(double[::1, :] a) nogil: 50 | """a must be of shape (3, n)""" 51 | cdef: 52 | int n_cols = a.shape[1] 53 | double *mean = [0, 0, 0] 54 | int c 55 | 56 | for c in range(n_cols): 57 | mean[0] += a[0, c] 58 | mean[1] += a[1, c] 59 | mean[2] += a[2, c] 60 | 61 | mean[0] /= n_cols 62 | mean[1] /= n_cols 63 | mean[2] /= n_cols 64 | 65 | for c in range(n_cols): 66 | a[0, c] -= mean[0] 67 | a[1, c] -= mean[1] 68 | a[2, c] -= mean[2] 69 | 70 | 71 | @cython.boundscheck(False) 72 | @cython.wraparound(False) 73 | @cython.cdivision(True) 74 | cdef inline void c_covariance(double[::1, :] a, double[::1, :] out) nogil: 75 | """a must be of shape (3, n)""" 76 | cdef: 77 | int n_rows = a.shape[0] 78 | int i, j 79 | double fact 80 | 81 | substract_mean(a) 82 | 83 | fact = 1.0 / (a.shape[1] - 1) 84 | 85 | c_matmul_transposed(a, out, fact) 86 | 87 | 88 | def py_covariance(a): 89 | out = np.empty((a.shape[0], a.shape[0]), dtype=np.float64, order="F") 90 | c_covariance(np.asfortranarray(a), out) 91 | return out 92 | 93 | 94 | @cython.boundscheck(False) 95 | @cython.wraparound(False) 96 | @cython.cdivision(True) 97 | cdef inline void c_eigenvectors(double[::1, :] a, double[:] eigenvalues) nogil: 98 | """ 99 | http://www.netlib.org/lapack/explore-html/d3/d88/group__real_s_yeigen_ga63d8d12aef8f2711d711d9e6bd833e46.html#ga63d8d12aef8f2711d711d9e6bd833e46 100 | """ 101 | cdef: 102 | char *jobz = 'V' # compute eigenvalues and eigenvectors 103 | char *uplo = 'U' # upper triangle of A is stored 104 | int n = a.shape[0] 105 | int lda = a.strides[1] // sizeof(double) # for column major 106 | double work[256] # not sure how to properly set the size of this array 107 | int lwork = 256 108 | int info 109 | 110 | dsyev(jobz, uplo, &n, &a[0,0], &lda, &eigenvalues[0], &work[0], &lwork, &info) 111 | 112 | c_sort_eigenvalues(a, eigenvalues) 113 | 114 | 115 | @cython.boundscheck(False) 116 | @cython.wraparound(False) 117 | cdef void c_sort_eigenvalues(double[::1, :] eigenvectors, double[:] eigenvalues) nogil: 118 | """Sort eigenvalues and eigenvectors""" 119 | cdef: 120 | int i 121 | int j 122 | int k 123 | int n = 3 124 | double eigenvalues_copy[3] 125 | double eigenvectors_copy[3][3] 126 | 127 | for i in range(n): 128 | eigenvalues[i] = fabs(eigenvalues[i]) 129 | 130 | eigenvalues_copy[i] = eigenvalues[i] 131 | for j in range(n): 132 | eigenvectors_copy[i][j] = eigenvectors[i, j] 133 | 134 | sort(&eigenvalues[0], (&eigenvalues[0]) + n) 135 | # reverse 136 | cdef double temp = eigenvalues[2] 137 | eigenvalues[2] = eigenvalues[0] 138 | eigenvalues[0] = temp 139 | 140 | for i in range(n): 141 | for j in range(n): 142 | if fabs(eigenvalues[i] - eigenvalues_copy[j]) < 1e-5: 143 | for k in range(n): 144 | # eigenvectors is column-major, and eigenvectors_copy is row major 145 | eigenvectors[i, k] = eigenvectors_copy[k][j] 146 | break 147 | 148 | 149 | def py_eigenvectors(a): 150 | eigenvalues = np.empty(a.shape[0], dtype=np.float64, order="F") 151 | a = np.asfortranarray(a) 152 | c_eigenvectors(a, eigenvalues) 153 | return eigenvalues, a -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["setuptools", "wheel", "Cython>=0.25", "numpy", "scipy"] 3 | -------------------------------------------------------------------------------- /requirements-dev.txt: -------------------------------------------------------------------------------- 1 | wheel 2 | black 3 | bump2version 4 | pytest 5 | pytest-runner 6 | twine 7 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | laspy>=2.1.1 2 | scipy 3 | typer 4 | Cython>=0.25 5 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import re 2 | import sys 3 | from os.path import join 4 | from setuptools import Extension, setup 5 | from setuptools.command.build_ext import build_ext 6 | 7 | from Cython.Build import cythonize 8 | import numpy 9 | 10 | 11 | LINUX = sys.platform == "linux" 12 | WINDOWS = sys.platform == "win32" 13 | 14 | 15 | # Avoid a gcc warning below: 16 | # cc1plus: warning: command line option ‘-Wstrict-prototypes’ is valid 17 | # for C/ObjC but not for C++ 18 | class BuildExt(build_ext): 19 | def build_extensions(self): 20 | if LINUX: 21 | if "-Wstrict-prototypes" in self.compiler.compiler_so: 22 | self.compiler.compiler_so.remove("-Wstrict-prototypes") 23 | super().build_extensions() 24 | 25 | 26 | ckdtree = join("jakteristics", "ckdtree") 27 | ckdtree_src = [ 28 | join(ckdtree, "ckdtree", "src", x) 29 | for x in [ 30 | "query.cxx", 31 | "build.cxx", 32 | "query_pairs.cxx", 33 | "count_neighbors.cxx", 34 | "query_ball_point.cxx", 35 | "query_ball_tree.cxx", 36 | "sparse_distances.cxx", 37 | ] 38 | ] 39 | ckdtree_src.append(join(ckdtree, "ckdtree.pyx")) 40 | 41 | ckdtree_includes = [ 42 | numpy.get_include(), 43 | join(ckdtree, "ckdtree", "src"), 44 | join(ckdtree, "_lib"), 45 | ] 46 | 47 | extra_compile_args = ["-fopenmp"] 48 | extra_link_args = ["-fopenmp"] 49 | 50 | if WINDOWS: 51 | extra_compile_args = ["/openmp"] 52 | extra_link_args = ["/openmp"] 53 | 54 | ext_modules = [ 55 | Extension( 56 | "jakteristics.extension", 57 | sources=[ 58 | "jakteristics/extension.pyx", 59 | join(ckdtree, "ckdtree", "src", "query_ball_point.cxx"), 60 | ], 61 | include_dirs=ckdtree_includes, 62 | extra_compile_args=extra_compile_args, 63 | extra_link_args=extra_link_args, 64 | define_macros=[("NPY_NO_DEPRECATED_API", "1")], 65 | ), 66 | Extension( 67 | "jakteristics.utils", 68 | sources=["jakteristics/utils.pyx"], 69 | include_dirs=[numpy.get_include()], 70 | define_macros=[("NPY_NO_DEPRECATED_API", "1")], 71 | ), 72 | Extension( 73 | "jakteristics.ckdtree.ckdtree", 74 | sources=ckdtree_src, 75 | include_dirs=ckdtree_includes, 76 | define_macros=[("NPY_NO_DEPRECATED_API", "1")], 77 | ), 78 | ] 79 | 80 | ext_modules = cythonize(ext_modules, language_level=3) 81 | cmdclass = {"build_ext": BuildExt} 82 | 83 | 84 | with open("README.rst") as readme_file: 85 | readme = readme_file.read() 86 | 87 | with open("HISTORY.rst") as history_file: 88 | history = history_file.read().replace(".. :changelog:", "") 89 | 90 | requirements = [line for line in open("requirements.txt").read().split("\n") if line] 91 | 92 | about = open(join("jakteristics", "__about__.py")).read() 93 | version = re.search(r"__version__ ?= ?['\"](.+)['\"]", about).group(1) 94 | 95 | setup( 96 | name="jakteristics", 97 | version=version, 98 | description="Point cloud geometric properties from python.", 99 | long_description=readme + "\n\n" + history, 100 | author="David Caron", 101 | author_email="david.caron@jakarto.com", 102 | url="https://github.com/jakarto3d/jakteristics", 103 | packages=["jakteristics"], 104 | package_dir={"jakteristics": "jakteristics"}, 105 | package_data={"": ["*.pyx", "*.pxd", "*.h", "*.cpp"]}, 106 | python_requires=">=3.7", 107 | install_requires=requirements, 108 | tests_require=["pytest"], 109 | license="BSD", 110 | zip_safe=False, 111 | keywords="jakteristics", 112 | classifiers=[ 113 | "Development Status :: 2 - Pre-Alpha", 114 | "Intended Audience :: Developers", 115 | "License :: OSI Approved :: BSD License", 116 | "Natural Language :: English", 117 | "Programming Language :: Cython", 118 | "Programming Language :: Python :: 3", 119 | "Programming Language :: Python :: 3.8", 120 | ], 121 | test_suite="tests", 122 | cmdclass=cmdclass, 123 | ext_modules=ext_modules, 124 | entry_points={"console_scripts": ["jakteristics = jakteristics.__main__:app"]}, 125 | ) 126 | -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | import shutil 3 | 4 | import pytest 5 | 6 | TEST_DATA = Path(__file__).parent / "data" 7 | 8 | 9 | @pytest.fixture 10 | def temp_dir(): 11 | tmp = TEST_DATA / "tmp" 12 | tmp.mkdir(parents=True, exist_ok=True) 13 | yield tmp 14 | shutil.rmtree(tmp, ignore_errors=True) 15 | -------------------------------------------------------------------------------- /tests/data/test_0.02_seconde.las: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jakarto3d/jakteristics/6a22e0072a42cf4b41a04a6e214fd7ef3df5ec67/tests/data/test_0.02_seconde.las -------------------------------------------------------------------------------- /tests/test_cli.py: -------------------------------------------------------------------------------- 1 | from jakteristics.constants import FEATURE_NAMES 2 | from pathlib import Path 3 | import shutil 4 | 5 | import pytest 6 | import typer 7 | from typer.testing import CliRunner 8 | 9 | from jakteristics.__main__ import typer_main 10 | 11 | 12 | runner = CliRunner() 13 | app = typer.Typer() 14 | app.command()(typer_main) 15 | 16 | TEST_DATA = Path(__file__).parent / "data" 17 | 18 | 19 | def test_cli_help(): 20 | result = runner.invoke(app, ["--help"]) 21 | assert result.exit_code == 0 22 | 23 | 24 | def test_cli_show_features(): 25 | result = runner.invoke(app, ["--show-features"]) 26 | assert result.stdout == "\n".join(FEATURE_NAMES) + "\n" 27 | assert result.exit_code == 0 28 | 29 | 30 | def test_cli_invalid_feature_name(temp_dir): 31 | input_file = TEST_DATA / "test_0.02_seconde.las" 32 | output_file = temp_dir / "out.las" 33 | result = runner.invoke( 34 | app, 35 | [ 36 | str(input_file), 37 | str(output_file), 38 | "--search-radius", 39 | "0.15", 40 | "--feature", 41 | "bananas", 42 | ], 43 | ) 44 | assert result.exit_code == 2 45 | assert "invalid choice: bananas" in result.stdout 46 | 47 | 48 | def test_cli_run_basic(temp_dir): 49 | input_file = TEST_DATA / "test_0.02_seconde.las" 50 | output_file = temp_dir / "non_existing_folder" / "out.las" 51 | result = runner.invoke( 52 | app, 53 | [ 54 | str(input_file), 55 | str(output_file), 56 | "--search-radius", 57 | "0.15", 58 | "--num-threads", 59 | "4", 60 | ], 61 | ) 62 | assert result.exit_code == 0 63 | assert output_file.exists 64 | -------------------------------------------------------------------------------- /tests/test_features.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | 3 | import laspy 4 | import numpy as np 5 | import pytest 6 | 7 | import jakteristics 8 | from jakteristics import FEATURE_NAMES, extension, las_utils, utils 9 | 10 | 11 | data_dir = Path(__file__).parent / "data" 12 | 13 | 14 | def test_matmul_transposed(): 15 | points = np.random.rand(3, 4).astype("d") 16 | np_dot = np.dot(points, points.T) 17 | 18 | result = utils.py_matmul_transposed(points) 19 | 20 | assert np.allclose(np_dot, result) 21 | 22 | 23 | def test_substract_mean(): 24 | points = np.random.rand(3, 4).astype("d") 25 | expected = points - points.mean(axis=1)[:, None] 26 | result = np.asfortranarray(points.copy()) 27 | utils.substract_mean(result) 28 | 29 | assert np.allclose(expected, result) 30 | 31 | 32 | def test_covariance(): 33 | points = np.random.rand(3, 4).astype("d") 34 | np_cov = np.cov(points) 35 | 36 | cov = utils.py_covariance(points) 37 | 38 | assert np.allclose(np_cov, cov) 39 | 40 | 41 | def test_eigenvalues(): 42 | # --- given --- 43 | points = np.random.rand(3, 4).astype("d") 44 | np_cov = np.asfortranarray(np.cov(points).astype("d")) 45 | 46 | np_eigenvalues, np_eigenvectors = np.linalg.eig(np_cov) 47 | np_eigenvalues = np.abs(np_eigenvalues) 48 | 49 | # reorder eigenvectors before comparison 50 | argsort = list(reversed(np.argsort(np_eigenvalues))) 51 | np_eigenvectors = np.array([np_eigenvectors[:, i] for i in argsort]) 52 | 53 | # --- when --- 54 | eigenvalues, eigenvectors = utils.py_eigenvectors(np_cov) 55 | 56 | # flip eigenvectors that are in the opposite direction (for comparison) 57 | for i in range(3): 58 | same_sign = ( 59 | eigenvectors[i, 0] < 0 60 | and np_eigenvectors[i, 0] < 0 61 | or eigenvectors[i, 0] > 0 62 | and np_eigenvectors[i, 0] > 0 63 | ) 64 | if not same_sign: 65 | np_eigenvectors[i, :] *= -1 66 | 67 | # --- then --- 68 | assert np.allclose(eigenvalues, np_eigenvalues[argsort]) 69 | 70 | assert np.allclose(np_eigenvectors, eigenvectors, atol=1e-4) 71 | 72 | 73 | def test_compute_features(): 74 | n_points = 1000 75 | points = np.random.random((n_points, 3)) * 10 76 | 77 | features = extension.compute_features(points, 0.15, feature_names=FEATURE_NAMES) 78 | 79 | assert features.shape == (n_points, len(FEATURE_NAMES)) 80 | 81 | 82 | def test_compute_some_features(): 83 | input_path = data_dir / "test_0.02_seconde.las" 84 | xyz = las_utils.read_las_xyz(input_path) 85 | n_points = xyz.shape[0] 86 | all_features = extension.compute_features(xyz, 0.15, feature_names=FEATURE_NAMES) 87 | 88 | for name in FEATURE_NAMES: 89 | features = extension.compute_features(xyz, 0.15, feature_names=[name]) 90 | index = FEATURE_NAMES.index(name) 91 | 92 | assert features.shape == (n_points, 1) 93 | assert np.allclose(all_features[:, index], features.reshape(-1), equal_nan=True) 94 | 95 | 96 | def test_write_extra_dims(tmp_path): 97 | input_path = data_dir / "test_0.02_seconde.las" 98 | output_path = tmp_path / "test_output.las" 99 | 100 | xyz = las_utils.read_las_xyz(input_path) 101 | 102 | features = extension.compute_features(xyz, 0.15, feature_names=FEATURE_NAMES) 103 | 104 | las_utils.write_with_extra_dims(input_path, output_path, features, FEATURE_NAMES) 105 | 106 | output_features = [] 107 | with laspy.open(output_path, mode="r") as las: 108 | las_data = las.read() 109 | xyz_out = las_data.xyz 110 | for spec in las.header.point_format.extra_dimensions: 111 | name = spec.name.encode().replace(b"\x00", b"").decode() 112 | output_features.append(getattr(las_data, name)) 113 | 114 | output_features = np.vstack(output_features).T 115 | 116 | assert np.allclose(xyz, xyz_out) 117 | assert np.allclose(features, output_features, equal_nan=True) 118 | 119 | 120 | def test_not_contiguous(): 121 | points = np.random.random((3, 1000)).T 122 | 123 | features = jakteristics.compute_features(points, 0.15, feature_names=FEATURE_NAMES) 124 | 125 | assert features.shape == (1000, len(FEATURE_NAMES)) 126 | 127 | 128 | def test_wrong_shape(): 129 | points = np.random.random((3, 1000)) 130 | 131 | with pytest.raises(ValueError): 132 | extension.compute_features(points, 0.15, feature_names=FEATURE_NAMES) 133 | 134 | 135 | def test_nan(): 136 | points = np.random.random((3, 1000)).T 137 | 138 | # compute kdtree where points are not located 139 | kdtree = jakteristics.cKDTree((points + 2).copy()) 140 | 141 | features = jakteristics.compute_features( 142 | points, 0.15, kdtree=kdtree, feature_names=FEATURE_NAMES 143 | ) 144 | assert np.all(np.isnan(features)) 145 | 146 | 147 | def test_with_kdtree_not_same_point_count(): 148 | points = np.random.random((3, 1000)).T 149 | 150 | kdtree = jakteristics.cKDTree((points).copy()) 151 | features = jakteristics.compute_features( 152 | points[::100], 0.30, kdtree=kdtree, feature_names=FEATURE_NAMES 153 | ) 154 | 155 | assert not np.any(np.isnan(features)) 156 | 157 | assert features.shape == (10, len(FEATURE_NAMES)) 158 | -------------------------------------------------------------------------------- /tests/test_las_utils.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | 3 | import numpy as np 4 | import pytest 5 | 6 | from jakteristics import las_utils 7 | 8 | 9 | TEST_DATA = Path(__file__).parent / "data" 10 | 11 | 12 | def test_read_las(): 13 | input_file = TEST_DATA / "test_0.02_seconde.las" 14 | xyz = las_utils.read_las_xyz(input_file) 15 | assert xyz.shape == (10310, 3) 16 | assert xyz.dtype == np.float64 17 | 18 | 19 | def test_read_las_offset(): 20 | input_file = TEST_DATA / "test_0.02_seconde.las" 21 | xyz, offset = las_utils.read_las_xyz(input_file, with_offset=True) 22 | assert xyz.shape == (10310, 3) 23 | assert xyz.dtype == np.float32 24 | assert np.allclose(offset, np.array([362327.0, 5157620.0, 106.271])) 25 | 26 | 27 | def test_write_extra_dims_same_path(): 28 | input_file = TEST_DATA / "test_0.02_seconde.las" 29 | with pytest.raises(ValueError): 30 | las_utils.write_with_extra_dims( 31 | input_file, input_file, np.array([1, 2, 3]), ["test_dim"] 32 | ) 33 | 34 | 35 | def test_write_extra_dims_wrong_point_count(temp_dir): 36 | input_file = TEST_DATA / "test_0.02_seconde.las" 37 | output_file = temp_dir / "out.las" 38 | with pytest.raises(ValueError) as e: 39 | las_utils.write_with_extra_dims( 40 | input_file, output_file, np.array([1, 2, 3]).T, ["test_dim"] 41 | ) 42 | --------------------------------------------------------------------------------