fastpgv 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- fastpgv-0.1.0/.github/workflows/testpypi.yml +48 -0
- fastpgv-0.1.0/.github/workflows/wheels.yml +57 -0
- fastpgv-0.1.0/.gitignore +7 -0
- fastpgv-0.1.0/CMakeLists.txt +15 -0
- fastpgv-0.1.0/LICENSE +21 -0
- fastpgv-0.1.0/PKG-INFO +144 -0
- fastpgv-0.1.0/PORTING.md +26 -0
- fastpgv-0.1.0/PUBLISHING.md +117 -0
- fastpgv-0.1.0/README.md +112 -0
- fastpgv-0.1.0/THIRD_PARTY_NOTICES.md +12 -0
- fastpgv-0.1.0/cpp/bindings.cpp +24 -0
- fastpgv-0.1.0/cpp/sled.cpp +195 -0
- fastpgv-0.1.0/cpp/sled.h +51 -0
- fastpgv-0.1.0/examples/two_polygons.py +8 -0
- fastpgv-0.1.0/licenses/pybind11-LICENSE +29 -0
- fastpgv-0.1.0/pyproject.toml +46 -0
- fastpgv-0.1.0/src/fastpgv/__init__.py +75 -0
- fastpgv-0.1.0/tests/test_benchmark_reference.py +37 -0
- fastpgv-0.1.0/tests/test_fastpgv.py +115 -0
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
name: TestPyPI
|
|
2
|
+
on:
|
|
3
|
+
workflow_dispatch:
|
|
4
|
+
|
|
5
|
+
permissions:
|
|
6
|
+
contents: read
|
|
7
|
+
|
|
8
|
+
jobs:
|
|
9
|
+
build:
|
|
10
|
+
uses: ./.github/workflows/wheels.yml
|
|
11
|
+
publish:
|
|
12
|
+
needs: build
|
|
13
|
+
runs-on: ubuntu-22.04
|
|
14
|
+
environment:
|
|
15
|
+
name: testpypi
|
|
16
|
+
url: https://test.pypi.org/p/fastpgv
|
|
17
|
+
permissions:
|
|
18
|
+
id-token: write
|
|
19
|
+
steps:
|
|
20
|
+
- uses: actions/download-artifact@v4
|
|
21
|
+
with:
|
|
22
|
+
path: dist
|
|
23
|
+
merge-multiple: true
|
|
24
|
+
- uses: actions/setup-python@v5
|
|
25
|
+
with:
|
|
26
|
+
python-version: '3.12'
|
|
27
|
+
- run: python -m pip install twine
|
|
28
|
+
- run: python -m twine check dist/*
|
|
29
|
+
- uses: pypa/gh-action-pypi-publish@release/v1
|
|
30
|
+
with:
|
|
31
|
+
repository-url: https://test.pypi.org/legacy/
|
|
32
|
+
verify:
|
|
33
|
+
needs: publish
|
|
34
|
+
runs-on: ubuntu-22.04
|
|
35
|
+
steps:
|
|
36
|
+
- uses: actions/checkout@v4
|
|
37
|
+
- uses: actions/setup-python@v5
|
|
38
|
+
with:
|
|
39
|
+
python-version: '3.12'
|
|
40
|
+
- run: python -m pip install numpy
|
|
41
|
+
- name: Install published package (allow index propagation)
|
|
42
|
+
run: |
|
|
43
|
+
for attempt in 1 2 3 4 5; do
|
|
44
|
+
python -m pip install --index-url https://test.pypi.org/simple/ --only-binary=:all: --no-deps fastpgv==0.1.0 && break
|
|
45
|
+
sleep 20
|
|
46
|
+
done
|
|
47
|
+
python -c 'import fastpgv; assert fastpgv.__version__ == "0.1.0"'
|
|
48
|
+
- run: python examples/two_polygons.py
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
name: Build and Test
|
|
2
|
+
on:
|
|
3
|
+
push:
|
|
4
|
+
pull_request:
|
|
5
|
+
workflow_dispatch:
|
|
6
|
+
workflow_call:
|
|
7
|
+
|
|
8
|
+
permissions:
|
|
9
|
+
contents: read
|
|
10
|
+
|
|
11
|
+
jobs:
|
|
12
|
+
wheels:
|
|
13
|
+
strategy:
|
|
14
|
+
fail-fast: false
|
|
15
|
+
matrix:
|
|
16
|
+
include:
|
|
17
|
+
- os: ubuntu-22.04
|
|
18
|
+
arch: x86_64
|
|
19
|
+
- os: macos-14
|
|
20
|
+
arch: arm64
|
|
21
|
+
- os: macos-15-intel
|
|
22
|
+
arch: x86_64
|
|
23
|
+
- os: windows-2022
|
|
24
|
+
arch: AMD64
|
|
25
|
+
runs-on: ${{ matrix.os }}
|
|
26
|
+
steps:
|
|
27
|
+
- uses: actions/checkout@v4
|
|
28
|
+
- uses: pypa/cibuildwheel@v3.1
|
|
29
|
+
env:
|
|
30
|
+
CIBW_ARCHS: ${{ matrix.arch }}
|
|
31
|
+
CIBW_ENVIRONMENT_MACOS: MACOSX_DEPLOYMENT_TARGET=11.0
|
|
32
|
+
- uses: actions/upload-artifact@v4
|
|
33
|
+
with:
|
|
34
|
+
name: wheels-${{ matrix.os }}
|
|
35
|
+
path: wheelhouse/*.whl
|
|
36
|
+
source:
|
|
37
|
+
runs-on: ubuntu-22.04
|
|
38
|
+
steps:
|
|
39
|
+
- uses: actions/checkout@v4
|
|
40
|
+
- uses: actions/setup-python@v5
|
|
41
|
+
with:
|
|
42
|
+
python-version: '3.12'
|
|
43
|
+
- run: python -m pip install build twine
|
|
44
|
+
- run: python -m build --sdist
|
|
45
|
+
- run: python -m twine check dist/*
|
|
46
|
+
- name: Build and test outside the repository
|
|
47
|
+
run: |
|
|
48
|
+
mkdir /tmp/fastpgv-source
|
|
49
|
+
tar -xf dist/*.tar.gz -C /tmp/fastpgv-source
|
|
50
|
+
cd /tmp/fastpgv-source/fastpgv-0.1.0
|
|
51
|
+
python -m pip install '.[test]'
|
|
52
|
+
python -m pytest
|
|
53
|
+
python examples/two_polygons.py
|
|
54
|
+
- uses: actions/upload-artifact@v4
|
|
55
|
+
with:
|
|
56
|
+
name: source
|
|
57
|
+
path: dist/*.tar.gz
|
fastpgv-0.1.0/.gitignore
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
cmake_minimum_required(VERSION 3.18)
|
|
2
|
+
project(fastpgv LANGUAGES CXX)
|
|
3
|
+
set(PYBIND11_FINDPYTHON ON)
|
|
4
|
+
find_package(Python COMPONENTS Interpreter Development.Module REQUIRED)
|
|
5
|
+
find_package(pybind11 CONFIG REQUIRED)
|
|
6
|
+
find_package(Threads REQUIRED)
|
|
7
|
+
pybind11_add_module(_native cpp/bindings.cpp cpp/sled.cpp)
|
|
8
|
+
target_link_libraries(_native PRIVATE Threads::Threads)
|
|
9
|
+
target_compile_features(_native PRIVATE cxx_std_17)
|
|
10
|
+
if(MSVC)
|
|
11
|
+
target_compile_options(_native PRIVATE /W4 /fp:precise)
|
|
12
|
+
else()
|
|
13
|
+
target_compile_options(_native PRIVATE -Wall -Wextra -Wpedantic)
|
|
14
|
+
endif()
|
|
15
|
+
install(TARGETS _native LIBRARY DESTINATION fastpgv RUNTIME DESTINATION fastpgv)
|
fastpgv-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 SLED contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
fastpgv-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: fastpgv
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Weighted polygon-to-grid aggregation with the native SLED algorithm
|
|
5
|
+
Keywords: polygon,raster,geospatial,SLED,aggregation
|
|
6
|
+
Author: SLED contributors
|
|
7
|
+
License-Expression: MIT
|
|
8
|
+
License-File: LICENSE
|
|
9
|
+
License-File: THIRD_PARTY_NOTICES.md
|
|
10
|
+
License-File: licenses/pybind11-LICENSE
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: Intended Audience :: Science/Research
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
19
|
+
Classifier: Programming Language :: C++
|
|
20
|
+
Classifier: Topic :: Scientific/Engineering :: GIS
|
|
21
|
+
Requires-Python: >=3.10
|
|
22
|
+
Requires-Dist: numpy>=1.23
|
|
23
|
+
Provides-Extra: test
|
|
24
|
+
Requires-Dist: pytest>=8; extra == "test"
|
|
25
|
+
Requires-Dist: shapely>=2; extra == "test"
|
|
26
|
+
Provides-Extra: dev
|
|
27
|
+
Requires-Dist: pytest>=8; extra == "dev"
|
|
28
|
+
Requires-Dist: shapely>=2; extra == "dev"
|
|
29
|
+
Requires-Dist: build>=1.2; extra == "dev"
|
|
30
|
+
Requires-Dist: twine>=6; extra == "dev"
|
|
31
|
+
Description-Content-Type: text/markdown
|
|
32
|
+
|
|
33
|
+
# fastpgv
|
|
34
|
+
|
|
35
|
+
**fastpgv** is a lightweight Python wrapper for fast density computation for Polygon-to-Grid Visualization (PGV).
|
|
36
|
+
|
|
37
|
+
This package provides an efficient way to compute density results through a simple Python interface.
|
|
38
|
+
Compute a density value for every pixel from weighted polygons using the C++17 SLED (Sweep-Line Edge-Difference Aggregation) algorithm.
|
|
39
|
+
|
|
40
|
+
## Features
|
|
41
|
+
|
|
42
|
+
- One Python interface: `fastpgv.run(weighted_polygons, resolution)`.
|
|
43
|
+
- Weighted polygons with explicit vertex coordinates.
|
|
44
|
+
- The polygons should be non-overlapping.
|
|
45
|
+
- C++ computation backend with the Python interface.
|
|
46
|
+
|
|
47
|
+
## Installation
|
|
48
|
+
|
|
49
|
+
From the `fastpgv/` source directory:
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
python -m pip install .
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
After version 0.1.0 is successfully published to production PyPI:
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
python -m pip install fastpgv==0.1.0
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Publication is pending. The prepared wheel is for CPython 3.12 on macOS 11+
|
|
62
|
+
Apple Silicon (arm64). Other environments need to build the source distribution
|
|
63
|
+
with a C++17 compiler. Cross-platform wheels are not included in this release.
|
|
64
|
+
|
|
65
|
+
## Requirements
|
|
66
|
+
|
|
67
|
+
- Python 3.10 or newer.
|
|
68
|
+
- NumPy, installed automatically by pip.
|
|
69
|
+
- A C++17 compiler for source builds. pip installs the Python build dependencies.
|
|
70
|
+
|
|
71
|
+
Java, DuckDB, APRIL, Boost, and the original benchmark repository are not required.
|
|
72
|
+
|
|
73
|
+
## Quick Example
|
|
74
|
+
|
|
75
|
+
```python
|
|
76
|
+
import fastpgv
|
|
77
|
+
|
|
78
|
+
# Polygons contain ordered vertex coordinates, not just identifiers.
|
|
79
|
+
polygon_a = {
|
|
80
|
+
"type": "Polygon",
|
|
81
|
+
"coordinates": [[[0, 0], [1, 0], [1, 1], [0, 1], [0, 0]]],
|
|
82
|
+
}
|
|
83
|
+
polygon_b = {
|
|
84
|
+
"type": "Polygon",
|
|
85
|
+
"coordinates": [[[1, 0], [2, 0], [2, 1], [1, 1], [1, 0]]],
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
density = fastpgv.run(
|
|
89
|
+
[(polygon_a, 2.0), (polygon_b, 3.0)],
|
|
90
|
+
resolution=(2, 1),
|
|
91
|
+
)
|
|
92
|
+
print(density)
|
|
93
|
+
# [[2. 3.]]
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
## Inputs and Output
|
|
97
|
+
|
|
98
|
+
Both arguments are required:
|
|
99
|
+
|
|
100
|
+
| Argument | Meaning |
|
|
101
|
+
|---|---|
|
|
102
|
+
| `weighted_polygons` | Nonempty iterable of `(geometry, weight)` pairs |
|
|
103
|
+
| `resolution` | Positive integer `(X, Y)`: columns and rows |
|
|
104
|
+
|
|
105
|
+
Geometry must contain ordered vertices as a GeoJSON `Polygon` or `MultiPolygon`
|
|
106
|
+
dictionary, or expose `__geo_interface__` (for example, a Shapely polygon).
|
|
107
|
+
Consecutive vertices define edges automatically; open rings are closed internally.
|
|
108
|
+
No separate edge input is required or accepted. The first ring is the exterior;
|
|
109
|
+
additional rings are holes. MultiPolygon components share one weight.
|
|
110
|
+
|
|
111
|
+
Weights must be explicit finite numbers. Negative and zero weights are supported.
|
|
112
|
+
The supported use case is non-overlapping polygonal inputs: interiors must not
|
|
113
|
+
overlap, but shared edges and vertices are allowed. Overlaps are not automatically
|
|
114
|
+
checked; validate this requirement before calling `run`.
|
|
115
|
+
The grid covers the combined minimum
|
|
116
|
+
bounding rectangle of **all** supplied geometries, including zero-weight ones.
|
|
117
|
+
|
|
118
|
+
`run` returns a `numpy.ndarray` directly, with dtype `float64` and shape `(Y, X)`.
|
|
119
|
+
`density[v, u]` is the value of the pixel in row `v` and column `u`. Row zero is
|
|
120
|
+
the top row at `y_max`; columns run from `x_min` to `x_max`.
|
|
121
|
+
|
|
122
|
+
The function returns data directly; it does not write files or render a visualization.
|
|
123
|
+
|
|
124
|
+
## Notes
|
|
125
|
+
|
|
126
|
+
- Each pixel value is the sum of `weight * polygon_pixel_intersection_area`:
|
|
127
|
+
**weighted area, not area-normalized density**.
|
|
128
|
+
- Coordinates are planar, with no CRS transformation or geodesic calculation.
|
|
129
|
+
Project geographic coordinates first when metric areas are required.
|
|
130
|
+
- Supply valid polygon topology. Ring orientation is corrected, but invalid
|
|
131
|
+
holes and self-intersections are not repaired. Empty input is rejected.
|
|
132
|
+
- Numerical tests use absolute tolerance `1e-9` or relative tolerance `1e-12`.
|
|
133
|
+
Floating-point results can differ across platforms; extreme coordinates or
|
|
134
|
+
weights may require rescaling and application-specific error analysis.
|
|
135
|
+
- The complete output array requires `8 * X * Y` bytes, plus polygon-edge and
|
|
136
|
+
working storage. Choose a resolution that fits available memory.
|
|
137
|
+
|
|
138
|
+
## License and Further Information
|
|
139
|
+
|
|
140
|
+
MIT licensed, by **SLED contributors**.
|
|
141
|
+
|
|
142
|
+
The source archive includes `PUBLISHING.md` (build and upload instructions),
|
|
143
|
+
`PORTING.md` (implementation provenance), and `THIRD_PARTY_NOTICES.md`
|
|
144
|
+
(dependency notices).
|
fastpgv-0.1.0/PORTING.md
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# Native engine provenance
|
|
2
|
+
|
|
3
|
+
Version 0.1.0 is a standalone adaptation of the SLED implementation in
|
|
4
|
+
`pgv-c-code/baseline/sled.cpp`, reviewed on 2026-09-07. The source workspace
|
|
5
|
+
had no committed revision to identify. No sibling files are needed to build.
|
|
6
|
+
The reference file's SHA-256 was
|
|
7
|
+
`0bf6fc00ae4a03633bf97e0b85939c1f766de206544b53477d8d0d93b25a4e4d`.
|
|
8
|
+
|
|
9
|
+
Retained algorithm: corrected outer/hole orientation, downward signed edges,
|
|
10
|
+
exact coordinate-pair cancellation, row scheduling, vertical-grid fragment
|
|
11
|
+
splitting, trapezoidal boundary integration, and row difference prefix sums.
|
|
12
|
+
The density-only revision removes candidate-count buffers and instrumentation.
|
|
13
|
+
|
|
14
|
+
Adaptation changes: Python validates weighted geometry pairs with vertices;
|
|
15
|
+
C++ takes coordinate arrays instead of JSON strings. A Builder and one-row
|
|
16
|
+
Sweep replace benchmark context/output dependencies. Translated shoelace
|
|
17
|
+
coordinates reduce orientation cancellation; grid-index conversion clamps
|
|
18
|
+
before integer conversion. Native buffers use long double as in SLED; output
|
|
19
|
+
is float64. Interactive updates, benchmark timers, DuckDB, Boost, and APRIL
|
|
20
|
+
are excluded. The public function is `run(weighted_polygons, resolution)`;
|
|
21
|
+
bounds are inferred from input geometries. File readers and public row streaming
|
|
22
|
+
are excluded. This snapshot is maintained independently, not auto-synchronized.
|
|
23
|
+
|
|
24
|
+
Tests use analytical fixtures and Shapely intersection areas. Repository-local
|
|
25
|
+
validation also compares densities against the original SLED and scan methods;
|
|
26
|
+
their executable is not a package or test dependency.
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
# Build and Publish
|
|
2
|
+
|
|
3
|
+
## Release Status
|
|
4
|
+
|
|
5
|
+
Package version: 0.1.0. TestPyPI and production PyPI publication have not yet
|
|
6
|
+
occurred. A successful name lookup does not reserve a project name. Do not
|
|
7
|
+
advertise `pip install fastpgv` from production PyPI until that release exists.
|
|
8
|
+
|
|
9
|
+
## Manual Production Upload
|
|
10
|
+
|
|
11
|
+
No GitHub repository is required. Upload only the freshly validated release
|
|
12
|
+
files, not the older files in `dist/`. From the release directory containing
|
|
13
|
+
the wheel, source archive, and `SHA256SUMS`:
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
python -m pip install --upgrade twine
|
|
17
|
+
shasum -a 256 -c SHA256SUMS
|
|
18
|
+
python -m twine check --strict fastpgv-0.1.0-cp312-cp312-macosx_11_0_arm64.whl fastpgv-0.1.0.tar.gz
|
|
19
|
+
python -m twine upload --repository pypi fastpgv-0.1.0-cp312-cp312-macosx_11_0_arm64.whl fastpgv-0.1.0.tar.gz
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Use a production account at pypi.org with a verified email and two-factor
|
|
23
|
+
authentication configured. Create a production PyPI API token locally. For a
|
|
24
|
+
first project upload, an account-scoped token may be needed; after creating the
|
|
25
|
+
project, replace it with a project-scoped token. At Twine's prompts, enter
|
|
26
|
+
`__token__` as the username and paste the token at the hidden password prompt.
|
|
27
|
+
Never include the token in command arguments, files, or chat. TestPyPI accounts
|
|
28
|
+
and tokens are separate and cannot authenticate to production PyPI.
|
|
29
|
+
|
|
30
|
+
Before uploading, check https://pypi.org/project/fastpgv/ again. A missing project
|
|
31
|
+
does not reserve the name or guarantee permission. If the name/version is no
|
|
32
|
+
longer available, stop and resolve it instead of renaming artifacts manually.
|
|
33
|
+
Uploaded filenames cannot be overwritten. Do not upload validation reports or
|
|
34
|
+
checksum files: the command above names only the two distribution files.
|
|
35
|
+
|
|
36
|
+
This release includes only a CPython 3.12 macOS 11+ arm64 wheel and a source
|
|
37
|
+
archive. Other Python/platform combinations require a C++17 source build and
|
|
38
|
+
are not confirmed by the local macOS validation. No cross-platform CI results
|
|
39
|
+
are claimed.
|
|
40
|
+
|
|
41
|
+
After uploading, verify in a new CPython 3.12 environment on macOS Apple Silicon:
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
python3.12 -m venv /tmp/fastpgv-pypi-check
|
|
45
|
+
/tmp/fastpgv-pypi-check/bin/python -m pip install --index-url https://pypi.org/simple/ --only-binary=:all: fastpgv==0.1.0
|
|
46
|
+
/tmp/fastpgv-pypi-check/bin/python -c 'import fastpgv; p={"type":"Polygon","coordinates":[[[0,0],[1,0],[1,1],[0,1],[0,0]]]}; assert fastpgv.run([(p,2)],(1,1)).tolist()==[[2.0]]; print("PyPI installation verified")'
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Also run `examples/two_polygons.py` from the extracted source archive using that
|
|
50
|
+
environment. Expected output is `[[2. 3.]]`. Publication and this post-upload
|
|
51
|
+
check remain manual steps until the upload succeeds.
|
|
52
|
+
|
|
53
|
+
## Optional GitHub Builds
|
|
54
|
+
|
|
55
|
+
The workflows in this folder assume **fastpgv is the repository root**. Place
|
|
56
|
+
this standalone folder in its own GitHub repository to activate them; workflows
|
|
57
|
+
nested inside a parent repository are not automatically executed by GitHub.
|
|
58
|
+
|
|
59
|
+
## Local Validation
|
|
60
|
+
|
|
61
|
+
Use Python 3.10 or newer and a C++17 compiler:
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
python -m pip install build twine
|
|
65
|
+
python -m build
|
|
66
|
+
python -m twine check dist/*
|
|
67
|
+
python -m venv /tmp/fastpgv-wheel-test
|
|
68
|
+
/tmp/fastpgv-wheel-test/bin/python -m pip install dist/*.whl pytest shapely
|
|
69
|
+
/tmp/fastpgv-wheel-test/bin/python -m pytest tests
|
|
70
|
+
/tmp/fastpgv-wheel-test/bin/python examples/two_polygons.py
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
On Windows use the environment's `Scripts/python.exe` instead of `bin/python`.
|
|
74
|
+
Also extract the source archive to a directory outside the original repository
|
|
75
|
+
and run `python -m build --wheel` there. Check that no sibling source folders
|
|
76
|
+
or installed benchmark binary are needed. Build outputs and validation results
|
|
77
|
+
are local only; cross-platform support is confirmed by CI, not inferred from
|
|
78
|
+
one macOS build.
|
|
79
|
+
|
|
80
|
+
The wheel workflow targets standard CPython 3.10–3.14 on Linux x86_64,
|
|
81
|
+
macOS arm64/x86_64, and Windows x86_64. It runs tests against installed wheels,
|
|
82
|
+
including translated-coordinate and geometry-oracle tests on Windows, where
|
|
83
|
+
`long double` has different precision. Free-threaded CPython and musl Linux
|
|
84
|
+
are not targeted in this release.
|
|
85
|
+
|
|
86
|
+
## Trusted Publishing to TestPyPI
|
|
87
|
+
|
|
88
|
+
1. Create a TestPyPI account and enable its required account security settings.
|
|
89
|
+
2. Create/configure the standalone GitHub repository. No repository URL or
|
|
90
|
+
publisher identity is embedded in this source snapshot.
|
|
91
|
+
3. On TestPyPI, register a pending Trusted Publisher for `fastpgv`, using your
|
|
92
|
+
GitHub owner/repository, workflow `testpypi.yml`, and environment `testpypi`.
|
|
93
|
+
If the name is unavailable, resolve the name before publishing.
|
|
94
|
+
4. Create the matching GitHub environment and require approval for publication.
|
|
95
|
+
5. Run the **TestPyPI** workflow manually. It first builds and tests all wheels
|
|
96
|
+
plus the source archive, checks metadata, then uploads through OIDC.
|
|
97
|
+
6. Review the post-upload installation check and TestPyPI project page.
|
|
98
|
+
|
|
99
|
+
Never put API tokens in source files, workflow YAML, or chat. The workflow uses
|
|
100
|
+
short-lived Trusted Publishing credentials. An upload is public and versioned;
|
|
101
|
+
an existing filename cannot be overwritten. Fixes require a new version.
|
|
102
|
+
|
|
103
|
+
Manual post-upload verification in a new environment:
|
|
104
|
+
|
|
105
|
+
```bash
|
|
106
|
+
python -m pip install numpy
|
|
107
|
+
python -m pip install --index-url https://test.pypi.org/simple/ \
|
|
108
|
+
--only-binary=:all: --no-deps fastpgv==0.1.0
|
|
109
|
+
python examples/two_polygons.py
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
The workflow above targets TestPyPI only. For the manual production release,
|
|
113
|
+
use the explicit production upload command earlier in this guide.
|
|
114
|
+
|
|
115
|
+
Official references: [Trusted Publishing](https://docs.pypi.org/trusted-publishers/),
|
|
116
|
+
[packaging binary extensions](https://packaging.python.org/en/latest/guides/packaging-binary-extensions/),
|
|
117
|
+
[cibuildwheel](https://cibuildwheel.pypa.io/).
|
fastpgv-0.1.0/README.md
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
# fastpgv
|
|
2
|
+
|
|
3
|
+
**fastpgv** is a lightweight Python wrapper for fast density computation for Polygon-to-Grid Visualization (PGV).
|
|
4
|
+
|
|
5
|
+
This package provides an efficient way to compute density results through a simple Python interface.
|
|
6
|
+
Compute a density value for every pixel from weighted polygons using the C++17 SLED (Sweep-Line Edge-Difference Aggregation) algorithm.
|
|
7
|
+
|
|
8
|
+
## Features
|
|
9
|
+
|
|
10
|
+
- One Python interface: `fastpgv.run(weighted_polygons, resolution)`.
|
|
11
|
+
- Weighted polygons with explicit vertex coordinates.
|
|
12
|
+
- The polygons should be non-overlapping.
|
|
13
|
+
- C++ computation backend with the Python interface.
|
|
14
|
+
|
|
15
|
+
## Installation
|
|
16
|
+
|
|
17
|
+
From the `fastpgv/` source directory:
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
python -m pip install .
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
After version 0.1.0 is successfully published to production PyPI:
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
python -m pip install fastpgv==0.1.0
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Publication is pending. The prepared wheel is for CPython 3.12 on macOS 11+
|
|
30
|
+
Apple Silicon (arm64). Other environments need to build the source distribution
|
|
31
|
+
with a C++17 compiler. Cross-platform wheels are not included in this release.
|
|
32
|
+
|
|
33
|
+
## Requirements
|
|
34
|
+
|
|
35
|
+
- Python 3.10 or newer.
|
|
36
|
+
- NumPy, installed automatically by pip.
|
|
37
|
+
- A C++17 compiler for source builds. pip installs the Python build dependencies.
|
|
38
|
+
|
|
39
|
+
Java, DuckDB, APRIL, Boost, and the original benchmark repository are not required.
|
|
40
|
+
|
|
41
|
+
## Quick Example
|
|
42
|
+
|
|
43
|
+
```python
|
|
44
|
+
import fastpgv
|
|
45
|
+
|
|
46
|
+
# Polygons contain ordered vertex coordinates, not just identifiers.
|
|
47
|
+
polygon_a = {
|
|
48
|
+
"type": "Polygon",
|
|
49
|
+
"coordinates": [[[0, 0], [1, 0], [1, 1], [0, 1], [0, 0]]],
|
|
50
|
+
}
|
|
51
|
+
polygon_b = {
|
|
52
|
+
"type": "Polygon",
|
|
53
|
+
"coordinates": [[[1, 0], [2, 0], [2, 1], [1, 1], [1, 0]]],
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
density = fastpgv.run(
|
|
57
|
+
[(polygon_a, 2.0), (polygon_b, 3.0)],
|
|
58
|
+
resolution=(2, 1),
|
|
59
|
+
)
|
|
60
|
+
print(density)
|
|
61
|
+
# [[2. 3.]]
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## Inputs and Output
|
|
65
|
+
|
|
66
|
+
Both arguments are required:
|
|
67
|
+
|
|
68
|
+
| Argument | Meaning |
|
|
69
|
+
|---|---|
|
|
70
|
+
| `weighted_polygons` | Nonempty iterable of `(geometry, weight)` pairs |
|
|
71
|
+
| `resolution` | Positive integer `(X, Y)`: columns and rows |
|
|
72
|
+
|
|
73
|
+
Geometry must contain ordered vertices as a GeoJSON `Polygon` or `MultiPolygon`
|
|
74
|
+
dictionary, or expose `__geo_interface__` (for example, a Shapely polygon).
|
|
75
|
+
Consecutive vertices define edges automatically; open rings are closed internally.
|
|
76
|
+
No separate edge input is required or accepted. The first ring is the exterior;
|
|
77
|
+
additional rings are holes. MultiPolygon components share one weight.
|
|
78
|
+
|
|
79
|
+
Weights must be explicit finite numbers. Negative and zero weights are supported.
|
|
80
|
+
The supported use case is non-overlapping polygonal inputs: interiors must not
|
|
81
|
+
overlap, but shared edges and vertices are allowed. Overlaps are not automatically
|
|
82
|
+
checked; validate this requirement before calling `run`.
|
|
83
|
+
The grid covers the combined minimum
|
|
84
|
+
bounding rectangle of **all** supplied geometries, including zero-weight ones.
|
|
85
|
+
|
|
86
|
+
`run` returns a `numpy.ndarray` directly, with dtype `float64` and shape `(Y, X)`.
|
|
87
|
+
`density[v, u]` is the value of the pixel in row `v` and column `u`. Row zero is
|
|
88
|
+
the top row at `y_max`; columns run from `x_min` to `x_max`.
|
|
89
|
+
|
|
90
|
+
The function returns data directly; it does not write files or render a visualization.
|
|
91
|
+
|
|
92
|
+
## Notes
|
|
93
|
+
|
|
94
|
+
- Each pixel value is the sum of `weight * polygon_pixel_intersection_area`:
|
|
95
|
+
**weighted area, not area-normalized density**.
|
|
96
|
+
- Coordinates are planar, with no CRS transformation or geodesic calculation.
|
|
97
|
+
Project geographic coordinates first when metric areas are required.
|
|
98
|
+
- Supply valid polygon topology. Ring orientation is corrected, but invalid
|
|
99
|
+
holes and self-intersections are not repaired. Empty input is rejected.
|
|
100
|
+
- Numerical tests use absolute tolerance `1e-9` or relative tolerance `1e-12`.
|
|
101
|
+
Floating-point results can differ across platforms; extreme coordinates or
|
|
102
|
+
weights may require rescaling and application-specific error analysis.
|
|
103
|
+
- The complete output array requires `8 * X * Y` bytes, plus polygon-edge and
|
|
104
|
+
working storage. Choose a resolution that fits available memory.
|
|
105
|
+
|
|
106
|
+
## License and Further Information
|
|
107
|
+
|
|
108
|
+
MIT licensed, by **SLED contributors**.
|
|
109
|
+
|
|
110
|
+
The source archive includes `PUBLISHING.md` (build and upload instructions),
|
|
111
|
+
`PORTING.md` (implementation provenance), and `THIRD_PARTY_NOTICES.md`
|
|
112
|
+
(dependency notices).
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# Dependencies and notices
|
|
2
|
+
|
|
3
|
+
The SLED implementation is distributed under the MIT license in LICENSE.
|
|
4
|
+
Native bindings use pybind11 (BSD-3-Clause); its notice is reproduced in
|
|
5
|
+
`licenses/pybind11-LICENSE`. NumPy is a separately installed runtime dependency
|
|
6
|
+
under its own BSD license; it is not bundled here. Build tools scikit-build-core,
|
|
7
|
+
CMake, and a C++17 compiler are installed separately. Test tools pytest and
|
|
8
|
+
Shapely are optional and are not runtime dependencies.
|
|
9
|
+
|
|
10
|
+
No DuckDB, APRIL, Boost, private datasets, or benchmark outputs are included.
|
|
11
|
+
CMake does not fetch source files from the network. A source build still needs
|
|
12
|
+
the declared Python build dependencies, normally provisioned by pip.
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
#include "sled.h"
|
|
2
|
+
#include <pybind11/numpy.h>
|
|
3
|
+
#include <pybind11/pybind11.h>
|
|
4
|
+
#include <pybind11/stl.h>
|
|
5
|
+
#include <algorithm>
|
|
6
|
+
#include <limits>
|
|
7
|
+
|
|
8
|
+
namespace py = pybind11;
|
|
9
|
+
PYBIND11_MODULE(_native, module) {
|
|
10
|
+
module.attr("long_double_bits") = std::numeric_limits<long double>::digits;
|
|
11
|
+
py::class_<fastpgv::Sweep>(module, "Sweep")
|
|
12
|
+
.def("next_row", [](fastpgv::Sweep& sweep) {
|
|
13
|
+
fastpgv::Row row;
|
|
14
|
+
{ py::gil_scoped_release release; row = sweep.next(); }
|
|
15
|
+
if (row.v < 0) throw py::stop_iteration();
|
|
16
|
+
py::array_t<double> density(row.density.size());
|
|
17
|
+
std::copy(row.density.begin(), row.density.end(), density.mutable_data());
|
|
18
|
+
return density;
|
|
19
|
+
});
|
|
20
|
+
py::class_<fastpgv::Builder>(module, "Builder")
|
|
21
|
+
.def(py::init<>())
|
|
22
|
+
.def("add", &fastpgv::Builder::add, py::call_guard<py::gil_scoped_release>())
|
|
23
|
+
.def("finish", &fastpgv::Builder::finish, py::call_guard<py::gil_scoped_release>());
|
|
24
|
+
}
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
// Adapted from pgv-c-code/baseline/sled.cpp; see PORTING.md.
|
|
2
|
+
#include "sled.h"
|
|
3
|
+
|
|
4
|
+
#include <algorithm>
|
|
5
|
+
#include <cmath>
|
|
6
|
+
#include <limits>
|
|
7
|
+
#include <stdexcept>
|
|
8
|
+
#include <tuple>
|
|
9
|
+
|
|
10
|
+
namespace fastpgv {
|
|
11
|
+
namespace {
|
|
12
|
+
auto key(const Edge& e) { return std::tie(e.xu, e.yu, e.xl, e.yl); }
|
|
13
|
+
|
|
14
|
+
long double snap(long double value) {
|
|
15
|
+
const auto nearest = std::round(value);
|
|
16
|
+
const auto tolerance = 64.0L * std::numeric_limits<long double>::epsilon() *
|
|
17
|
+
std::max(1.0L, std::abs(value));
|
|
18
|
+
return std::abs(value - nearest) <= tolerance ? nearest : value;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
long double interpolate(const Edge& e, long double y) {
|
|
22
|
+
const auto t = (static_cast<long double>(e.yu) - y) /
|
|
23
|
+
(static_cast<long double>(e.yu) - e.yl);
|
|
24
|
+
return e.xu + (static_cast<long double>(e.xl) - e.xu) * t;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Clamp before integer conversion, including edges far outside a custom bbox.
|
|
28
|
+
int bounded_index(long double position, int maximum) {
|
|
29
|
+
return static_cast<int>(std::clamp(position, 0.0L,
|
|
30
|
+
static_cast<long double>(maximum)));
|
|
31
|
+
}
|
|
32
|
+
} // namespace
|
|
33
|
+
|
|
34
|
+
void Builder::add(const Geometry& geometry, double weight) {
|
|
35
|
+
std::lock_guard<std::mutex> lock(mutex_);
|
|
36
|
+
if (finished_) throw std::logic_error("builder has already been consumed");
|
|
37
|
+
if (!std::isfinite(weight)) throw std::invalid_argument("weight must be finite");
|
|
38
|
+
if (geometry.empty()) throw std::invalid_argument("geometry must not be empty");
|
|
39
|
+
std::vector<Edge> additions;
|
|
40
|
+
Bounds local{};
|
|
41
|
+
bool first = true;
|
|
42
|
+
for (const auto& polygon : geometry) {
|
|
43
|
+
if (polygon.empty()) throw std::invalid_argument("polygon requires an outer ring");
|
|
44
|
+
for (std::size_t r = 0; r < polygon.size(); ++r) {
|
|
45
|
+
std::vector<Point> ring;
|
|
46
|
+
for (const auto& point : polygon[r]) {
|
|
47
|
+
if (!std::isfinite(point[0]) || !std::isfinite(point[1]))
|
|
48
|
+
throw std::invalid_argument("coordinates must be finite");
|
|
49
|
+
if (first) { local = {point[0], point[1], point[0], point[1]}; first = false; }
|
|
50
|
+
local[0] = std::min(local[0], point[0]);
|
|
51
|
+
local[1] = std::min(local[1], point[1]);
|
|
52
|
+
local[2] = std::max(local[2], point[0]);
|
|
53
|
+
local[3] = std::max(local[3], point[1]);
|
|
54
|
+
if (ring.empty() || ring.back() != point) ring.push_back(point);
|
|
55
|
+
}
|
|
56
|
+
if (ring.size() < 3) throw std::invalid_argument("ring requires three distinct vertices");
|
|
57
|
+
if (ring.front() != ring.back()) ring.push_back(ring.front());
|
|
58
|
+
if (ring.size() < 4) throw std::invalid_argument("degenerate ring");
|
|
59
|
+
// Translation reduces cancellation on platforms where long double is double.
|
|
60
|
+
long double area = 0;
|
|
61
|
+
for (std::size_t i = 0; i + 1 < ring.size(); ++i) {
|
|
62
|
+
const long double ax = static_cast<long double>(ring[i][0]) - ring[0][0];
|
|
63
|
+
const long double ay = static_cast<long double>(ring[i][1]) - ring[0][1];
|
|
64
|
+
const long double bx = static_cast<long double>(ring[i+1][0]) - ring[0][0];
|
|
65
|
+
const long double by = static_cast<long double>(ring[i+1][1]) - ring[0][1];
|
|
66
|
+
area += ax * by - bx * ay;
|
|
67
|
+
}
|
|
68
|
+
if (!std::isfinite(area) || area == 0)
|
|
69
|
+
throw std::invalid_argument("ring has zero or non-finite signed area");
|
|
70
|
+
if ((area > 0) != (r == 0)) std::reverse(ring.begin(), ring.end());
|
|
71
|
+
for (std::size_t i = 0; i + 1 < ring.size(); ++i) {
|
|
72
|
+
const auto& a = ring[i]; const auto& b = ring[i+1];
|
|
73
|
+
if (a[1] == b[1]) continue;
|
|
74
|
+
if (a[1] > b[1]) additions.push_back({a[0], a[1], b[0], b[1], weight});
|
|
75
|
+
else additions.push_back({b[0], b[1], a[0], a[1], -static_cast<long double>(weight)});
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
edges_.insert(edges_.end(), additions.begin(), additions.end());
|
|
80
|
+
if (!have_bounds_) { bounds_ = local; have_bounds_ = true; }
|
|
81
|
+
else {
|
|
82
|
+
bounds_[0] = std::min(bounds_[0], local[0]); bounds_[1] = std::min(bounds_[1], local[1]);
|
|
83
|
+
bounds_[2] = std::max(bounds_[2], local[2]); bounds_[3] = std::max(bounds_[3], local[3]);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
std::unique_ptr<Sweep> Builder::finish(int width, int height) {
|
|
88
|
+
std::lock_guard<std::mutex> lock(mutex_);
|
|
89
|
+
if (finished_) throw std::logic_error("builder has already been consumed");
|
|
90
|
+
if (!have_bounds_) throw std::invalid_argument("weighted_polygons must not be empty");
|
|
91
|
+
const Bounds bounds = bounds_;
|
|
92
|
+
for (double value : bounds)
|
|
93
|
+
if (!std::isfinite(value)) throw std::invalid_argument("bounds must be finite");
|
|
94
|
+
if (width <= 0 || height <= 0 || bounds[2] <= bounds[0] || bounds[3] <= bounds[1])
|
|
95
|
+
throw std::invalid_argument("resolution and bounds must have positive extent");
|
|
96
|
+
if (width == std::numeric_limits<int>::max() || height == std::numeric_limits<int>::max())
|
|
97
|
+
throw std::invalid_argument("resolution exceeds native index capacity");
|
|
98
|
+
for (int axis = 0; axis < 2; ++axis) {
|
|
99
|
+
double span = bounds[axis+2] - bounds[axis];
|
|
100
|
+
double step = span / (axis == 0 ? width : height);
|
|
101
|
+
if (!std::isfinite(span) || !(step > 0) ||
|
|
102
|
+
bounds[axis] + step == bounds[axis] || bounds[axis+2] - step == bounds[axis+2])
|
|
103
|
+
throw std::invalid_argument("grid spacing is not representable at these coordinates");
|
|
104
|
+
}
|
|
105
|
+
std::stable_sort(edges_.begin(), edges_.end(), [](const Edge& a, const Edge& b) { return key(a) < key(b); });
|
|
106
|
+
std::size_t write = 0;
|
|
107
|
+
for (std::size_t i = 0; i < edges_.size();) {
|
|
108
|
+
auto edge = edges_[i++];
|
|
109
|
+
while (i < edges_.size() && key(edge) == key(edges_[i])) edge.weight += edges_[i++].weight;
|
|
110
|
+
if (!std::isfinite(edge.weight)) throw std::overflow_error("edge coefficient overflow");
|
|
111
|
+
if (edge.weight != 0) edges_[write++] = edge;
|
|
112
|
+
}
|
|
113
|
+
edges_.resize(write);
|
|
114
|
+
edges_.shrink_to_fit();
|
|
115
|
+
finished_ = true;
|
|
116
|
+
return std::make_unique<Sweep>(std::move(edges_), bounds, width, height);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
Sweep::Sweep(std::vector<Edge> edges, Bounds bounds, int width, int height)
|
|
120
|
+
: edges_(std::move(edges)), bounds_(bounds), width_(width), height_(height),
|
|
121
|
+
dx_((bounds[2]-bounds[0])/width), dy_((bounds[3]-bounds[1])/height),
|
|
122
|
+
last_rows_(edges_.size(), -1), offsets_(static_cast<std::size_t>(height)+1, 0),
|
|
123
|
+
boundary_(width), difference_(static_cast<std::size_t>(width)+1) {
|
|
124
|
+
std::vector<int> first_rows(edges_.size(), -1);
|
|
125
|
+
for (std::size_t i = 0; i < edges_.size(); ++i) {
|
|
126
|
+
auto top = std::min(static_cast<long double>(edges_[i].yu), static_cast<long double>(bounds[3]));
|
|
127
|
+
auto bottom = std::max(static_cast<long double>(edges_[i].yl), static_cast<long double>(bounds[1]));
|
|
128
|
+
if (!(bottom < top)) continue;
|
|
129
|
+
int first = bounded_index(std::floor(snap((bounds[3]-top)/dy_)), height-1);
|
|
130
|
+
int last = bounded_index(std::ceil(snap((bounds[3]-bottom)/dy_))-1, height-1);
|
|
131
|
+
first_rows[i] = first; last_rows_[i] = last; ++offsets_[first+1];
|
|
132
|
+
}
|
|
133
|
+
for (int row = 1; row <= height; ++row) offsets_[row] += offsets_[row-1];
|
|
134
|
+
entries_.resize(offsets_.back());
|
|
135
|
+
auto cursors = offsets_;
|
|
136
|
+
for (std::size_t i = 0; i < edges_.size(); ++i)
|
|
137
|
+
if (first_rows[i] >= 0) entries_[cursors[first_rows[i]]++] = i;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
void Sweep::accumulate(const Edge& edge, int row) {
|
|
141
|
+
long double top = std::min(static_cast<long double>(edge.yu),
|
|
142
|
+
static_cast<long double>(bounds_[3] - row * dy_));
|
|
143
|
+
long double bottom = std::max(static_cast<long double>(edge.yl),
|
|
144
|
+
static_cast<long double>(bounds_[3] - (row+1) * dy_));
|
|
145
|
+
if (!(bottom < top)) return;
|
|
146
|
+
auto xt = interpolate(edge, top), xb = interpolate(edge, bottom);
|
|
147
|
+
std::vector<long double> parameters{0, 1};
|
|
148
|
+
if (xt != xb) {
|
|
149
|
+
int first = bounded_index(std::floor(snap((std::min(xt,xb)-bounds_[0])/dx_))+1, width_);
|
|
150
|
+
int last = bounded_index(std::ceil(snap((std::max(xt,xb)-bounds_[0])/dx_))-1, width_);
|
|
151
|
+
for (int line = first; line <= last; ++line) {
|
|
152
|
+
auto t = (static_cast<long double>(bounds_[0]+line*dx_) - xt)/(xb-xt);
|
|
153
|
+
if (t > 0 && t < 1) parameters.push_back(t);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
std::sort(parameters.begin(), parameters.end());
|
|
157
|
+
parameters.erase(std::unique(parameters.begin(), parameters.end()), parameters.end());
|
|
158
|
+
for (std::size_t i = 0; i+1 < parameters.size(); ++i) {
|
|
159
|
+
auto a = xt + (xb-xt)*parameters[i], b = xt + (xb-xt)*parameters[i+1];
|
|
160
|
+
auto h = (top-bottom)*(parameters[i+1]-parameters[i]);
|
|
161
|
+
auto midpoint = (a+b)*0.5L, suffix = edge.weight*h*dx_;
|
|
162
|
+
if (midpoint < bounds_[0]) { difference_[0] += suffix; difference_[width_] -= suffix; continue; }
|
|
163
|
+
if (midpoint >= bounds_[2]) continue;
|
|
164
|
+
int column = bounded_index(std::floor(snap((midpoint-bounds_[0])/dx_)), width_);
|
|
165
|
+
if (column >= width_) continue;
|
|
166
|
+
const long double right = bounds_[0]+(column+1)*dx_;
|
|
167
|
+
boundary_[column] += edge.weight*((right-a)+(right-b))*0.5L*h;
|
|
168
|
+
difference_[column+1] += suffix; difference_[width_] -= suffix;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
Row Sweep::next() {
|
|
173
|
+
std::lock_guard<std::mutex> lock(mutex_);
|
|
174
|
+
if (next_row_ == height_) return {-1, {}};
|
|
175
|
+
const int row = next_row_;
|
|
176
|
+
std::fill(boundary_.begin(), boundary_.end(), 0);
|
|
177
|
+
std::fill(difference_.begin(), difference_.end(), 0);
|
|
178
|
+
active_.insert(active_.end(), entries_.begin()+offsets_[row], entries_.begin()+offsets_[row+1]);
|
|
179
|
+
std::size_t retained = 0;
|
|
180
|
+
for (auto i : active_) {
|
|
181
|
+
accumulate(edges_[i], row);
|
|
182
|
+
if (last_rows_[i] > row) active_[retained++] = i;
|
|
183
|
+
}
|
|
184
|
+
active_.resize(retained);
|
|
185
|
+
Row result{row, std::vector<double>(width_)};
|
|
186
|
+
long double suffix = 0;
|
|
187
|
+
for (int column = 0; column < width_; ++column) {
|
|
188
|
+
suffix += difference_[column];
|
|
189
|
+
result.density[column] = static_cast<double>(boundary_[column]+suffix);
|
|
190
|
+
if (!std::isfinite(result.density[column])) throw std::overflow_error("density overflow");
|
|
191
|
+
}
|
|
192
|
+
++next_row_;
|
|
193
|
+
return result;
|
|
194
|
+
}
|
|
195
|
+
} // namespace fastpgv
|
fastpgv-0.1.0/cpp/sled.h
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
#pragma once
|
|
2
|
+
|
|
3
|
+
#include <array>
|
|
4
|
+
#include <memory>
|
|
5
|
+
#include <mutex>
|
|
6
|
+
#include <vector>
|
|
7
|
+
|
|
8
|
+
namespace fastpgv {
|
|
9
|
+
using Point = std::array<double, 2>;
|
|
10
|
+
using Polygon = std::vector<std::vector<Point>>;
|
|
11
|
+
using Geometry = std::vector<Polygon>;
|
|
12
|
+
using Bounds = std::array<double, 4>;
|
|
13
|
+
|
|
14
|
+
struct Edge {
|
|
15
|
+
double xu, yu, xl, yl;
|
|
16
|
+
long double weight;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
struct Row {
|
|
20
|
+
int v;
|
|
21
|
+
std::vector<double> density;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
class Sweep {
|
|
25
|
+
public:
|
|
26
|
+
Sweep(std::vector<Edge> edges, Bounds bounds, int width, int height);
|
|
27
|
+
Row next();
|
|
28
|
+
|
|
29
|
+
private:
|
|
30
|
+
void accumulate(const Edge& edge, int row);
|
|
31
|
+
std::vector<Edge> edges_;
|
|
32
|
+
Bounds bounds_;
|
|
33
|
+
int width_, height_, next_row_ = 0;
|
|
34
|
+
double dx_, dy_;
|
|
35
|
+
std::vector<int> last_rows_;
|
|
36
|
+
std::vector<std::size_t> offsets_, entries_, active_;
|
|
37
|
+
std::vector<long double> boundary_, difference_;
|
|
38
|
+
std::mutex mutex_;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
class Builder {
|
|
42
|
+
public:
|
|
43
|
+
void add(const Geometry& geometry, double weight);
|
|
44
|
+
std::unique_ptr<Sweep> finish(int width, int height);
|
|
45
|
+
private:
|
|
46
|
+
std::vector<Edge> edges_;
|
|
47
|
+
Bounds bounds_{};
|
|
48
|
+
bool have_bounds_ = false, finished_ = false;
|
|
49
|
+
std::mutex mutex_;
|
|
50
|
+
};
|
|
51
|
+
} // namespace fastpgv
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import fastpgv
|
|
2
|
+
|
|
3
|
+
polygons = [
|
|
4
|
+
{"type": "Polygon", "coordinates": [[[0, 0], [1, 0], [1, 1], [0, 1], [0, 0]]]},
|
|
5
|
+
{"type": "Polygon", "coordinates": [[[1, 0], [2, 0], [2, 1], [1, 1], [1, 0]]]},
|
|
6
|
+
]
|
|
7
|
+
density = fastpgv.run([(polygons[0], 2), (polygons[1], 3)], resolution=(2, 1))
|
|
8
|
+
print(density) # [[2., 3.]]
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
Copyright (c) 2016 Wenzel Jakob <wenzel.jakob@epfl.ch>, All rights reserved.
|
|
2
|
+
|
|
3
|
+
Redistribution and use in source and binary forms, with or without
|
|
4
|
+
modification, are permitted provided that the following conditions are met:
|
|
5
|
+
|
|
6
|
+
1. Redistributions of source code must retain the above copyright notice, this
|
|
7
|
+
list of conditions and the following disclaimer.
|
|
8
|
+
|
|
9
|
+
2. Redistributions in binary form must reproduce the above copyright notice,
|
|
10
|
+
this list of conditions and the following disclaimer in the documentation
|
|
11
|
+
and/or other materials provided with the distribution.
|
|
12
|
+
|
|
13
|
+
3. Neither the name of the copyright holder nor the names of its contributors
|
|
14
|
+
may be used to endorse or promote products derived from this software
|
|
15
|
+
without specific prior written permission.
|
|
16
|
+
|
|
17
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
|
18
|
+
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
|
19
|
+
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
20
|
+
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
|
21
|
+
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
|
22
|
+
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
|
23
|
+
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
|
24
|
+
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
|
25
|
+
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
26
|
+
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
27
|
+
|
|
28
|
+
Please also refer to the file .github/CONTRIBUTING.md, which clarifies licensing of
|
|
29
|
+
external contributions to this project including patches, pull requests, etc.
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["scikit-build-core>=0.11,<2", "pybind11>=3.0,<4"]
|
|
3
|
+
build-backend = "scikit_build_core.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "fastpgv"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Weighted polygon-to-grid aggregation with the native SLED algorithm"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
license = "MIT"
|
|
12
|
+
license-files = ["LICENSE", "THIRD_PARTY_NOTICES.md", "licenses/*"]
|
|
13
|
+
authors = [{name = "SLED contributors"}]
|
|
14
|
+
dependencies = ["numpy>=1.23"]
|
|
15
|
+
keywords = ["polygon", "raster", "geospatial", "SLED", "aggregation"]
|
|
16
|
+
classifiers = [
|
|
17
|
+
"Development Status :: 3 - Alpha",
|
|
18
|
+
"Intended Audience :: Science/Research",
|
|
19
|
+
"Programming Language :: Python :: 3",
|
|
20
|
+
"Programming Language :: Python :: 3.10",
|
|
21
|
+
"Programming Language :: Python :: 3.11",
|
|
22
|
+
"Programming Language :: Python :: 3.12",
|
|
23
|
+
"Programming Language :: Python :: 3.13",
|
|
24
|
+
"Programming Language :: Python :: 3.14",
|
|
25
|
+
"Programming Language :: C++",
|
|
26
|
+
"Topic :: Scientific/Engineering :: GIS",
|
|
27
|
+
]
|
|
28
|
+
|
|
29
|
+
[project.optional-dependencies]
|
|
30
|
+
test = ["pytest>=8", "shapely>=2"]
|
|
31
|
+
dev = ["pytest>=8", "shapely>=2", "build>=1.2", "twine>=6"]
|
|
32
|
+
|
|
33
|
+
[tool.scikit-build]
|
|
34
|
+
cmake.version = ">=3.18"
|
|
35
|
+
wheel.packages = ["src/fastpgv"]
|
|
36
|
+
sdist.include = ["cpp/*.cpp", "cpp/*.h", "src/fastpgv/*.py", "tests/*.py", "examples/*.py", "licenses/*", "/README.md", "/PORTING.md", "/PUBLISHING.md", "/THIRD_PARTY_NOTICES.md", "/LICENSE", "/CMakeLists.txt", "/pyproject.toml", ".github/workflows/*.yml"]
|
|
37
|
+
sdist.exclude = ["dist/**", "build/**", ".venv/**", "**/__pycache__/**", "**/*.pyc", ".pytest_cache/**"]
|
|
38
|
+
|
|
39
|
+
[tool.cibuildwheel]
|
|
40
|
+
build = "cp310-* cp311-* cp312-* cp313-* cp314-*"
|
|
41
|
+
skip = "*-musllinux_* *-win32 *-manylinux_i686"
|
|
42
|
+
test-requires = ["pytest>=8", "shapely>=2"]
|
|
43
|
+
test-command = "python -m pytest {project}/tests"
|
|
44
|
+
|
|
45
|
+
[tool.pytest.ini_options]
|
|
46
|
+
testpaths = ["tests"]
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
"""SLED weighted polygon-to-grid aggregation."""
|
|
2
|
+
import math as _math
|
|
3
|
+
from numbers import Integral as _Integral, Real as _Real
|
|
4
|
+
|
|
5
|
+
import numpy as _np
|
|
6
|
+
from . import _native
|
|
7
|
+
|
|
8
|
+
__version__ = "0.1.0"
|
|
9
|
+
__all__ = ["run"]
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def _number(value, label):
|
|
13
|
+
if isinstance(value, bool) or not isinstance(value, _Real):
|
|
14
|
+
raise ValueError(f"{label} must be a finite number")
|
|
15
|
+
try:
|
|
16
|
+
result = float(value)
|
|
17
|
+
except (OverflowError, ValueError) as exc:
|
|
18
|
+
raise ValueError(f"{label} must be a finite number") from exc
|
|
19
|
+
if not _math.isfinite(result):
|
|
20
|
+
raise ValueError(f"{label} must be a finite number")
|
|
21
|
+
return result
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _geometry(value):
|
|
25
|
+
value = getattr(value, "__geo_interface__", value)
|
|
26
|
+
if not isinstance(value, dict) or value.get("type") not in ("Polygon", "MultiPolygon"):
|
|
27
|
+
raise ValueError("geometry must be a GeoJSON Polygon or MultiPolygon with vertex coordinates")
|
|
28
|
+
try:
|
|
29
|
+
polygons = [value["coordinates"]] if value["type"] == "Polygon" else value["coordinates"]
|
|
30
|
+
return [
|
|
31
|
+
[[[_number(p[0], "coordinate"), _number(p[1], "coordinate")]
|
|
32
|
+
for p in ring] for ring in polygon] for polygon in polygons]
|
|
33
|
+
except (KeyError, IndexError, TypeError) as exc:
|
|
34
|
+
raise ValueError("invalid polygon vertex coordinates") from exc
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def run(weighted_polygons, resolution):
|
|
38
|
+
"""Return weighted intersection areas as a float64 array shaped (Y, X).
|
|
39
|
+
|
|
40
|
+
weighted_polygons is an iterable of (geometry, weight) pairs. Geometry
|
|
41
|
+
contains ordered vertices as a GeoJSON Polygon/MultiPolygon dictionary
|
|
42
|
+
or an object exposing __geo_interface__. Weights must be finite.
|
|
43
|
+
resolution is (X, Y); both dimensions are required positive integers.
|
|
44
|
+
Bounds are inferred from all polygons. Row zero is the top/northern row.
|
|
45
|
+
Valid polygon topology is required; coordinates are treated as planar.
|
|
46
|
+
"""
|
|
47
|
+
try:
|
|
48
|
+
dimensions = tuple(resolution)
|
|
49
|
+
except TypeError as exc:
|
|
50
|
+
raise ValueError("resolution must be two positive integers (X, Y)") from exc
|
|
51
|
+
if len(dimensions) != 2 or any(isinstance(v, bool) or not isinstance(v, _Integral)
|
|
52
|
+
or not 0 < v < 2147483647 for v in dimensions):
|
|
53
|
+
raise ValueError("resolution must be two positive integers (X, Y)")
|
|
54
|
+
x, y = map(int, dimensions)
|
|
55
|
+
builder = _native.Builder()
|
|
56
|
+
try:
|
|
57
|
+
records = iter(weighted_polygons)
|
|
58
|
+
except TypeError as exc:
|
|
59
|
+
raise ValueError("weighted_polygons must be an iterable of (geometry, weight) pairs") from exc
|
|
60
|
+
for index, record in enumerate(records):
|
|
61
|
+
if isinstance(record, (str, bytes, dict)):
|
|
62
|
+
raise ValueError(f"weighted polygon {index} must be a (geometry, weight) pair")
|
|
63
|
+
try:
|
|
64
|
+
geometry, weight = record
|
|
65
|
+
except (TypeError, ValueError) as exc:
|
|
66
|
+
raise ValueError(f"weighted polygon {index} must be a (geometry, weight) pair") from exc
|
|
67
|
+
try:
|
|
68
|
+
builder.add(_geometry(geometry), _number(weight, "weight"))
|
|
69
|
+
except ValueError as exc:
|
|
70
|
+
raise ValueError(f"weighted polygon {index}: {exc}") from exc
|
|
71
|
+
sweep = builder.finish(x, y)
|
|
72
|
+
density = _np.empty((y, x), dtype=_np.float64)
|
|
73
|
+
for v in range(y):
|
|
74
|
+
density[v] = sweep.next_row()
|
|
75
|
+
return density
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"""Optional snapshot parity check; not required for standalone wheel tests.
|
|
2
|
+
|
|
3
|
+
Set FASTPGV_BASELINE to the absolute pgv_baselines executable path.
|
|
4
|
+
"""
|
|
5
|
+
import csv
|
|
6
|
+
import os
|
|
7
|
+
import subprocess
|
|
8
|
+
|
|
9
|
+
import numpy as np
|
|
10
|
+
import pytest
|
|
11
|
+
from shapely.geometry import box
|
|
12
|
+
|
|
13
|
+
import fastpgv
|
|
14
|
+
from test_fastpgv import fixture, write_records
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@pytest.mark.skipif(not os.environ.get("FASTPGV_BASELINE"), reason="optional benchmark executable")
|
|
18
|
+
@pytest.mark.parametrize("resolution", [(4, 4), (17, 13)])
|
|
19
|
+
@pytest.mark.parametrize("shared", [False, True])
|
|
20
|
+
def test_benchmark_reference(tmp_path, resolution, shared):
|
|
21
|
+
source = tmp_path / "records.jsonl"
|
|
22
|
+
geometries, weights = ([box(0, 0, 2, 4), box(2, 0, 4, 4)], [2, 2]) if shared else fixture()
|
|
23
|
+
write_records(source, geometries, weights)
|
|
24
|
+
process = subprocess.run([
|
|
25
|
+
os.environ["FASTPGV_BASELINE"], "--records", str(source), "--resolution", f"{resolution[0]}x{resolution[1]}",
|
|
26
|
+
"--method", "scan,SLED", "--threads", "1",
|
|
27
|
+
"--out-dir", str(tmp_path / "reference"), "--run-name", "package_validation",
|
|
28
|
+
"--compare", "--no-progress"], capture_output=True, text=True)
|
|
29
|
+
assert process.returncode == 0, process.stderr + process.stdout
|
|
30
|
+
result = fastpgv.run(zip(geometries, weights), resolution)
|
|
31
|
+
paths = list((tmp_path / "reference").rglob("pixels.csv"))
|
|
32
|
+
assert len(paths) == 2
|
|
33
|
+
for path in paths:
|
|
34
|
+
with path.open() as f:
|
|
35
|
+
rows = list(csv.DictReader(f))
|
|
36
|
+
np.testing.assert_allclose(result.ravel(), [float(r["density"]) for r in rows],
|
|
37
|
+
atol=1e-9, rtol=1e-12)
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import inspect
|
|
3
|
+
|
|
4
|
+
import numpy as np
|
|
5
|
+
import pytest
|
|
6
|
+
from shapely.geometry import Polygon, MultiPolygon, box, mapping
|
|
7
|
+
|
|
8
|
+
import fastpgv
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def fixture():
|
|
12
|
+
return [box(0, 0, 2, 2), Polygon([(0.25, 0.25), (3.75, 0.75), (2.25, 3.75)]),
|
|
13
|
+
Polygon([(0, 0), (4, 0), (4, 4), (0, 4)],
|
|
14
|
+
[[(1, 1), (3, 1), (3, 3), (1, 3)]]),
|
|
15
|
+
MultiPolygon([box(0, 3, 1, 4), box(3, 0, 4, 1)]), box(1.25, 1.25, 2.75, 2.75)], \
|
|
16
|
+
[1, 2, 0.5, -0.75, 0]
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def write_records(path, geometries, weights):
|
|
20
|
+
with path.open("w") as f:
|
|
21
|
+
for geometry, weight in zip(geometries, weights):
|
|
22
|
+
f.write(json.dumps({"cell_id": "duplicate", "geometry": mapping(geometry),
|
|
23
|
+
"weight": {"value": weight}}) + "\n")
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@pytest.mark.parametrize("resolution", [(4, 4), (17, 13), (1, 1)])
|
|
27
|
+
def test_geometry_oracle(resolution):
|
|
28
|
+
geometries, weights = fixture()
|
|
29
|
+
result = fastpgv.run(zip(geometries, weights), resolution)
|
|
30
|
+
x, y = resolution
|
|
31
|
+
expected = np.zeros((y, x))
|
|
32
|
+
dx, dy = 4/x, 4/y
|
|
33
|
+
for v in range(y):
|
|
34
|
+
for u in range(x):
|
|
35
|
+
pixel = box(u*dx, 4-(v+1)*dy, (u+1)*dx, 4-v*dy)
|
|
36
|
+
expected[v, u] = sum(w*g.intersection(pixel).area for g, w in zip(geometries, weights))
|
|
37
|
+
difference = np.abs(result - expected)
|
|
38
|
+
assert np.all((difference <= 1e-9) | (difference <= 1e-12*np.abs(expected)))
|
|
39
|
+
assert result.dtype == np.float64 and result.shape == (y, x)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def test_example_and_vertices():
|
|
43
|
+
# Overlaps remain an internal algorithm regression, not a public guarantee.
|
|
44
|
+
a = {"type": "Polygon", "coordinates": [[[0, 0], [2, 0], [2, 2], [0, 2], [0, 0]]]}
|
|
45
|
+
b = {"type": "Polygon", "coordinates": [[[1, 1], [2, 1], [2, 2], [1, 2]]]}
|
|
46
|
+
np.testing.assert_array_equal(fastpgv.run([(a, 2), (b, 3)], (2, 2)), [[2, 5], [2, 2]])
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def test_release_example():
|
|
50
|
+
a, b = box(0, 0, 1, 1), box(1, 0, 2, 1)
|
|
51
|
+
assert a.intersection(b).area == 0
|
|
52
|
+
np.testing.assert_array_equal(fastpgv.run([(mapping(a), 2), (mapping(b), 3)], (2, 1)), [[2, 3]])
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def test_shared_boundaries_and_zero_weights():
|
|
56
|
+
left, right = box(0, 0, 1, 2), box(1, 0, 2, 2)
|
|
57
|
+
np.testing.assert_array_equal(fastpgv.run([(left, 1), (right, 2)], (2, 2)), [[1, 2], [1, 2]])
|
|
58
|
+
assert not fastpgv.run([(left, 1), (left, -1)], (2, 2)).any()
|
|
59
|
+
# Zero-weight geometry still defines the domain.
|
|
60
|
+
np.testing.assert_array_equal(fastpgv.run([(box(0, 0, 1, 1), 1), (box(0, 0, 2, 2), 0)], (2, 2)),
|
|
61
|
+
[[0, 0], [1, 0]])
|
|
62
|
+
assert not fastpgv.run([(left, 0)], (2, 2)).any()
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def test_orientation_and_unsegmented_edges():
|
|
66
|
+
left = Polygon([(0, 0), (0, 2), (1, 2), (1, 1), (1, 0)])
|
|
67
|
+
right = box(1, 0, 2, 2)
|
|
68
|
+
np.testing.assert_array_equal(fastpgv.run([(left, 1), (right, 1)], (2, 2)), np.ones((2, 2)))
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def test_translated_coordinates():
|
|
72
|
+
# Also runs on Windows, where long double has 53 significand bits.
|
|
73
|
+
origin = 1e8
|
|
74
|
+
polygon = Polygon([(origin, origin), (origin+2, origin), (origin, origin+2)])
|
|
75
|
+
np.testing.assert_allclose(fastpgv.run([(polygon, 1)], (2, 2)), [[0.5, 0], [1, 0.5]], atol=1e-9)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
@pytest.mark.parametrize("resolution", [(0, 2), (2.5, 2), (True, 2), (2,), (2147483647, 2), None])
|
|
79
|
+
def test_invalid_resolution(resolution):
|
|
80
|
+
with pytest.raises(ValueError, match="resolution"):
|
|
81
|
+
fastpgv.run([(box(0, 0, 1, 1), 1)], resolution)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
@pytest.mark.parametrize("geometry", [{"type": "Point", "coordinates": [0, 0]},
|
|
85
|
+
{"type": "Polygon", "coordinates": []},
|
|
86
|
+
{"type": "Polygon", "coordinates": [[[0, 0], [1, 1], [2, 2]]]},
|
|
87
|
+
{"type": "Polygon", "coordinates": [[[0, 0], [1, float("nan")], [2, 0]]]},
|
|
88
|
+
{"type": "Polygon", "coordinates": None}, "polygon_id"])
|
|
89
|
+
def test_invalid_geometry(geometry):
|
|
90
|
+
with pytest.raises(ValueError):
|
|
91
|
+
fastpgv.run([(geometry, 1)], (2, 2))
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
@pytest.mark.parametrize("weight", [float("nan"), float("inf"), "1", True, 10**400])
|
|
95
|
+
def test_invalid_weight(weight):
|
|
96
|
+
with pytest.raises(ValueError, match="weight"):
|
|
97
|
+
fastpgv.run([(box(0, 0, 1, 1), weight)], (2, 2))
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
@pytest.mark.parametrize("records", [[], None, [()], [(1, 2, 3)], ["ab"], [{"geometry": {}, "weight": 1}]])
|
|
101
|
+
def test_invalid_pairs(records):
|
|
102
|
+
with pytest.raises(ValueError):
|
|
103
|
+
fastpgv.run(records, (2, 2))
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def test_public_api():
|
|
107
|
+
assert fastpgv.__all__ == ["run"]
|
|
108
|
+
assert list(inspect.signature(fastpgv.run).parameters) == ["weighted_polygons", "resolution"]
|
|
109
|
+
for name in ("rasterize", "rasterize_jsonl", "iter_rows_jsonl", "GridResult", "GridRow"):
|
|
110
|
+
assert not hasattr(fastpgv, name)
|
|
111
|
+
for keyword in ("bounds", "weights", "streaming"):
|
|
112
|
+
with pytest.raises(TypeError):
|
|
113
|
+
fastpgv.run([(box(0, 0, 1, 1), 1)], (2, 2), **{keyword: None})
|
|
114
|
+
with pytest.raises(TypeError):
|
|
115
|
+
fastpgv.run([(box(0, 0, 1, 1), 1)])
|