pad-python 4.0.0__cp314-none-macosx_10_15_x86_64.whl
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.
libpad_python.dylib
ADDED
|
Binary file
|
|
@@ -0,0 +1,351 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pad_python
|
|
3
|
+
Version: 4.0.0
|
|
4
|
+
Summary: Precipitation Attribution Distance for fields on a planar grid
|
|
5
|
+
Author-email: Gregor Skok <Gregor.Skok@fmf.uni-lj.si>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/skokg/PAD
|
|
8
|
+
Project-URL: Repository, https://github.com/skokg/PAD
|
|
9
|
+
Project-URL: Issues, https://github.com/skokg/PAD/issues
|
|
10
|
+
Classifier: Intended Audience :: Science/Research
|
|
11
|
+
Classifier: Operating System :: POSIX :: Linux
|
|
12
|
+
Classifier: Operating System :: Microsoft :: Windows
|
|
13
|
+
Classifier: Operating System :: MacOS :: MacOS X
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
16
|
+
Classifier: Programming Language :: C++
|
|
17
|
+
Classifier: Topic :: Scientific/Engineering :: Atmospheric Science
|
|
18
|
+
Requires-Python: >=3.10
|
|
19
|
+
Description-Content-Type: text/markdown
|
|
20
|
+
License-File: LICENSE
|
|
21
|
+
Requires-Dist: numpy>=1.23
|
|
22
|
+
Provides-Extra: examples
|
|
23
|
+
Requires-Dist: netCDF4; extra == "examples"
|
|
24
|
+
Requires-Dist: matplotlib>=3.5; extra == "examples"
|
|
25
|
+
Requires-Dist: cartopy; extra == "examples"
|
|
26
|
+
Dynamic: license-file
|
|
27
|
+
|
|
28
|
+
# PAD Python: Precipitation Attribution Distance
|
|
29
|
+
|
|
30
|
+
Precipitation Attribution Distance (PAD) measures spatial displacement between
|
|
31
|
+
precipitation fields on a planar grid. Its results describe how much
|
|
32
|
+
precipitation is attributed between locations, the distances between those
|
|
33
|
+
locations, and the amounts left unattributed in each field.
|
|
34
|
+
|
|
35
|
+
This Python library provides a NumPy interface to the C++ implementation.
|
|
36
|
+
Attribution uses randomized nearest-neighbor searches with k-d trees.
|
|
37
|
+
Normalization, overlap removal and distance limits are controlled from Python.
|
|
38
|
+
|
|
39
|
+
The package is distributed and imported as `pad_python`.
|
|
40
|
+
|
|
41
|
+
GitHub repository: [skokg/PAD](https://github.com/skokg/PAD).
|
|
42
|
+
|
|
43
|
+
## Installation
|
|
44
|
+
|
|
45
|
+
Python 3.10 or newer is required. Install the package with:
|
|
46
|
+
|
|
47
|
+
```sh
|
|
48
|
+
pip install pad_python
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
To install from source:
|
|
52
|
+
|
|
53
|
+
```sh
|
|
54
|
+
pip install --no-binary=pad_python pad_python
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Compiling from source requires a C++ compiler: GCC or Clang on Linux, MSVC Build
|
|
58
|
+
Tools on Windows, or Xcode Command Line Tools on macOS.
|
|
59
|
+
|
|
60
|
+
The package can also be installed from conda-forge:
|
|
61
|
+
|
|
62
|
+
```sh
|
|
63
|
+
conda install -c conda-forge pad_python
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
Alternatively, download the source from the
|
|
67
|
+
[GitHub repository](https://github.com/skokg/PAD) and run `make` in the
|
|
68
|
+
project's root directory on Linux. This requires GNU Make and a C++ compiler.
|
|
69
|
+
Running `make` creates `libpad_python.so` in the `src` folder.
|
|
70
|
+
Copy `pad_python.py` and `libpad_python.so` from `src` into the folder
|
|
71
|
+
containing your Python script. NumPy must also be installed.
|
|
72
|
+
No compiled library is included in this source checkout.
|
|
73
|
+
|
|
74
|
+
## Usage
|
|
75
|
+
|
|
76
|
+
Import the library with:
|
|
77
|
+
|
|
78
|
+
```python
|
|
79
|
+
import pad_python as pad
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
Inputs are precipitation amounts in two equally shaped, nonempty 2D NumPy
|
|
83
|
+
arrays. A field has shape `(y, x)` and represents a rectangular grid with equal
|
|
84
|
+
spacing along both axes. Values must be finite and nonnegative, with at least
|
|
85
|
+
one positive value in each field. Masked arrays are not supported.
|
|
86
|
+
|
|
87
|
+
The two main calculation functions are:
|
|
88
|
+
|
|
89
|
+
- `pad.calculate_PAD_attributions()`: attribute precipitation between fields
|
|
90
|
+
and return the attribution records and the two residual fields.
|
|
91
|
+
- `pad.calculate_PAD_distance_from_attributions()`: calculate the amount-weighted
|
|
92
|
+
mean attribution distance from those records.
|
|
93
|
+
|
|
94
|
+
By default, `normalize=True` independently scales each field to sum to one,
|
|
95
|
+
`remove_overlap=True` attributes pointwise overlap at zero distance first,
|
|
96
|
+
and `distance_cutoff=None` imposes no distance limit.
|
|
97
|
+
A fixed `random_seed` makes repeated calculations
|
|
98
|
+
reproducible for identical inputs and the same native-library build.
|
|
99
|
+
The selected seed is printed when nearest-neighbor work is needed.
|
|
100
|
+
|
|
101
|
+
Distances and cutoffs are in grid cells. Multiply a distance by the common
|
|
102
|
+
grid spacing to express it in physical units. With `normalize=False`, amounts
|
|
103
|
+
and residuals retain the input units. For equal-area cells, rainfall depths
|
|
104
|
+
give the same weighting as water volumes; the library does not calculate
|
|
105
|
+
cell areas.
|
|
106
|
+
|
|
107
|
+
PAD averages only attributed amounts. A cutoff can leave precipitation
|
|
108
|
+
unmatched; if there are no attributions, the mean distance is undefined and
|
|
109
|
+
the distance function raises `ValueError`. Detailed parameters and returned
|
|
110
|
+
columns are described in **Function descriptions** below.
|
|
111
|
+
|
|
112
|
+
This interface supports equally shaped planar fields. For latitude/longitude
|
|
113
|
+
coordinates and spherical distances, see
|
|
114
|
+
[PAD on Sphere](https://github.com/skokg/PAD_on_Sphere).
|
|
115
|
+
|
|
116
|
+
### Idealized planar field example
|
|
117
|
+
|
|
118
|
+
This example uses a grid with 120 rows and 200 columns. A rectangular region
|
|
119
|
+
has 10 mm of observed rainfall; the forecast contains the same region shifted
|
|
120
|
+
60 grid cells to the right, leaving 20 dry columns between the rectangles.
|
|
121
|
+
An attribution cutoff of 50 grid cells leaves the outer edges unmatched.
|
|
122
|
+
|
|
123
|
+
The grid cells are assumed to have equal areas. Setting `normalize=False`
|
|
124
|
+
retains rainfall depths in mm, so the residual fields can be plotted directly.
|
|
125
|
+
Weighting by depth is equivalent to weighting by volume on this grid.
|
|
126
|
+
|
|
127
|
+
```python
|
|
128
|
+
import numpy as np
|
|
129
|
+
import pad_python as pad
|
|
130
|
+
|
|
131
|
+
# Rainfall on a planar grid with equal spacing in both directions.
|
|
132
|
+
cutoff_cells = 50
|
|
133
|
+
observed = np.zeros((120, 200))
|
|
134
|
+
observed[40:80, 30:70] = 10.0
|
|
135
|
+
forecast = np.roll(observed, shift=60, axis=1) # No rainfall wraps at the boundary.
|
|
136
|
+
|
|
137
|
+
# Keep original amounts: attribution weights and residuals are in mm.
|
|
138
|
+
attributions, remaining_forecast, remaining_observed = pad.calculate_PAD_attributions(
|
|
139
|
+
forecast, observed, normalize=False, distance_cutoff=cutoff_cells, random_seed=42,
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
distance_cells = pad.calculate_PAD_distance_from_attributions(attributions)
|
|
143
|
+
print(f"PAD: {distance_cells:.2f} grid cells")
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
To display the fields, attributions and unattributed precipitation, append:
|
|
147
|
+
|
|
148
|
+
```python
|
|
149
|
+
import matplotlib.pyplot as plt
|
|
150
|
+
|
|
151
|
+
fig, axes = plt.subplots(2, 2, figsize=(14, 10), constrained_layout=True)
|
|
152
|
+
axes = axes.ravel()
|
|
153
|
+
ax_pdf = axes[2]
|
|
154
|
+
|
|
155
|
+
for ax, fields, title in zip(axes[:2],
|
|
156
|
+
[(observed, forecast), (remaining_observed, remaining_forecast)],
|
|
157
|
+
["Rainfall and sampled attributions", "Unattributed precipitation"]):
|
|
158
|
+
for rain, cmap, label in zip(fields, ["Blues", "Reds"], ["Observed", "Forecast"]):
|
|
159
|
+
image = ax.imshow(np.ma.masked_equal(rain, 0), cmap=cmap,
|
|
160
|
+
vmin=0, vmax=10, alpha=0.6, origin="lower",
|
|
161
|
+
interpolation="nearest")
|
|
162
|
+
fig.colorbar(image, ax=ax, location="right", label=f"{label} (mm)", shrink=0.7)
|
|
163
|
+
ax.set(xlabel="Column (grid cells)", ylabel="Row (grid cells)",
|
|
164
|
+
title=f"{title} (cutoff: {cutoff_cells} grid cells)")
|
|
165
|
+
|
|
166
|
+
# Plot at most 100 attributions; all are still used to calculate PAD.
|
|
167
|
+
indices = np.random.default_rng(42).choice(len(attributions),
|
|
168
|
+
min(100, len(attributions)), replace=False)
|
|
169
|
+
for x1, y1, x2, y2 in attributions[indices, 2:]:
|
|
170
|
+
axes[0].plot([x1, x2], [y1, y2], "k.-", linewidth=0.8, markersize=3)
|
|
171
|
+
|
|
172
|
+
# Amount-weighted PDF using all attributions (also volume-weighted here).
|
|
173
|
+
ax_pdf.hist(attributions[:, 0], bins=25,
|
|
174
|
+
weights=attributions[:, 1], density=True, color="steelblue")
|
|
175
|
+
ax_pdf.axvline(distance_cells, color="black", linestyle="--",
|
|
176
|
+
label=f"PAD distance: {distance_cells:.1f} grid cells")
|
|
177
|
+
ax_pdf.legend()
|
|
178
|
+
ax_pdf.set(xlabel="Attribution distance (grid cells)",
|
|
179
|
+
ylabel="Probability density (per grid cell)",
|
|
180
|
+
title=f"Attribution PDF (cutoff: {cutoff_cells} grid cells)")
|
|
181
|
+
|
|
182
|
+
# Amount-weighted two-dimensional displacement PDF.
|
|
183
|
+
displacement = attributions[:, 4:6] - attributions[:, 2:4]
|
|
184
|
+
limit = max(1., np.abs(displacement).max())
|
|
185
|
+
ax_displacement = axes[3]
|
|
186
|
+
hist = ax_displacement.hist2d(*displacement.T, bins=25, range=[(-limit, limit)] * 2,
|
|
187
|
+
weights=attributions[:, 1], density=True, cmap="Blues")
|
|
188
|
+
fig.colorbar(hist[3], ax=ax_displacement, label="Probability density (per grid cell²)")
|
|
189
|
+
ax_displacement.axhline(0, color="grey", linewidth=0.5)
|
|
190
|
+
ax_displacement.axvline(0, color="grey", linewidth=0.5)
|
|
191
|
+
ax_displacement.set(xlabel="Δx (grid cells)", ylabel="Δy (grid cells)",
|
|
192
|
+
title="2D attribution PDF", aspect="equal")
|
|
193
|
+
plt.show()
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
Observed rainfall is blue and forecast rainfall is red. Dry cells are
|
|
197
|
+
transparent. The top-left panel shows a reproducible random sample of up to
|
|
198
|
+
100 attributions in black; zero-distance matches would appear as points.
|
|
199
|
+
The top-right panel shows observed and forecast residuals, both as
|
|
200
|
+
nonnegative rainfall depths in mm. Each field panel has two colourbars on
|
|
201
|
+
the right. The bottom-left panel is an amount-weighted PDF histogram of all
|
|
202
|
+
attribution distances; the bottom-right panel shows their two-dimensional
|
|
203
|
+
displacement PDF. Displacements are `Δx = x2 - x1` and `Δy = y2 - y1`, from
|
|
204
|
+
the first input field to the second. Here the forecast is the first input,
|
|
205
|
+
so attribution lines point toward the observed rectangle on its left.
|
|
206
|
+
Unattributed precipitation is excluded from both histograms.
|
|
207
|
+
This planar example needs NumPy and Matplotlib, without geographic map data.
|
|
208
|
+
|
|
209
|
+
PAD is the amount-weighted mean attribution distance, equivalent to the
|
|
210
|
+
volume-weighted mean for these equal-area cells. It is not simply the
|
|
211
|
+
60-cell shift of the rectangle. Observed columns 30–39 and forecast columns
|
|
212
|
+
120–129 are more than 50 grid cells from any counterpart and remain
|
|
213
|
+
unattributed. Other residuals depend on the attribution. Returned residuals
|
|
214
|
+
are already in mm because normalization is disabled; with the default
|
|
215
|
+
`normalize=True`, they would instead be in normalized amount units.
|
|
216
|
+
|
|
217
|
+
## Further examples
|
|
218
|
+
|
|
219
|
+
The source distribution and GitHub repository include two scripts and their
|
|
220
|
+
sample NetCDF data in `examples/`. Examples and data are not installed into
|
|
221
|
+
`site-packages` by a wheel. Download and extract the source to use them.
|
|
222
|
+
The examples additionally require Matplotlib, netCDF4 and Cartopy.
|
|
223
|
+
|
|
224
|
+
Then run the examples from the downloaded source root:
|
|
225
|
+
|
|
226
|
+
```sh
|
|
227
|
+
python examples/example_PAD_01.py
|
|
228
|
+
python examples/example_PAD_02.py
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
| Script | Input | Output |
|
|
232
|
+
| --- | --- | --- |
|
|
233
|
+
| `example_PAD_01.py` | Two synthetic fields, shape `(300, 500)`, with displaced rainy rectangles. | Printed attributions and PAD; a 2×2 figure with fields and sampled attribution lines, residuals, distance PDF and displacement PDF. |
|
|
234
|
+
| `example_PAD_02.py` | `sample_data/sample_field_A.nc` and `sample_data/sample_field_B.nc`, relative to the script. | Printed attributions and PAD; the same 2×2 layout with maps in the top row. |
|
|
235
|
+
|
|
236
|
+
Both scripts display figures rather than save output files. They can also be
|
|
237
|
+
run from another working directory because the second resolves its data paths
|
|
238
|
+
relative to its own file. Longitude/latitude in the second example are used
|
|
239
|
+
only for map display; the PAD calculation still uses grid-cell distances.
|
|
240
|
+
The second script disables normalization and retains the original amount units;
|
|
241
|
+
it applies no distance cutoff by default.
|
|
242
|
+
Its precipitation colours use a square-root stretch, and the 2D attribution
|
|
243
|
+
PDF uses logarithmic colours to reveal weaker features.
|
|
244
|
+
Cartopy may download coastline data the first time the map example is used.
|
|
245
|
+
|
|
246
|
+
## Function descriptions
|
|
247
|
+
|
|
248
|
+
The following describes the two calculation functions used above. Attribution
|
|
249
|
+
records and residual fields are NumPy arrays. Input fields are never modified.
|
|
250
|
+
|
|
251
|
+
### `pad.calculate_PAD_attributions(fa, fb, remove_overlap=True, distance_cutoff=None, random_seed=None, normalize=True)`
|
|
252
|
+
|
|
253
|
+
**Inputs:** `fa` and `fb` are real numeric NumPy arrays shaped `(y, x)`, with
|
|
254
|
+
identical shapes and nonempty dimensions. Each field must contain only finite,
|
|
255
|
+
nonnegative values and at least one positive value. Masked arrays are rejected.
|
|
256
|
+
The fields represent the same rectangular grid, with equal spacing along x
|
|
257
|
+
and y; no separate coordinate arrays are supplied. Values are converted to
|
|
258
|
+
`float64` for calculation and must remain finite, with a positive amount in
|
|
259
|
+
each field after conversion.
|
|
260
|
+
|
|
261
|
+
| Parameter | Meaning |
|
|
262
|
+
| --- | --- |
|
|
263
|
+
| `remove_overlap` | Boolean, default `True`. Attribute the pointwise minimum of the two prepared fields at zero distance before nearest-neighbor attribution. `False` skips this preprocessing, but zero-distance matches can still occur. |
|
|
264
|
+
| `distance_cutoff` | `None` for no limit, or a finite nonnegative Euclidean distance in grid cells. Matches exactly on the cutoff are permitted. Zero allows only zero-distance matches. |
|
|
265
|
+
| `random_seed` | `None` or `-1` chooses a random seed. An integer from `0` to `4294967295` selects a seed explicitly; Boolean values are rejected. A fixed seed reproduces results for identical inputs and the same native-library build. |
|
|
266
|
+
| `normalize` | Boolean, default `True`. Independently scale each field to sum to one before overlap removal and attribution. `False` retains the original amounts. |
|
|
267
|
+
|
|
268
|
+
**Output:** A list `[attributions, remaining1, remaining2]`:
|
|
269
|
+
|
|
270
|
+
| Array | Contents |
|
|
271
|
+
| --- | --- |
|
|
272
|
+
| `attributions` | A `float64` array shaped `(N, 6)`, with one row per attributed amount. It has shape `(0, 6)` when no matches are possible under the cutoff. |
|
|
273
|
+
| `remaining1` | A `float64` array with the shape of `fa`, containing its remaining unattributed amounts. |
|
|
274
|
+
| `remaining2` | A `float64` array with the shape of `fb`, containing its remaining unattributed amounts. |
|
|
275
|
+
|
|
276
|
+
The six attribution columns are:
|
|
277
|
+
|
|
278
|
+
| Column | Contents |
|
|
279
|
+
| --- | --- |
|
|
280
|
+
| `0`: distance | Euclidean distance between the endpoints, in grid cells. |
|
|
281
|
+
| `1`: amount | Amount attributed between the endpoints. |
|
|
282
|
+
| `2`: x1 | Zero-based column of the endpoint in `fa`. |
|
|
283
|
+
| `3`: y1 | Zero-based row of the endpoint in `fa`. |
|
|
284
|
+
| `4`: x2 | Zero-based column of the endpoint in `fb`. |
|
|
285
|
+
| `5`: y2 | Zero-based row of the endpoint in `fb`. |
|
|
286
|
+
|
|
287
|
+
Endpoint coordinates represent integer grid indices stored as `float64` values.
|
|
288
|
+
For array indexing, convert them to integers and use `[y, x]` order.
|
|
289
|
+
Attribution amounts and both residual fields use normalized amount units when
|
|
290
|
+
`normalize=True`, and the original input units when `normalize=False`.
|
|
291
|
+
Returned arrays do not share storage with the inputs and remain valid after
|
|
292
|
+
later calculation calls.
|
|
293
|
+
|
|
294
|
+
**Functionality:** Validates the fields and options, optionally normalizes each
|
|
295
|
+
field, and optionally attributes their pointwise overlap. It then alternates
|
|
296
|
+
between fields, selecting points in randomized order and attributing amounts
|
|
297
|
+
to the nearest remaining precipitation in the opposite field. Each match
|
|
298
|
+
transfers the smaller available amount, consuming at least one endpoint.
|
|
299
|
+
Points without an acceptable neighbor under the cutoff retain their amounts
|
|
300
|
+
in the residual output. Attribution stops when one field has no points left
|
|
301
|
+
for further searches.
|
|
302
|
+
|
|
303
|
+
The returned rows describe the attribution, rather than the scalar PAD value;
|
|
304
|
+
pass them to `calculate_PAD_distance_from_attributions` for that value.
|
|
305
|
+
Nearest-neighbor work prints the selected random seed, including when a seed
|
|
306
|
+
was supplied explicitly. No seed is printed when overlap removal leaves no
|
|
307
|
+
nearest-neighbor work. Invalid inputs or options raise `TypeError` or
|
|
308
|
+
`ValueError`; native calculation failures are reported as `RuntimeError`.
|
|
309
|
+
|
|
310
|
+
### `pad.calculate_PAD_distance_from_attributions(PAD_attributions)`
|
|
311
|
+
|
|
312
|
+
**Input:** A nonempty six-column attribution array, normally returned by
|
|
313
|
+
`calculate_PAD_attributions`. Array-like input convertible to `float64` is
|
|
314
|
+
also accepted. Column 0 contains distances and column 1 contains attribution
|
|
315
|
+
amounts. Both columns must be finite and nonnegative, with at least one
|
|
316
|
+
strictly positive amount. The four endpoint columns are not used in the
|
|
317
|
+
distance calculation.
|
|
318
|
+
|
|
319
|
+
**Output:** A Python `float` containing the amount-weighted mean attribution
|
|
320
|
+
distance. For attributions produced by this library, the unit is grid cells.
|
|
321
|
+
The result is `0.0` when all positively weighted distances are zero.
|
|
322
|
+
|
|
323
|
+
**Functionality:** Computes `sum(distance * amount) / sum(amount)` using scaled
|
|
324
|
+
values to reduce overflow risk. Zero-weight rows contribute nothing and are
|
|
325
|
+
excluded before choosing the distance scale; their distances must still pass
|
|
326
|
+
input validation. The input array is not modified.
|
|
327
|
+
|
|
328
|
+
Only attributed amounts enter the mean. Residual precipitation is not included,
|
|
329
|
+
and the cutoff used to construct the attributions therefore affects which
|
|
330
|
+
amounts contribute. An empty array, incorrect number of columns, invalid
|
|
331
|
+
distances or amounts, or entirely zero amounts raises `ValueError`; PAD is
|
|
332
|
+
undefined without a positive attributed amount.
|
|
333
|
+
|
|
334
|
+
## Author and license
|
|
335
|
+
|
|
336
|
+
Gregor Skok, Faculty of Mathematics and Physics, University of Ljubljana,
|
|
337
|
+
Slovenia.
|
|
338
|
+
|
|
339
|
+
Email: [Gregor.Skok@fmf.uni-lj.si](mailto:Gregor.Skok@fmf.uni-lj.si).
|
|
340
|
+
Please contact the author if you have questions or trouble using the package.
|
|
341
|
+
|
|
342
|
+
Licensed under the MIT License. See LICENSE, which also includes
|
|
343
|
+
the modified third-party k-d tree notice.
|
|
344
|
+
|
|
345
|
+
## References
|
|
346
|
+
|
|
347
|
+
The paper below describes precipitation attribution distance; please cite it
|
|
348
|
+
when publishing work using PAD:
|
|
349
|
+
|
|
350
|
+
- Skok, G. (2023). Precipitation attribution distance. Atmospheric Research, 295.
|
|
351
|
+
[https://doi.org/10.1016/j.atmosres.2023.106998](https://doi.org/10.1016/j.atmosres.2023.106998)
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
pad_python.py,sha256=E5pdWZj8XHttHZ2Owo1y04iI6-eSm1e7i9iAUcJTevo,10465
|
|
2
|
+
libpad_python.dylib,sha256=ZaLZ2cqpO2ErlexGdooe7mlyLLDIxUU_nSRZfo7MfIo,52200
|
|
3
|
+
pad_python-4.0.0.dist-info/RECORD,,
|
|
4
|
+
pad_python-4.0.0.dist-info/WHEEL,sha256=MdQE6WgGThpwVKyLHve-fOe1kp5jqSMJVoZiC4XrQ84,137
|
|
5
|
+
pad_python-4.0.0.dist-info/top_level.txt,sha256=LFLTG4mxINgSV4zgzGlM30ByO8SH38cxl1hqckIrQxQ,25
|
|
6
|
+
pad_python-4.0.0.dist-info/METADATA,sha256=uCN8l_IUJX1_ZNMNahvKzItrbKzciRGW7baS5uXT5c4,17126
|
|
7
|
+
pad_python-4.0.0.dist-info/licenses/LICENSE,sha256=s_K7B8QvZfGhJR6XU7EJ9AWWXfCZf8mE63ErYbLzHho,2415
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2023 Gregor Skok
|
|
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.
|
|
22
|
+
KD-tree implementation
|
|
23
|
+
======================
|
|
24
|
+
|
|
25
|
+
src/cpp/CU_kdtree_with_index.cc contains an implementation
|
|
26
|
+
originally by SuYuxi, subsequently modified by Gregor Skok. The original notice
|
|
27
|
+
is preserved in that file and reproduced here for installed distributions.
|
|
28
|
+
|
|
29
|
+
MIT License
|
|
30
|
+
|
|
31
|
+
Copyright (c) 2019 SuYuxi
|
|
32
|
+
|
|
33
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
34
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
35
|
+
in the Software without restriction, including without limitation the rights
|
|
36
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
37
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
38
|
+
furnished to do so, subject to the following conditions:
|
|
39
|
+
|
|
40
|
+
The above copyright notice and this permission notice shall be included in all
|
|
41
|
+
copies or substantial portions of the Software.
|
|
42
|
+
|
|
43
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
44
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
45
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
46
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
47
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
48
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
49
|
+
SOFTWARE.
|
pad_python.py
ADDED
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
"""Planar PAD using the C++ wrapper (planar ABI 2; rebuild the shared library).
|
|
2
|
+
|
|
3
|
+
All results have six columns: Euclidean distance, amount, x1, y1, x2, y2.
|
|
4
|
+
The 2D-field API optionally normalizes each field (enabled by default) and always
|
|
5
|
+
return [attributions, remaining1, remaining2]. No cutoff is used by default.
|
|
6
|
+
"""
|
|
7
|
+
import ctypes as ct
|
|
8
|
+
import operator
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
import sys
|
|
11
|
+
|
|
12
|
+
import numpy as np
|
|
13
|
+
|
|
14
|
+
# Windows wheel repair inserts the bundled-DLL search path here.
|
|
15
|
+
# delvewheel: patch
|
|
16
|
+
|
|
17
|
+
__all__ = [
|
|
18
|
+
"check_input_fields",
|
|
19
|
+
"calculate_PAD_attributions",
|
|
20
|
+
"calculate_PAD_distance_from_attributions",
|
|
21
|
+
]
|
|
22
|
+
|
|
23
|
+
_native_library_names = {
|
|
24
|
+
"linux": "libpad_python.so",
|
|
25
|
+
"win32": "pad_python.dll",
|
|
26
|
+
"darwin": "libpad_python.dylib",
|
|
27
|
+
}
|
|
28
|
+
if sys.platform not in _native_library_names:
|
|
29
|
+
raise ImportError("pad_python supports Linux, Windows and macOS.")
|
|
30
|
+
_library_path = Path(__file__).resolve().with_name(_native_library_names[sys.platform])
|
|
31
|
+
_compile_advice = (
|
|
32
|
+
"Reinstall pad_python using a compatible wheel, or build from source with "
|
|
33
|
+
"a C++ compiler. For manual Linux builds, run make in the repository root "
|
|
34
|
+
"to create src/libpad_python.so beside src/pad_python.py."
|
|
35
|
+
)
|
|
36
|
+
if not _library_path.is_file():
|
|
37
|
+
raise ImportError(
|
|
38
|
+
f"PAD shared library not found: {_library_path}. {_compile_advice}"
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
try:
|
|
42
|
+
libc = ct.CDLL(str(_library_path))
|
|
43
|
+
except OSError as exc:
|
|
44
|
+
raise ImportError(
|
|
45
|
+
f"Cannot load PAD shared library: {_library_path}. {_compile_advice}"
|
|
46
|
+
) from exc
|
|
47
|
+
try:
|
|
48
|
+
libc.PAD_planar_wrapper_abi_version.argtypes = []
|
|
49
|
+
libc.PAD_planar_wrapper_abi_version.restype = ct.c_int
|
|
50
|
+
except AttributeError as exc:
|
|
51
|
+
raise ImportError(
|
|
52
|
+
"Rebuild pad_python's native library: planar wrapper ABI 2 is required. "
|
|
53
|
+
+ _compile_advice
|
|
54
|
+
) from exc
|
|
55
|
+
if libc.PAD_planar_wrapper_abi_version() != 2:
|
|
56
|
+
raise ImportError(
|
|
57
|
+
"Incompatible PAD shared library: planar wrapper ABI 2 is required. "
|
|
58
|
+
+ _compile_advice
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
ND_POINTER_1D = np.ctypeslib.ndpointer(
|
|
62
|
+
dtype=np.float64, ndim=1, flags=("C_CONTIGUOUS", "ALIGNED")
|
|
63
|
+
)
|
|
64
|
+
libc.free_mem_double_array.argtypes = [ct.POINTER(ct.c_double)]
|
|
65
|
+
libc.free_mem_double_array.restype = None
|
|
66
|
+
libc.PAD_last_error.argtypes = []
|
|
67
|
+
libc.PAD_last_error.restype = ct.c_char_p
|
|
68
|
+
libc.calculate_PAD_results_assume_same_grid_ctypes.argtypes = [
|
|
69
|
+
ND_POINTER_1D, ND_POINTER_1D, ND_POINTER_1D, ND_POINTER_1D,
|
|
70
|
+
ct.c_size_t, ct.POINTER(ct.c_size_t), ct.c_double, ct.c_int64,
|
|
71
|
+
]
|
|
72
|
+
libc.calculate_PAD_results_assume_same_grid_ctypes.restype = ct.POINTER(ct.c_double)
|
|
73
|
+
libc.calculate_PAD_results_assume_different_grid_ctypes.argtypes = [
|
|
74
|
+
ND_POINTER_1D, ND_POINTER_1D, ND_POINTER_1D, ct.c_size_t,
|
|
75
|
+
ND_POINTER_1D, ND_POINTER_1D, ND_POINTER_1D, ct.c_size_t,
|
|
76
|
+
ct.POINTER(ct.c_size_t), ct.c_double, ct.c_int64,
|
|
77
|
+
]
|
|
78
|
+
libc.calculate_PAD_results_assume_different_grid_ctypes.restype = ct.POINTER(ct.c_double)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def _check_array(array, name, ndim):
|
|
82
|
+
if not isinstance(array, np.ndarray) or isinstance(array, np.ma.MaskedArray):
|
|
83
|
+
raise TypeError(f"{name} must be an unmasked NumPy array.")
|
|
84
|
+
if array.ndim != ndim or array.size == 0:
|
|
85
|
+
raise ValueError(f"{name} must be a nonempty {ndim}-dimensional array.")
|
|
86
|
+
if not np.issubdtype(array.dtype, np.number) or np.iscomplexobj(array):
|
|
87
|
+
raise TypeError(f"{name} must contain real numeric values.")
|
|
88
|
+
if not np.all(np.isfinite(array)):
|
|
89
|
+
raise ValueError(f"{name} must contain only finite values.")
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def _check_amounts(values, name):
|
|
93
|
+
if np.any(values < 0) or not np.any(values > 0):
|
|
94
|
+
raise ValueError(f"{name} must be nonnegative with at least one positive amount.")
|
|
95
|
+
if values.size > np.iinfo(np.int32).max:
|
|
96
|
+
raise ValueError("Too many grid points for the C++ tree.")
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def _array(array, name):
|
|
100
|
+
_check_array(array, name, 1)
|
|
101
|
+
converted = np.require(array, dtype=np.float64, requirements=["C", "A"])
|
|
102
|
+
if not np.all(np.isfinite(converted)):
|
|
103
|
+
raise ValueError(f"{name} must be representable as finite float64 values.")
|
|
104
|
+
return converted
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def check_input_fields(fa, fb):
|
|
108
|
+
"""Validate equally shaped, nonnegative 2D fields; raise on invalid input."""
|
|
109
|
+
for field, name in ((fa, "fa"), (fb, "fb")):
|
|
110
|
+
_check_array(field, name, 2)
|
|
111
|
+
_check_amounts(field, name)
|
|
112
|
+
if fa.shape != fb.shape:
|
|
113
|
+
raise ValueError("fa and fb must have identical shapes.")
|
|
114
|
+
return True
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def _seed(random_seed):
|
|
118
|
+
if random_seed is None:
|
|
119
|
+
return -1
|
|
120
|
+
if isinstance(random_seed, (bool, np.bool_)):
|
|
121
|
+
raise TypeError("random_seed must be an integer, not a boolean.")
|
|
122
|
+
seed = operator.index(random_seed)
|
|
123
|
+
if seed < -1 or seed > 0xFFFFFFFF:
|
|
124
|
+
raise ValueError("random_seed must be None, -1, or an unsigned 32-bit integer.")
|
|
125
|
+
return seed
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def _cutoff(distance_cutoff):
|
|
129
|
+
if distance_cutoff is None:
|
|
130
|
+
return np.finfo(np.float64).max
|
|
131
|
+
value = float(distance_cutoff)
|
|
132
|
+
if not np.isfinite(value) or value < 0:
|
|
133
|
+
raise ValueError("Euclidean distance cutoff must be finite and nonnegative, or None.")
|
|
134
|
+
return value
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def _normalized_values(values):
|
|
138
|
+
"""Normalize already validated float64 amounts without modifying inputs."""
|
|
139
|
+
scaled = values / values.max()
|
|
140
|
+
return np.ascontiguousarray(scaled / scaled.sum(), dtype=np.float64)
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def calculate_PAD_attributions(
|
|
144
|
+
fa, fb, remove_overlap=True, distance_cutoff=None, random_seed=None,
|
|
145
|
+
normalize=True,
|
|
146
|
+
):
|
|
147
|
+
"""Calculate planar PAD from two equally shaped, equidistant 2D fields.
|
|
148
|
+
|
|
149
|
+
Coordinates are generated from columns (x) and rows (y), shared by both
|
|
150
|
+
fields. Separate coordinate arrays, 1D inputs and different grids are not
|
|
151
|
+
supported by this Python API.
|
|
152
|
+
|
|
153
|
+
remove_overlap=True attributes pointwise overlap at distance zero first.
|
|
154
|
+
False skips preprocessing; zero-distance NN matches can still occur.
|
|
155
|
+
normalize=True independently scales each field to sum to one before
|
|
156
|
+
attribution; False keeps original amounts. Inputs are never modified.
|
|
157
|
+
|
|
158
|
+
distance_cutoff=None means no cutoff. A finite nonnegative Euclidean cutoff
|
|
159
|
+
is in grid-cell units, as are returned distances. Zero is valid.
|
|
160
|
+
random_seed=None or -1 chooses and prints a seed when NN work is needed;
|
|
161
|
+
uint32 seeds reproduce runs for identical inputs and library versions.
|
|
162
|
+
|
|
163
|
+
Always returns [attributions, remaining1, remaining2]. Attributions is
|
|
164
|
+
float64 with shape (N, 6), including (0, 6) when no matches are allowed:
|
|
165
|
+
distance, amount, x1, y1, x2, y2. Endpoint coordinates are zero-based
|
|
166
|
+
column (x) and row (y) positions, as in the original planar Python API.
|
|
167
|
+
Residuals have the original 2D field shapes, in normalized units when
|
|
168
|
+
normalize=True and original amount units otherwise.
|
|
169
|
+
"""
|
|
170
|
+
check_input_fields(fa, fb)
|
|
171
|
+
if not isinstance(remove_overlap, (bool, np.bool_)):
|
|
172
|
+
raise TypeError("remove_overlap must be a boolean.")
|
|
173
|
+
if not isinstance(normalize, (bool, np.bool_)):
|
|
174
|
+
raise TypeError("normalize must be a boolean.")
|
|
175
|
+
seed, cutoff = _seed(random_seed), _cutoff(distance_cutoff)
|
|
176
|
+
values1 = _array(fa.ravel(order="C"), "fa")
|
|
177
|
+
values2 = _array(fb.ravel(order="C"), "fb")
|
|
178
|
+
# Check again after conversion, which may underflow extended-precision data.
|
|
179
|
+
_check_amounts(values1, "fa")
|
|
180
|
+
_check_amounts(values2, "fb")
|
|
181
|
+
if normalize:
|
|
182
|
+
values1 = _normalized_values(values1)
|
|
183
|
+
values2 = _normalized_values(values2)
|
|
184
|
+
x = np.tile(np.arange(fa.shape[1], dtype=np.float64), fa.shape[0])
|
|
185
|
+
y = np.repeat(np.arange(fa.shape[0], dtype=np.float64), fa.shape[1])
|
|
186
|
+
count = ct.c_size_t()
|
|
187
|
+
if remove_overlap:
|
|
188
|
+
result = libc.calculate_PAD_results_assume_same_grid_ctypes(
|
|
189
|
+
x, y, values1, values2, values1.size, ct.byref(count), cutoff, seed,
|
|
190
|
+
)
|
|
191
|
+
else:
|
|
192
|
+
# Use the general C++ engine on the SAME generated grid to skip overlap.
|
|
193
|
+
result = libc.calculate_PAD_results_assume_different_grid_ctypes(
|
|
194
|
+
x, y, values1, values1.size, x, y, values2, values2.size,
|
|
195
|
+
ct.byref(count), cutoff, seed,
|
|
196
|
+
)
|
|
197
|
+
if not result:
|
|
198
|
+
message = libc.PAD_last_error()
|
|
199
|
+
raise RuntimeError(
|
|
200
|
+
message.decode("utf-8", errors="replace") if message else "Planar PAD calculation failed."
|
|
201
|
+
)
|
|
202
|
+
try:
|
|
203
|
+
n = count.value
|
|
204
|
+
if n > values1.size + values2.size:
|
|
205
|
+
raise RuntimeError("Invalid attribution count returned by PAD.")
|
|
206
|
+
total = 4 * n + values1.size + values2.size
|
|
207
|
+
# One owned copy instead of a list of Python float objects.
|
|
208
|
+
packed = np.ctypeslib.as_array(result, shape=(total,)).copy()
|
|
209
|
+
# The internal C++ buffer retains indices; expose original Python XY output.
|
|
210
|
+
records = packed[:4 * n].reshape(n, 4)
|
|
211
|
+
index1 = records[:, 2].astype(np.intp)
|
|
212
|
+
index2 = records[:, 3].astype(np.intp)
|
|
213
|
+
attributions = np.empty((n, 6), dtype=np.float64)
|
|
214
|
+
attributions[:, :2] = records[:, :2]
|
|
215
|
+
attributions[:, 2] = x[index1]
|
|
216
|
+
attributions[:, 3] = y[index1]
|
|
217
|
+
attributions[:, 4] = x[index2]
|
|
218
|
+
attributions[:, 5] = y[index2]
|
|
219
|
+
remaining1 = packed[4 * n:4 * n + values1.size]
|
|
220
|
+
remaining2 = packed[4 * n + values1.size:]
|
|
221
|
+
return [attributions, remaining1.reshape(fa.shape), remaining2.reshape(fb.shape)]
|
|
222
|
+
finally:
|
|
223
|
+
libc.free_mem_double_array(result)
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
def calculate_PAD_distance_from_attributions(PAD_attributions):
|
|
227
|
+
"""Return the amount-weighted mean distance; empty results are undefined."""
|
|
228
|
+
rows = np.asarray(PAD_attributions, dtype=np.float64)
|
|
229
|
+
if rows.ndim != 2 or rows.shape[1] != 6 or rows.shape[0] == 0:
|
|
230
|
+
raise ValueError("PAD requires a nonempty six-column attribution array.")
|
|
231
|
+
distances, weights = rows[:, 0], rows[:, 1]
|
|
232
|
+
if (not np.all(np.isfinite(distances)) or not np.all(np.isfinite(weights))
|
|
233
|
+
or np.any(distances < 0) or np.any(weights < 0) or not np.any(weights > 0)):
|
|
234
|
+
raise ValueError("PAD requires finite nonnegative distances and positive total weight.")
|
|
235
|
+
# Zero-weight rows contribute nothing and must not set the distance scale:
|
|
236
|
+
# an irrelevant huge distance could otherwise underflow all useful ratios.
|
|
237
|
+
positive = weights > 0
|
|
238
|
+
distances, weights = distances[positive], weights[positive]
|
|
239
|
+
weights = weights / weights.max()
|
|
240
|
+
scale = distances.max()
|
|
241
|
+
if scale == 0:
|
|
242
|
+
return 0.0
|
|
243
|
+
fraction = np.sum((distances / scale) * weights) / np.sum(weights)
|
|
244
|
+
return float(min(fraction, 1.0) * scale)
|