rastr 0.4.0__tar.gz → 0.6.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.
Potentially problematic release.
This version of rastr might be problematic. Click here for more details.
- {rastr-0.4.0 → rastr-0.6.0}/.github/copilot-instructions.md +1 -1
- {rastr-0.4.0 → rastr-0.6.0}/.github/workflows/ci.yml +39 -26
- rastr-0.6.0/.github/workflows/release.yml +35 -0
- {rastr-0.4.0 → rastr-0.6.0}/CONTRIBUTING.md +14 -2
- {rastr-0.4.0 → rastr-0.6.0}/PKG-INFO +11 -5
- {rastr-0.4.0 → rastr-0.6.0}/README.md +9 -3
- {rastr-0.4.0 → rastr-0.6.0}/pyproject.toml +3 -4
- {rastr-0.4.0 → rastr-0.6.0}/src/rastr/_version.py +2 -2
- {rastr-0.4.0 → rastr-0.6.0}/src/rastr/arr/fill.py +3 -2
- {rastr-0.4.0 → rastr-0.6.0}/src/rastr/create.py +32 -18
- {rastr-0.4.0 → rastr-0.6.0}/src/rastr/gis/fishnet.py +14 -2
- {rastr-0.4.0 → rastr-0.6.0}/src/rastr/gis/smooth.py +4 -4
- {rastr-0.4.0 → rastr-0.6.0}/src/rastr/io.py +40 -14
- {rastr-0.4.0 → rastr-0.6.0}/src/rastr/raster.py +627 -101
- rastr-0.6.0/src/scripts/demo_taper_border.py +4 -0
- rastr-0.6.0/tests/conftest.py +54 -0
- {rastr-0.4.0 → rastr-0.6.0}/tests/rastr/test_create.py +206 -20
- {rastr-0.4.0 → rastr-0.6.0}/tests/rastr/test_io.py +87 -5
- {rastr-0.4.0 → rastr-0.6.0}/tests/rastr/test_meta.py +7 -7
- rastr-0.6.0/tests/rastr/test_raster.py +3205 -0
- {rastr-0.4.0 → rastr-0.6.0}/uv.lock +43 -22
- rastr-0.4.0/.github/workflows/release.yml +0 -32
- rastr-0.4.0/tests/conftest.py +0 -12
- rastr-0.4.0/tests/rastr/test_raster.py +0 -1488
- {rastr-0.4.0 → rastr-0.6.0}/.copier-answers.yml +0 -0
- {rastr-0.4.0 → rastr-0.6.0}/.github/ISSUE_TEMPLATE/bug-report.md +0 -0
- {rastr-0.4.0 → rastr-0.6.0}/.github/ISSUE_TEMPLATE/enhancement.md +0 -0
- {rastr-0.4.0 → rastr-0.6.0}/.gitignore +0 -0
- {rastr-0.4.0 → rastr-0.6.0}/.pre-commit-config.yaml +0 -0
- {rastr-0.4.0 → rastr-0.6.0}/.python-version +0 -0
- {rastr-0.4.0 → rastr-0.6.0}/LICENSE +0 -0
- {rastr-0.4.0 → rastr-0.6.0}/docs/index.md +0 -0
- {rastr-0.4.0 → rastr-0.6.0}/docs/logo.svg +0 -0
- {rastr-0.4.0 → rastr-0.6.0}/mkdocs.yml +0 -0
- {rastr-0.4.0 → rastr-0.6.0}/pyrightconfig.json +0 -0
- {rastr-0.4.0 → rastr-0.6.0}/requirements.txt +0 -0
- {rastr-0.4.0 → rastr-0.6.0}/src/archive/.gitkeep +0 -0
- {rastr-0.4.0 → rastr-0.6.0}/src/notebooks/.gitkeep +0 -0
- {rastr-0.4.0 → rastr-0.6.0}/src/rastr/__init__.py +0 -0
- {rastr-0.4.0 → rastr-0.6.0}/src/rastr/arr/__init__.py +0 -0
- {rastr-0.4.0 → rastr-0.6.0}/src/rastr/gis/__init__.py +0 -0
- {rastr-0.4.0 → rastr-0.6.0}/src/rastr/meta.py +0 -0
- {rastr-0.4.0 → rastr-0.6.0}/src/scripts/.gitkeep +0 -0
- {rastr-0.4.0 → rastr-0.6.0}/src/scripts/demo_point_cloud.py +0 -0
- {rastr-0.4.0 → rastr-0.6.0}/tasks/ABOUT_TASKS.md +0 -0
- {rastr-0.4.0 → rastr-0.6.0}/tasks/dev_sync.ps1 +0 -0
- {rastr-0.4.0 → rastr-0.6.0}/tasks/scripts/activate_venv.sh +0 -0
- {rastr-0.4.0 → rastr-0.6.0}/tasks/scripts/configure_project.sh +0 -0
- {rastr-0.4.0 → rastr-0.6.0}/tasks/scripts/dev_sync.sh +0 -0
- {rastr-0.4.0 → rastr-0.6.0}/tasks/scripts/install_backend.sh +0 -0
- {rastr-0.4.0 → rastr-0.6.0}/tasks/scripts/install_venv.sh +0 -0
- {rastr-0.4.0 → rastr-0.6.0}/tasks/scripts/recover_corrupt_venv.sh +0 -0
- {rastr-0.4.0 → rastr-0.6.0}/tasks/scripts/sh_runner.ps1 +0 -0
- {rastr-0.4.0 → rastr-0.6.0}/tasks/scripts/sync_requirements.sh +0 -0
- {rastr-0.4.0 → rastr-0.6.0}/tasks/scripts/sync_template.sh +0 -0
- {rastr-0.4.0 → rastr-0.6.0}/tasks/shims/activate_venv +0 -0
- {rastr-0.4.0 → rastr-0.6.0}/tasks/shims/activate_venv.cmd +0 -0
- {rastr-0.4.0 → rastr-0.6.0}/tasks/shims/activate_venv.ps1 +0 -0
- {rastr-0.4.0 → rastr-0.6.0}/tasks/shims/configure_project +0 -0
- {rastr-0.4.0 → rastr-0.6.0}/tasks/shims/configure_project.cmd +0 -0
- {rastr-0.4.0 → rastr-0.6.0}/tasks/shims/configure_project.ps1 +0 -0
- {rastr-0.4.0 → rastr-0.6.0}/tasks/shims/dev_sync +0 -0
- {rastr-0.4.0 → rastr-0.6.0}/tasks/shims/dev_sync.cmd +0 -0
- {rastr-0.4.0 → rastr-0.6.0}/tasks/shims/dev_sync.ps1 +0 -0
- {rastr-0.4.0 → rastr-0.6.0}/tasks/shims/install_backend +0 -0
- {rastr-0.4.0 → rastr-0.6.0}/tasks/shims/install_backend.cmd +0 -0
- {rastr-0.4.0 → rastr-0.6.0}/tasks/shims/install_backend.ps1 +0 -0
- {rastr-0.4.0 → rastr-0.6.0}/tasks/shims/install_venv +0 -0
- {rastr-0.4.0 → rastr-0.6.0}/tasks/shims/install_venv.cmd +0 -0
- {rastr-0.4.0 → rastr-0.6.0}/tasks/shims/install_venv.ps1 +0 -0
- {rastr-0.4.0 → rastr-0.6.0}/tasks/shims/recover_corrupt_venv +0 -0
- {rastr-0.4.0 → rastr-0.6.0}/tasks/shims/recover_corrupt_venv.cmd +0 -0
- {rastr-0.4.0 → rastr-0.6.0}/tasks/shims/recover_corrupt_venv.ps1 +0 -0
- {rastr-0.4.0 → rastr-0.6.0}/tasks/shims/sync_requirements +0 -0
- {rastr-0.4.0 → rastr-0.6.0}/tasks/shims/sync_requirements.cmd +0 -0
- {rastr-0.4.0 → rastr-0.6.0}/tasks/shims/sync_requirements.ps1 +0 -0
- {rastr-0.4.0 → rastr-0.6.0}/tasks/shims/sync_template +0 -0
- {rastr-0.4.0 → rastr-0.6.0}/tasks/shims/sync_template.cmd +0 -0
- {rastr-0.4.0 → rastr-0.6.0}/tasks/shims/sync_template.ps1 +0 -0
- {rastr-0.4.0 → rastr-0.6.0}/tasks/sync_template.ps1 +0 -0
- {rastr-0.4.0 → rastr-0.6.0}/tests/assets/.gitkeep +0 -0
- {rastr-0.4.0 → rastr-0.6.0}/tests/assets/pga_g_clipped.grd +0 -0
- {rastr-0.4.0 → rastr-0.6.0}/tests/assets/pga_g_clipped.tif +0 -0
- {rastr-0.4.0 → rastr-0.6.0}/tests/rastr/.gitkeep +0 -0
- {rastr-0.4.0 → rastr-0.6.0}/tests/rastr/gis/test_fishnet.py +0 -0
- {rastr-0.4.0 → rastr-0.6.0}/tests/rastr/gis/test_smooth.py +0 -0
- {rastr-0.4.0 → rastr-0.6.0}/tests/rastr/regression_test_data/test_plot_raster.png +0 -0
- {rastr-0.4.0 → rastr-0.6.0}/tests/rastr/regression_test_data/test_write_raster_to_file.tif +0 -0
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
- Write unit tests using `pytest` inside `tests/`, structured based on `src/`.
|
|
11
11
|
- Example: `src/x/y/z` → `tests/x/y/test_z.py`
|
|
12
12
|
- Use test fixtures and group tests into classes when appropriate.
|
|
13
|
-
- When testing methods on classes, create nested test classes within the test class associated with the overall tested class (e.g., `TestContour` nested within `
|
|
13
|
+
- When testing methods on classes, create nested test classes within the test class associated with the overall tested class (e.g., `TestContour` nested within `TestRaster` when testing the `contour` method of `Raster`).
|
|
14
14
|
- After modifying a function, run its unit tests using pytest.
|
|
15
15
|
- Run tests in virtual environment: `.\.venv\Scripts\activate; python -m pytest tests/path/to/test_file.py -v`
|
|
16
16
|
|
|
@@ -1,32 +1,34 @@
|
|
|
1
1
|
name: CI
|
|
2
2
|
permissions:
|
|
3
3
|
contents: read
|
|
4
|
-
pull-requests: write
|
|
5
4
|
on:
|
|
6
5
|
workflow_dispatch:
|
|
7
6
|
push:
|
|
8
|
-
branches: [
|
|
7
|
+
branches: ["master", "develop"]
|
|
9
8
|
paths-ignore:
|
|
10
|
-
-
|
|
11
|
-
-
|
|
12
|
-
-
|
|
9
|
+
- "docs/**"
|
|
10
|
+
- "**/*.md"
|
|
11
|
+
- "mkdocs.yml"
|
|
13
12
|
pull_request:
|
|
14
|
-
branches: [
|
|
13
|
+
branches: ["master", "develop"]
|
|
15
14
|
paths-ignore:
|
|
16
|
-
-
|
|
17
|
-
-
|
|
18
|
-
-
|
|
15
|
+
- "docs/**"
|
|
16
|
+
- "**/*.md"
|
|
17
|
+
- "mkdocs.yml"
|
|
19
18
|
concurrency:
|
|
20
19
|
group: ${{ github.workflow }}-${{ github.ref }}
|
|
21
20
|
cancel-in-progress: true
|
|
22
21
|
jobs:
|
|
23
22
|
tests:
|
|
23
|
+
name: Run Tests and Checks
|
|
24
24
|
runs-on: ${{ matrix.os }}
|
|
25
25
|
env:
|
|
26
26
|
PYTHONIOENCODING: utf-8
|
|
27
27
|
steps:
|
|
28
28
|
- name: Checkout code
|
|
29
29
|
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
|
30
|
+
with:
|
|
31
|
+
persist-credentials: false
|
|
30
32
|
|
|
31
33
|
- name: Setup git user config
|
|
32
34
|
run: |
|
|
@@ -39,32 +41,38 @@ jobs:
|
|
|
39
41
|
version: "0.7.13" # Sync with pyproject.toml
|
|
40
42
|
enable-cache: true
|
|
41
43
|
|
|
42
|
-
- name:
|
|
43
|
-
uses: actions/setup-python@0b93645e9fea7318ecaed2b359559ac225c90a2b # v5.3.0
|
|
44
|
-
with:
|
|
45
|
-
python-version: ${{ matrix.python-version }}
|
|
46
|
-
|
|
47
|
-
- name: Setup dependencies
|
|
44
|
+
- name: Setup Python
|
|
48
45
|
run: |
|
|
49
|
-
uv python pin ${
|
|
50
|
-
uv
|
|
51
|
-
|
|
46
|
+
uv python pin ${PYTHON_VERSION}
|
|
47
|
+
uv python install
|
|
48
|
+
env:
|
|
49
|
+
PYTHON_VERSION: ${{ matrix.python-version }}
|
|
52
50
|
|
|
53
51
|
- name: Run pre-commit
|
|
54
52
|
if: matrix.checks
|
|
55
53
|
run: |
|
|
56
|
-
uv run
|
|
54
|
+
uv run pre-commit run --all-files
|
|
57
55
|
|
|
58
56
|
- name: Run pyright
|
|
59
57
|
if: matrix.checks
|
|
60
58
|
run: |
|
|
61
59
|
uv run pyright
|
|
62
60
|
|
|
61
|
+
- name: Run zizmor
|
|
62
|
+
if: matrix.os == 'ubuntu-latest' && matrix.python-version == '3.13'
|
|
63
|
+
run: |
|
|
64
|
+
uv run zizmor --no-progress --pedantic .github/
|
|
65
|
+
env:
|
|
66
|
+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
67
|
+
|
|
63
68
|
- name: Run pytest
|
|
64
69
|
uses: pavelzw/pytest-action@510c5e90c360a185039bea56ce8b3e7e51a16507 # v2.2.0
|
|
65
70
|
if: matrix.pytest
|
|
66
71
|
with:
|
|
72
|
+
custom-pytest: uv run pytest
|
|
67
73
|
custom-arguments: --cov --junitxml=junit.xml -o junit_family=legacy --cov-report=xml
|
|
74
|
+
env:
|
|
75
|
+
MPLBACKEND: Agg # https://github.com/orgs/community/discussions/26434
|
|
68
76
|
|
|
69
77
|
- name: Create test reports directory
|
|
70
78
|
if: matrix.pytest && matrix.os == 'ubuntu-latest' && matrix.python-version == '3.13'
|
|
@@ -95,25 +103,28 @@ jobs:
|
|
|
95
103
|
name: Analyse Code Quality
|
|
96
104
|
runs-on: ubuntu-latest
|
|
97
105
|
needs: tests
|
|
106
|
+
permissions:
|
|
107
|
+
pull-requests: write # SonarQube needs to post comments on PRs
|
|
98
108
|
if: always() && needs.tests.result == 'success'
|
|
99
|
-
|
|
109
|
+
|
|
100
110
|
steps:
|
|
101
111
|
- name: Checkout code
|
|
102
112
|
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
|
103
113
|
with:
|
|
104
|
-
fetch-depth: 0
|
|
105
|
-
|
|
114
|
+
fetch-depth: 0 # Shallow clones should be disabled for better relevancy of analysis
|
|
115
|
+
persist-credentials: false
|
|
116
|
+
|
|
106
117
|
- name: Download coverage reports
|
|
107
118
|
uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8
|
|
108
119
|
with:
|
|
109
120
|
name: coverage-reports
|
|
110
121
|
path: test-reports/
|
|
111
122
|
continue-on-error: true
|
|
112
|
-
|
|
123
|
+
|
|
113
124
|
- name: Create SonarQube properties
|
|
114
125
|
run: |
|
|
115
126
|
cat > sonar-project.properties << EOF
|
|
116
|
-
sonar.projectKey=${
|
|
127
|
+
sonar.projectKey=${SONAR_PROJECT_KEY}
|
|
117
128
|
sonar.language=py
|
|
118
129
|
sonar.python.version=3.13
|
|
119
130
|
sonar.sources=./src
|
|
@@ -122,9 +133,11 @@ jobs:
|
|
|
122
133
|
sonar.exclusions=**/Dockerfile,**/notebooks/**,**/scripts/**
|
|
123
134
|
sonar.verbose=false
|
|
124
135
|
EOF
|
|
125
|
-
|
|
136
|
+
env:
|
|
137
|
+
SONAR_PROJECT_KEY: ${{ vars.SONAR_PROJECT_KEY }}
|
|
138
|
+
|
|
126
139
|
- name: Run SonarQube analysis
|
|
127
|
-
uses: SonarSource/sonarqube-scan-action@
|
|
140
|
+
uses: SonarSource/sonarqube-scan-action@fd88b7d7ccbaefd23d8f36f73b59db7a3d246602 # v6.0.0
|
|
128
141
|
env:
|
|
129
142
|
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
|
|
130
143
|
SONAR_HOST_URL: ${{ vars.SONAR_HOST_URL }}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
name: Release to PyPI
|
|
2
|
+
permissions:
|
|
3
|
+
contents: read
|
|
4
|
+
on:
|
|
5
|
+
push:
|
|
6
|
+
tags:
|
|
7
|
+
- "v*"
|
|
8
|
+
jobs:
|
|
9
|
+
deploy:
|
|
10
|
+
name: Release on PyPI
|
|
11
|
+
runs-on: ubuntu-latest
|
|
12
|
+
environment: release
|
|
13
|
+
permissions:
|
|
14
|
+
id-token: write # This is necessary to use trusted publishing with uv
|
|
15
|
+
steps:
|
|
16
|
+
- name: Checkout code
|
|
17
|
+
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
|
18
|
+
with:
|
|
19
|
+
persist-credentials: false
|
|
20
|
+
|
|
21
|
+
- name: Set up uv
|
|
22
|
+
uses: astral-sh/setup-uv@3b9817b1bf26186f03ab8277bab9b827ea5cc254 # v3.2.0
|
|
23
|
+
with:
|
|
24
|
+
version: "0.7.13" # Sync with pyproject.toml
|
|
25
|
+
enable-cache: false
|
|
26
|
+
|
|
27
|
+
- name: "Set up Python"
|
|
28
|
+
uses: actions/setup-python@0b93645e9fea7318ecaed2b359559ac225c90a2b # v5.3.0
|
|
29
|
+
with:
|
|
30
|
+
python-version: 3.13
|
|
31
|
+
|
|
32
|
+
- name: Release
|
|
33
|
+
run: |
|
|
34
|
+
uv build
|
|
35
|
+
uv publish --trusted-publishing always
|
|
@@ -55,9 +55,21 @@ uv run detect-test-pollution --fuzz --tests ./tests
|
|
|
55
55
|
|
|
56
56
|
and follow the prompts to bisect the tests.
|
|
57
57
|
|
|
58
|
-
### Profiling Test
|
|
58
|
+
### Profiling the Test Suite
|
|
59
59
|
|
|
60
|
-
To profile the
|
|
60
|
+
To profile the test suite, use `pyinstrument`:
|
|
61
|
+
|
|
62
|
+
```shell
|
|
63
|
+
uv run pyinstrument -m pytest
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
For more in-depth exploration, try the `-r html` option to generate an HTML report:
|
|
67
|
+
|
|
68
|
+
```shell
|
|
69
|
+
uv run pyinstrument -r html -m pytest
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
To profile the speed of test _collection_ (which is mostly related to import times), you can also use `pyinstrument`:
|
|
61
73
|
|
|
62
74
|
```shell
|
|
63
75
|
uv run pyinstrument -m pytest --collect-only
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: rastr
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.6.0
|
|
4
4
|
Summary: Geospatial Raster datatype library for Python.
|
|
5
5
|
Project-URL: Source Code, https://github.com/tonkintaylor/rastr
|
|
6
6
|
Project-URL: Bug Tracker, https://github.com/tonkintaylor/rastr/issues
|
|
7
7
|
Project-URL: Releases, https://github.com/tonkintaylor/rastr/releases
|
|
8
|
-
Project-URL: Source Archive, https://github.com/tonkintaylor/rastr/archive/
|
|
8
|
+
Project-URL: Source Archive, https://github.com/tonkintaylor/rastr/archive/a1b755ea9cde7f81b2963dcb12917090019f2b47.zip
|
|
9
9
|
Author-email: Tonkin & Taylor Limited <Sub-DisciplineData+AnalyticsStaff@tonkintaylor.co.nz>, Nathan McDougall <nmcdougall@tonkintaylor.co.nz>, Ben Karl <bkarl@tonkintaylor.co.nz>
|
|
10
10
|
License-Expression: MIT
|
|
11
11
|
License-File: LICENSE
|
|
@@ -75,10 +75,10 @@ from pyproj.crs.crs import CRS
|
|
|
75
75
|
from rasterio.transform import from_origin
|
|
76
76
|
from rastr.create import full_raster
|
|
77
77
|
from rastr.meta import RasterMeta
|
|
78
|
-
from rastr.raster import
|
|
78
|
+
from rastr.raster import Raster
|
|
79
79
|
|
|
80
|
-
#
|
|
81
|
-
raster =
|
|
80
|
+
# Read a raster from a file
|
|
81
|
+
raster = Raster.read_file("path/to/raster.tif")
|
|
82
82
|
|
|
83
83
|
# Basic arithmetic operations
|
|
84
84
|
doubled = raster * 2
|
|
@@ -131,6 +131,12 @@ Current version limitations:
|
|
|
131
131
|
- Square cells only (rectangular cell support planned).
|
|
132
132
|
- Only float dtypes (integer support planned).
|
|
133
133
|
|
|
134
|
+
## Similar Projects
|
|
135
|
+
|
|
136
|
+
- [rasters](https://github.com/python-rasters/rasters) is a project with similar goals of providing a dedicated raster datatype in Python with higher-level interfaces for GIS operations. Unlike `rastr`, it has support for multi-band rasters, and has some more advanced functionality for Earth Science applications. Both projects are relatively new and under active development.
|
|
137
|
+
- [rasterio](https://rasterio.readthedocs.io/) is a core dependency of `rastr` and provides low-level raster I/O and processing capabilities.
|
|
138
|
+
- [rioxarray](https://corteva.github.io/rioxarray/stable/getting_started/getting_started.html) extends [`xarray`](https://docs.xarray.dev/en/stable/index.html) for raster data with geospatial support via `rasterio`.
|
|
139
|
+
|
|
134
140
|
### Contributing
|
|
135
141
|
|
|
136
142
|
See the
|
|
@@ -41,10 +41,10 @@ from pyproj.crs.crs import CRS
|
|
|
41
41
|
from rasterio.transform import from_origin
|
|
42
42
|
from rastr.create import full_raster
|
|
43
43
|
from rastr.meta import RasterMeta
|
|
44
|
-
from rastr.raster import
|
|
44
|
+
from rastr.raster import Raster
|
|
45
45
|
|
|
46
|
-
#
|
|
47
|
-
raster =
|
|
46
|
+
# Read a raster from a file
|
|
47
|
+
raster = Raster.read_file("path/to/raster.tif")
|
|
48
48
|
|
|
49
49
|
# Basic arithmetic operations
|
|
50
50
|
doubled = raster * 2
|
|
@@ -97,6 +97,12 @@ Current version limitations:
|
|
|
97
97
|
- Square cells only (rectangular cell support planned).
|
|
98
98
|
- Only float dtypes (integer support planned).
|
|
99
99
|
|
|
100
|
+
## Similar Projects
|
|
101
|
+
|
|
102
|
+
- [rasters](https://github.com/python-rasters/rasters) is a project with similar goals of providing a dedicated raster datatype in Python with higher-level interfaces for GIS operations. Unlike `rastr`, it has support for multi-band rasters, and has some more advanced functionality for Earth Science applications. Both projects are relatively new and under active development.
|
|
103
|
+
- [rasterio](https://rasterio.readthedocs.io/) is a core dependency of `rastr` and provides low-level raster I/O and processing capabilities.
|
|
104
|
+
- [rioxarray](https://corteva.github.io/rioxarray/stable/getting_started/getting_started.html) extends [`xarray`](https://docs.xarray.dev/en/stable/index.html) for raster data with geospatial support via `rasterio`.
|
|
105
|
+
|
|
100
106
|
### Contributing
|
|
101
107
|
|
|
102
108
|
See the
|
|
@@ -63,9 +63,10 @@ dev = [
|
|
|
63
63
|
"pre-commit-update>=0.8.0",
|
|
64
64
|
"pyinstrument>=5.1.1",
|
|
65
65
|
"pyright[nodejs]>=1.1.403",
|
|
66
|
-
"ruff>=0.
|
|
66
|
+
"ruff>=0.13.2",
|
|
67
67
|
"tqdm>=4.67.1",
|
|
68
68
|
"usethis>=0.15.2",
|
|
69
|
+
"zizmor>=1.14.2",
|
|
69
70
|
]
|
|
70
71
|
test = [
|
|
71
72
|
"coverage[toml]>=7.10.1",
|
|
@@ -123,7 +124,6 @@ lint.ignore = [
|
|
|
123
124
|
# In some cases where your code is poorly-performing you might want to enable them again.
|
|
124
125
|
# ##############################
|
|
125
126
|
"PD101", # Harms readability for a performance optimization.
|
|
126
|
-
"PD901", # We often call variables "df" in functions dealing with pandas DataFrames.
|
|
127
127
|
"PERF203", # Too many false positives.
|
|
128
128
|
"PERF401", # This can hurt readability; the performance is not always worth it.
|
|
129
129
|
"PIE804", # This is controversial, some pandas APIs work better with dict approach.
|
|
@@ -182,7 +182,6 @@ lint.per-file-ignores."tests/**/*.py" = [
|
|
|
182
182
|
"PLR0913", # When we use fixtures, our test functions can have many arguments
|
|
183
183
|
"S101", # Using assert is fine in tests
|
|
184
184
|
"SLF001", # Accessing private members is sometimes necessary in tests
|
|
185
|
-
"TID253", # Conditional imports are not banned for tests
|
|
186
185
|
]
|
|
187
186
|
lint.allowed-confusables = [ "ℹ", "–", "σ" ]
|
|
188
187
|
lint.flake8-tidy-imports.banned-api."pytz".msg = "'zoneinfo' should be preferred to 'pytz' when using Python 3.9 and later, see https://tonkintaylor-sonarqube.azurewebsites.net/coding_rules?open=python%3AS6890&rule_key=python%3AS6890"
|
|
@@ -265,6 +264,6 @@ root_packages = [ "rastr" ]
|
|
|
265
264
|
[[tool.importlinter.contracts]]
|
|
266
265
|
name = "rastr"
|
|
267
266
|
type = "layers"
|
|
268
|
-
layers = [ "create
|
|
267
|
+
layers = [ "create : io : raster", "meta", "arr | gis", "_version" ]
|
|
269
268
|
containers = [ "rastr" ]
|
|
270
269
|
exhaustive = true
|
|
@@ -28,7 +28,7 @@ version_tuple: VERSION_TUPLE
|
|
|
28
28
|
commit_id: COMMIT_ID
|
|
29
29
|
__commit_id__: COMMIT_ID
|
|
30
30
|
|
|
31
|
-
__version__ = version = '0.
|
|
32
|
-
__version_tuple__ = version_tuple = (0,
|
|
31
|
+
__version__ = version = '0.6.0'
|
|
32
|
+
__version_tuple__ = version_tuple = (0, 6, 0)
|
|
33
33
|
|
|
34
34
|
__commit_id__ = commit_id = None
|
|
@@ -8,7 +8,7 @@ if TYPE_CHECKING:
|
|
|
8
8
|
from numpy.typing import NDArray
|
|
9
9
|
|
|
10
10
|
|
|
11
|
-
def fillna_nearest_neighbours(arr: NDArray
|
|
11
|
+
def fillna_nearest_neighbours(arr: NDArray) -> NDArray:
|
|
12
12
|
"""Fill NaN values in an N-dimensional array with their nearest neighbours' values.
|
|
13
13
|
|
|
14
14
|
The nearest neighbour is determined using the Euclidean distance between array
|
|
@@ -28,4 +28,5 @@ def fillna_nearest_neighbours(arr: NDArray[np.float64]) -> NDArray[np.float64]:
|
|
|
28
28
|
# Interpolate at the array indices
|
|
29
29
|
interp = NearestNDInterpolator(nonnan_idxs, arr[nonnan_mask])
|
|
30
30
|
filled_arr = interp(*np.indices(arr.shape))
|
|
31
|
-
|
|
31
|
+
# Preserve the original dtype
|
|
32
|
+
return filled_arr.astype(arr.dtype)
|
|
@@ -13,10 +13,10 @@ from shapely.geometry import Point
|
|
|
13
13
|
|
|
14
14
|
from rastr.gis.fishnet import create_point_grid, get_point_grid_shape
|
|
15
15
|
from rastr.meta import RasterMeta
|
|
16
|
-
from rastr.raster import
|
|
16
|
+
from rastr.raster import Raster
|
|
17
17
|
|
|
18
18
|
if TYPE_CHECKING:
|
|
19
|
-
from collections.abc import Iterable
|
|
19
|
+
from collections.abc import Collection, Iterable
|
|
20
20
|
|
|
21
21
|
import geopandas as gpd
|
|
22
22
|
from numpy.typing import ArrayLike
|
|
@@ -49,9 +49,9 @@ def raster_distance_from_polygon(
|
|
|
49
49
|
*,
|
|
50
50
|
raster_meta: RasterMeta,
|
|
51
51
|
extent_polygon: Polygon | None = None,
|
|
52
|
-
snap_raster:
|
|
52
|
+
snap_raster: Raster | None = None,
|
|
53
53
|
show_pbar: bool = False,
|
|
54
|
-
) ->
|
|
54
|
+
) -> Raster:
|
|
55
55
|
"""Make a raster where each cell's value is its centre's distance to a polygon.
|
|
56
56
|
|
|
57
57
|
The raster should use a projected coordinate system.
|
|
@@ -116,7 +116,7 @@ def raster_distance_from_polygon(
|
|
|
116
116
|
distances = np.where(mask, np.array([polygon.distance(pt) for pt in _pts]), np.nan)
|
|
117
117
|
distance_raster = distances.reshape(x.shape)
|
|
118
118
|
|
|
119
|
-
return
|
|
119
|
+
return Raster(arr=distance_raster, raster_meta=raster_meta)
|
|
120
120
|
|
|
121
121
|
|
|
122
122
|
def _pbar(iterable: Iterable[_T], *, desc: str | None = None) -> Iterable[_T]:
|
|
@@ -130,19 +130,19 @@ def full_raster(
|
|
|
130
130
|
*,
|
|
131
131
|
bounds: tuple[float, float, float, float],
|
|
132
132
|
fill_value: float = np.nan,
|
|
133
|
-
) ->
|
|
133
|
+
) -> Raster:
|
|
134
134
|
"""Create a raster with a specified fill value for all cells."""
|
|
135
135
|
shape = get_point_grid_shape(bounds=bounds, cell_size=raster_meta.cell_size)
|
|
136
136
|
arr = np.full(shape, fill_value, dtype=np.float32)
|
|
137
|
-
return
|
|
137
|
+
return Raster(arr=arr, raster_meta=raster_meta)
|
|
138
138
|
|
|
139
139
|
|
|
140
140
|
def rasterize_gdf(
|
|
141
141
|
gdf: gpd.GeoDataFrame,
|
|
142
142
|
*,
|
|
143
143
|
raster_meta: RasterMeta,
|
|
144
|
-
target_cols:
|
|
145
|
-
) -> list[
|
|
144
|
+
target_cols: Collection[str],
|
|
145
|
+
) -> list[Raster]:
|
|
146
146
|
"""Rasterize geometries from a GeoDataFrame.
|
|
147
147
|
|
|
148
148
|
Supports polygons, points, linestrings, and other geometry types.
|
|
@@ -205,14 +205,16 @@ def rasterize_gdf(
|
|
|
205
205
|
dtype=np.float32,
|
|
206
206
|
)
|
|
207
207
|
|
|
208
|
-
# Create
|
|
209
|
-
raster =
|
|
208
|
+
# Create Raster
|
|
209
|
+
raster = Raster(arr=raster_array, raster_meta=raster_meta)
|
|
210
210
|
rasters.append(raster)
|
|
211
211
|
|
|
212
212
|
return rasters
|
|
213
213
|
|
|
214
214
|
|
|
215
|
-
def _validate_columns_exist(
|
|
215
|
+
def _validate_columns_exist(
|
|
216
|
+
gdf: gpd.GeoDataFrame, target_cols: Collection[str]
|
|
217
|
+
) -> None:
|
|
216
218
|
"""Validate that all target columns exist in the GeoDataFrame.
|
|
217
219
|
|
|
218
220
|
Args:
|
|
@@ -228,7 +230,9 @@ def _validate_columns_exist(gdf: gpd.GeoDataFrame, target_cols: list[str]) -> No
|
|
|
228
230
|
raise MissingColumnsError(msg)
|
|
229
231
|
|
|
230
232
|
|
|
231
|
-
def _validate_columns_numeric(
|
|
233
|
+
def _validate_columns_numeric(
|
|
234
|
+
gdf: gpd.GeoDataFrame, target_cols: Collection[str]
|
|
235
|
+
) -> None:
|
|
232
236
|
"""Validate that all target columns contain numeric data.
|
|
233
237
|
|
|
234
238
|
Args:
|
|
@@ -286,7 +290,7 @@ def raster_from_point_cloud(
|
|
|
286
290
|
*,
|
|
287
291
|
crs: CRS | str,
|
|
288
292
|
cell_size: float | None = None,
|
|
289
|
-
) ->
|
|
293
|
+
) -> Raster:
|
|
290
294
|
"""Create a raster from a point cloud via interpolation.
|
|
291
295
|
|
|
292
296
|
Interpolation is only possible within the convex hull of the points. Outside of
|
|
@@ -320,8 +324,18 @@ def raster_from_point_cloud(
|
|
|
320
324
|
if len(x) != len(y) or len(x) != len(z):
|
|
321
325
|
msg = "Length of x, y, and z must be equal."
|
|
322
326
|
raise ValueError(msg)
|
|
327
|
+
xy_finite_mask = np.isfinite(x) & np.isfinite(y)
|
|
328
|
+
if np.any(~xy_finite_mask):
|
|
329
|
+
msg = "Some (x,y) points are NaN-valued or non-finite. These will be ignored."
|
|
330
|
+
warnings.warn(msg, stacklevel=2)
|
|
331
|
+
x = x[xy_finite_mask]
|
|
332
|
+
y = y[xy_finite_mask]
|
|
333
|
+
z = z[xy_finite_mask]
|
|
323
334
|
if len(x) < 3:
|
|
324
|
-
msg =
|
|
335
|
+
msg = (
|
|
336
|
+
"At least three valid (x, y, z) points are required to triangulate a "
|
|
337
|
+
"surface."
|
|
338
|
+
)
|
|
325
339
|
raise ValueError(msg)
|
|
326
340
|
# Check for duplicate (x, y) points
|
|
327
341
|
xy_points = np.column_stack((x, y))
|
|
@@ -332,8 +346,8 @@ def raster_from_point_cloud(
|
|
|
332
346
|
# Heuristic for cell size if not provided
|
|
333
347
|
if cell_size is None:
|
|
334
348
|
# Half the 5th percentile of nearest neighbor distances between the (x,y) points
|
|
335
|
-
tree = KDTree(
|
|
336
|
-
distances, _ = tree.query(
|
|
349
|
+
tree = KDTree(xy_points)
|
|
350
|
+
distances, _ = tree.query(xy_points, k=2)
|
|
337
351
|
distances: np.ndarray
|
|
338
352
|
cell_size = float(np.percentile(distances[distances > 0], 5)) / 2
|
|
339
353
|
|
|
@@ -378,4 +392,4 @@ def raster_from_point_cloud(
|
|
|
378
392
|
crs=crs,
|
|
379
393
|
transform=transform,
|
|
380
394
|
)
|
|
381
|
-
return
|
|
395
|
+
return Raster(arr=arr, raster_meta=raster_meta)
|
|
@@ -41,8 +41,20 @@ def get_point_grid_shape(
|
|
|
41
41
|
"""Calculate the shape of the point grid based on bounds and cell size."""
|
|
42
42
|
|
|
43
43
|
xmin, ymin, xmax, ymax = bounds
|
|
44
|
-
|
|
45
|
-
|
|
44
|
+
ncols_exact = (xmax - xmin) / cell_size
|
|
45
|
+
nrows_exact = (ymax - ymin) / cell_size
|
|
46
|
+
|
|
47
|
+
# Use round for values very close to integers to avoid floating-point
|
|
48
|
+
# sensitivity while maintaining ceil behavior for truly fractional values
|
|
49
|
+
if np.isclose(ncols_exact, np.round(ncols_exact)):
|
|
50
|
+
ncols = int(np.round(ncols_exact))
|
|
51
|
+
else:
|
|
52
|
+
ncols = int(np.ceil(ncols_exact))
|
|
53
|
+
|
|
54
|
+
if np.isclose(nrows_exact, np.round(nrows_exact)):
|
|
55
|
+
nrows = int(np.round(nrows_exact))
|
|
56
|
+
else:
|
|
57
|
+
nrows = int(np.ceil(nrows_exact))
|
|
46
58
|
|
|
47
59
|
return nrows, ncols
|
|
48
60
|
|
|
@@ -5,7 +5,7 @@ Fork + Port of <https://github.com/philipschall/shapelysmooth> (Public domain)
|
|
|
5
5
|
|
|
6
6
|
from __future__ import annotations
|
|
7
7
|
|
|
8
|
-
from typing import TYPE_CHECKING,
|
|
8
|
+
from typing import TYPE_CHECKING, TypeVar
|
|
9
9
|
|
|
10
10
|
import numpy as np
|
|
11
11
|
from shapely.geometry import LineString, Polygon
|
|
@@ -14,7 +14,7 @@ from typing_extensions import assert_never
|
|
|
14
14
|
if TYPE_CHECKING:
|
|
15
15
|
from numpy.typing import NDArray
|
|
16
16
|
|
|
17
|
-
T
|
|
17
|
+
T = TypeVar("T", bound=LineString | Polygon)
|
|
18
18
|
|
|
19
19
|
|
|
20
20
|
class InputeTypeError(TypeError):
|
|
@@ -38,12 +38,12 @@ def catmull_rom_smooth(geometry: T, alpha: float = 0.5, subdivs: int = 10) -> T:
|
|
|
38
38
|
coords, interior_coords = _get_coords(geometry)
|
|
39
39
|
coords_smoothed = _catmull_rom(coords, alpha=alpha, subdivs=subdivs)
|
|
40
40
|
if isinstance(geometry, LineString):
|
|
41
|
-
return
|
|
41
|
+
return geometry.__class__(coords_smoothed)
|
|
42
42
|
elif isinstance(geometry, Polygon):
|
|
43
43
|
interior_coords_smoothed = [
|
|
44
44
|
_catmull_rom(c, alpha=alpha, subdivs=subdivs) for c in interior_coords
|
|
45
45
|
]
|
|
46
|
-
return
|
|
46
|
+
return geometry.__class__(coords_smoothed, holes=interior_coords_smoothed)
|
|
47
47
|
else:
|
|
48
48
|
assert_never(geometry)
|
|
49
49
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
from pathlib import Path
|
|
4
|
-
from typing import TYPE_CHECKING
|
|
4
|
+
from typing import TYPE_CHECKING, TypeVar
|
|
5
5
|
|
|
6
6
|
import numpy as np
|
|
7
7
|
import rasterio
|
|
@@ -9,39 +9,59 @@ import rasterio.merge
|
|
|
9
9
|
from pyproj.crs.crs import CRS
|
|
10
10
|
|
|
11
11
|
from rastr.meta import RasterMeta
|
|
12
|
-
from rastr.raster import
|
|
12
|
+
from rastr.raster import Raster
|
|
13
13
|
|
|
14
14
|
if TYPE_CHECKING:
|
|
15
15
|
from numpy.typing import NDArray
|
|
16
16
|
|
|
17
|
+
R = TypeVar("R", bound=Raster)
|
|
18
|
+
|
|
17
19
|
|
|
18
20
|
def read_raster_inmem(
|
|
19
|
-
raster_path: Path | str,
|
|
20
|
-
|
|
21
|
-
|
|
21
|
+
raster_path: Path | str,
|
|
22
|
+
*,
|
|
23
|
+
crs: CRS | str | None = None,
|
|
24
|
+
cls: type[R] = Raster,
|
|
25
|
+
) -> R:
|
|
26
|
+
"""Read raster data from a file and return an in-memory Raster object.
|
|
27
|
+
|
|
28
|
+
Args:
|
|
29
|
+
raster_path: Path to the raster file.
|
|
30
|
+
crs: Optional CRS to override the raster's native CRS.
|
|
31
|
+
cls: The Raster subclass to instantiate. This is mostly for internal use,
|
|
32
|
+
but can be useful if you have a custom `Raster` subclass.
|
|
33
|
+
"""
|
|
22
34
|
crs = CRS.from_user_input(crs) if crs is not None else None
|
|
23
35
|
|
|
24
36
|
with rasterio.open(raster_path, mode="r") as dst:
|
|
25
37
|
# Read the entire array
|
|
26
|
-
|
|
27
|
-
|
|
38
|
+
raw_arr: NDArray = dst.read()
|
|
39
|
+
raw_arr = raw_arr.squeeze()
|
|
40
|
+
|
|
28
41
|
# Extract metadata
|
|
29
42
|
cell_size = dst.res[0]
|
|
30
43
|
if crs is None:
|
|
31
44
|
crs = CRS.from_user_input(dst.crs)
|
|
32
45
|
transform = dst.transform
|
|
33
46
|
nodata = dst.nodata
|
|
47
|
+
|
|
48
|
+
# Cast integers to float16 to handle NaN values
|
|
49
|
+
if np.issubdtype(raw_arr.dtype, np.integer):
|
|
50
|
+
arr = raw_arr.astype(np.float16)
|
|
51
|
+
else:
|
|
52
|
+
arr = raw_arr
|
|
53
|
+
|
|
34
54
|
if nodata is not None:
|
|
35
|
-
arr[
|
|
55
|
+
arr[raw_arr == nodata] = np.nan
|
|
36
56
|
|
|
37
57
|
raster_meta = RasterMeta(cell_size=cell_size, crs=crs, transform=transform)
|
|
38
|
-
raster_obj =
|
|
58
|
+
raster_obj = cls(arr=arr, raster_meta=raster_meta)
|
|
39
59
|
return raster_obj
|
|
40
60
|
|
|
41
61
|
|
|
42
62
|
def read_raster_mosaic_inmem(
|
|
43
63
|
mosaic_dir: Path | str, *, glob: str = "*.tif", crs: CRS | None = None
|
|
44
|
-
) ->
|
|
64
|
+
) -> Raster:
|
|
45
65
|
"""Read a raster mosaic from a directory and return an in-memory Raster object.
|
|
46
66
|
|
|
47
67
|
This assumes that all rasters have the same metadata, e.g. coordinate system,
|
|
@@ -81,13 +101,19 @@ def read_raster_mosaic_inmem(
|
|
|
81
101
|
crs = CRS.from_user_input(sources[0].crs)
|
|
82
102
|
|
|
83
103
|
nodata = sources[0].nodata
|
|
84
|
-
|
|
85
|
-
arr[arr == nodata] = np.nan
|
|
104
|
+
raw_arr = arr.squeeze()
|
|
86
105
|
|
|
87
|
-
|
|
106
|
+
# Cast integers to float16 to handle NaN values
|
|
107
|
+
if np.issubdtype(raw_arr.dtype, np.integer):
|
|
108
|
+
arr = raw_arr.astype(np.float16)
|
|
109
|
+
else:
|
|
110
|
+
arr = raw_arr
|
|
111
|
+
|
|
112
|
+
if nodata is not None:
|
|
113
|
+
arr[raw_arr == nodata] = np.nan
|
|
88
114
|
|
|
89
115
|
raster_meta = RasterMeta(cell_size=cell_size, crs=crs, transform=transform)
|
|
90
|
-
raster_obj =
|
|
116
|
+
raster_obj = Raster(arr=arr, raster_meta=raster_meta)
|
|
91
117
|
return raster_obj
|
|
92
118
|
finally:
|
|
93
119
|
for src in sources:
|