dct-toolkit 1.0.0__tar.gz → 2.0.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.
- {dct_toolkit-1.0.0 → dct_toolkit-2.0.0}/AGENTS.md +13 -8
- {dct_toolkit-1.0.0 → dct_toolkit-2.0.0}/CHANGELOG.md +43 -0
- {dct_toolkit-1.0.0 → dct_toolkit-2.0.0}/PKG-INFO +117 -8
- {dct_toolkit-1.0.0 → dct_toolkit-2.0.0}/README.md +116 -7
- {dct_toolkit-1.0.0 → dct_toolkit-2.0.0}/dct_toolkit/__init__.py +5 -3
- dct_toolkit-2.0.0/dct_toolkit/_gaussian.py +50 -0
- {dct_toolkit-1.0.0 → dct_toolkit-2.0.0}/dct_toolkit/_kernels.py +1 -1
- {dct_toolkit-1.0.0 → dct_toolkit-2.0.0}/dct_toolkit/cartesian.py +7 -3
- {dct_toolkit-1.0.0 → dct_toolkit-2.0.0}/dct_toolkit/core.py +15 -3
- {dct_toolkit-1.0.0 → dct_toolkit-2.0.0}/dct_toolkit/polar.py +26 -6
- {dct_toolkit-1.0.0 → dct_toolkit-2.0.0}/dct_toolkit/stats.py +11 -1
- {dct_toolkit-1.0.0 → dct_toolkit-2.0.0}/docs/API_REFERENCE.md +37 -10
- {dct_toolkit-1.0.0 → dct_toolkit-2.0.0}/docs/MATHEMATICAL_BASIS.md +52 -5
- {dct_toolkit-1.0.0 → dct_toolkit-2.0.0}/docs/RELEASING.md +13 -13
- {dct_toolkit-1.0.0 → dct_toolkit-2.0.0}/pyproject.toml +1 -1
- {dct_toolkit-1.0.0 → dct_toolkit-2.0.0}/tests/test_core.py +4 -4
- dct_toolkit-2.0.0/tests/test_gaussian.py +194 -0
- {dct_toolkit-1.0.0 → dct_toolkit-2.0.0}/tests/test_polar_kernels.py +13 -4
- {dct_toolkit-1.0.0 → dct_toolkit-2.0.0}/tests/test_polar_nd.py +18 -9
- {dct_toolkit-1.0.0 → dct_toolkit-2.0.0}/.gitignore +0 -0
- {dct_toolkit-1.0.0 → dct_toolkit-2.0.0}/LICENSE +0 -0
- {dct_toolkit-1.0.0 → dct_toolkit-2.0.0}/dct_toolkit/_geometry.py +0 -0
- {dct_toolkit-1.0.0 → dct_toolkit-2.0.0}/dct_toolkit/_widths.py +0 -0
- {dct_toolkit-1.0.0 → dct_toolkit-2.0.0}/dct_toolkit/gap_filling.py +0 -0
- {dct_toolkit-1.0.0 → dct_toolkit-2.0.0}/examples/basic_polar.py +0 -0
- {dct_toolkit-1.0.0 → dct_toolkit-2.0.0}/examples/comprehensive_demo.py +0 -0
- {dct_toolkit-1.0.0 → dct_toolkit-2.0.0}/examples/nd_polar.py +0 -0
- {dct_toolkit-1.0.0 → dct_toolkit-2.0.0}/tests/test_api.py +0 -0
- {dct_toolkit-1.0.0 → dct_toolkit-2.0.0}/tests/test_cartesian.py +0 -0
- {dct_toolkit-1.0.0 → dct_toolkit-2.0.0}/tests/test_gap_filling.py +0 -0
- {dct_toolkit-1.0.0 → dct_toolkit-2.0.0}/tests/test_geometry_precision.py +0 -0
- {dct_toolkit-1.0.0 → dct_toolkit-2.0.0}/tests/test_polar.py +0 -0
- {dct_toolkit-1.0.0 → dct_toolkit-2.0.0}/tests/test_polar_nd_fallback.py +0 -0
- {dct_toolkit-1.0.0 → dct_toolkit-2.0.0}/tests/test_polar_nd_stats.py +0 -0
- {dct_toolkit-1.0.0 → dct_toolkit-2.0.0}/tests/test_polar_robustness.py +0 -0
- {dct_toolkit-1.0.0 → dct_toolkit-2.0.0}/tests/test_stats.py +0 -0
- {dct_toolkit-1.0.0 → dct_toolkit-2.0.0}/tests/test_stats_nd.py +0 -0
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
# AGENTS.md - DCT Toolkit Agent Guidelines
|
|
2
2
|
|
|
3
3
|
**Project**: dct-toolkit
|
|
4
|
-
**Version**:
|
|
4
|
+
**Version**: 2.0.0
|
|
5
5
|
|
|
6
|
-
**Status**:
|
|
6
|
+
**Status**: Full sampled Gaussian statistics release track
|
|
7
7
|
|
|
8
|
-
**Last Updated**: 2026-
|
|
8
|
+
**Last Updated**: 2026-11-11
|
|
9
9
|
|
|
10
10
|
---
|
|
11
11
|
|
|
@@ -330,7 +330,7 @@ Before declaring task complete:
|
|
|
330
330
|
- New public APIs need independent numerical tests and an independent code/docs review.
|
|
331
331
|
- Do not modify manuscripts or review trackers as part of this software task.
|
|
332
332
|
|
|
333
|
-
### Future Beyond
|
|
333
|
+
### Future Beyond v2.0.0
|
|
334
334
|
|
|
335
335
|
- [ ] Additional kernels (Savitzky-Golay, Hanning)
|
|
336
336
|
- [ ] Performance optimizations (numba?)
|
|
@@ -345,7 +345,11 @@ Before declaring task complete:
|
|
|
345
345
|
### Transfer Functions
|
|
346
346
|
- **Boxcar (Analytical)**: `H[k] = sin(W·θ)/(W·sin(θ))`, θ = πk/(2n)
|
|
347
347
|
- **Boxcar (Discrete)**: Sum of cosines for discrete kernels
|
|
348
|
-
- **Gaussian**:
|
|
348
|
+
- **Gaussian (default)**: full normalized sampled Gaussian, σ = width/√12;
|
|
349
|
+
evaluate its cosine sum or equivalent frequency-alias sum to float64 precision.
|
|
350
|
+
No fixed spatial cutoff; nonnegative spatial weights support statistical moments.
|
|
351
|
+
- **Gaussian (spectral compatibility)**: `gaussian_spectral` retains
|
|
352
|
+
`H[k] = exp(-0.5·(ω·σ)²)`, σ = width/√12; spatial weights can be signed.
|
|
349
353
|
|
|
350
354
|
### Normalized Convolution
|
|
351
355
|
Formula for robust mean with gaps:
|
|
@@ -456,8 +460,9 @@ git merge feature/hanning-window
|
|
|
456
460
|
|
|
457
461
|
### Current Version
|
|
458
462
|
|
|
459
|
-
- Version: `
|
|
460
|
-
|
|
463
|
+
- Version: `2.0.0`, using the full sampled Gaussian by default across smoothing
|
|
464
|
+
and statistics. Use `gaussian_spectral` to reproduce pre-2.0 Gaussian results.
|
|
465
|
+
- Preserve the historical `v1.0.0`, `v0.6.0`, and older release records.
|
|
461
466
|
- Do not tag, push, upload to PyPI/conda-forge, or create a public archival record
|
|
462
467
|
without approval.
|
|
463
468
|
- See `docs/RELEASING.md` for local artifact verification and release handoff.
|
|
@@ -465,7 +470,7 @@ git merge feature/hanning-window
|
|
|
465
470
|
### Gap-Filling Status
|
|
466
471
|
- Code exists in `dct_toolkit/gap_filling.py` (available for import directly)
|
|
467
472
|
- **Not exported** in public API (`__init__.py`) — research-internal
|
|
468
|
-
- Full gap-filling to be released in future
|
|
473
|
+
- Full gap-filling to be released in a future version after research matures
|
|
469
474
|
|
|
470
475
|
---
|
|
471
476
|
|
|
@@ -5,6 +5,49 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [Unreleased]
|
|
9
|
+
|
|
10
|
+
## [2.0.0] - 2026-11-11
|
|
11
|
+
|
|
12
|
+
Major release changing the default Gaussian's numerical definition. Existing
|
|
13
|
+
calls remain accepted, but Gaussian outputs change. For reproducible pre-2.0
|
|
14
|
+
results, explicitly select `'gaussian_spectral'` wherever Gaussian filtering was
|
|
15
|
+
previously used, including each Gaussian entry in hybrid kernel sequences.
|
|
16
|
+
Widths, coordinate conventions, and the public function signatures are unchanged.
|
|
17
|
+
|
|
18
|
+
### Breaking Changes
|
|
19
|
+
- Redefine `'gaussian'`, the default kernel, as the full normalized sampled
|
|
20
|
+
Gaussian rather than the exponential spectral response used in v1.0.0.
|
|
21
|
+
This affects existing smoothing/statistics calls even when `kernel_type`
|
|
22
|
+
is omitted. Use `'gaussian_spectral'` to retain the previous response.
|
|
23
|
+
|
|
24
|
+
### Changed
|
|
25
|
+
- Make the default `'gaussian'` kernel the full normalized sampled Gaussian,
|
|
26
|
+
evaluated to floating-point precision without a fixed spatial truncation.
|
|
27
|
+
Cartesian, legacy polar, explicit N-D polar, both boundaries, normalized
|
|
28
|
+
moments, prefill, and fallback now share this nonnegative spatial kernel.
|
|
29
|
+
Numerical results change, particularly for widths near or below one grid cell.
|
|
30
|
+
- Preserve the previous exponential response as `'gaussian_spectral'` for
|
|
31
|
+
reproducing v1.0.0 Gaussian results, including per-axis hybrid selections.
|
|
32
|
+
Its spatial weights can be signed; it is not the default for statistics.
|
|
33
|
+
|
|
34
|
+
### Fixed
|
|
35
|
+
- Remove substantive negative raw variances caused by Gaussian spatial ringing.
|
|
36
|
+
The final variance clamp remains for floating-point cancellation. Positivity
|
|
37
|
+
requires nonnegative kernels on every selected axis; fractional analytical
|
|
38
|
+
boxcars and `'gaussian_spectral'` do not provide that guarantee.
|
|
39
|
+
- Evaluate broad sampled Gaussians with an equivalent frequency-alias sum,
|
|
40
|
+
avoiding allocations proportional to potentially enormous effective widths.
|
|
41
|
+
|
|
42
|
+
### Documentation
|
|
43
|
+
- Document sampled Gaussian normalization, numerical evaluation, narrow-width
|
|
44
|
+
behavior, statistical guarantees, and migration to `'gaussian_spectral'`.
|
|
45
|
+
- Clarify required N-D polar metadata, the default physical width units, and
|
|
46
|
+
coordinate dependencies; add a runnable coordinate-free grid-width example.
|
|
47
|
+
- Compare Cartesian and explicit polar N-D processing, including boundaries,
|
|
48
|
+
axis selection, and the conditions for equivalent finite-data smoothing.
|
|
49
|
+
- Explain input-NaN restoration and the low-level polar width argument in the README.
|
|
50
|
+
|
|
8
51
|
## [1.0.0] - 2026-10-26
|
|
9
52
|
|
|
10
53
|
Geometry-aware N-D polar release, preserving the v0.6.0 legacy 2D interface.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: dct-toolkit
|
|
3
|
-
Version:
|
|
3
|
+
Version: 2.0.0
|
|
4
4
|
Summary: DCT/FFT-based convolutional statistics for Cartesian and polar data
|
|
5
5
|
Author-email: "Jairo M. Valdivia" <Jairo.ValdiviaPrado@colorado.edu>
|
|
6
6
|
License: MIT License
|
|
@@ -52,8 +52,10 @@ Description-Content-Type: text/markdown
|
|
|
52
52
|
`dct_toolkit` provides DCT/FFT-based smoothing and normalized-convolution statistics
|
|
53
53
|
for data with gaps. Runtime dependencies are NumPy and SciPy only.
|
|
54
54
|
|
|
55
|
-
**Version:
|
|
56
|
-
|
|
55
|
+
**Version: v2.0.0.** The default Gaussian now uses the full normalized sampled
|
|
56
|
+
kernel. To retain pre-2.0 Gaussian results, select `'gaussian_spectral'` explicitly.
|
|
57
|
+
Geometry-aware N-D polar processing requires version 1.0.0 or newer. Existing 2D
|
|
58
|
+
polar calls retain their index-based width conventions.
|
|
57
59
|
|
|
58
60
|
## Scope
|
|
59
61
|
|
|
@@ -148,7 +150,7 @@ order. Azimuth width adapts with range:
|
|
|
148
150
|
azimuth spacing in radians. Range width is in range gates. Gaussian
|
|
149
151
|
`sigma = effective_width / sqrt(12)` on the selected axis. Filtering applies
|
|
150
152
|
azimuth first, then range; both periodic and reflective azimuth support all
|
|
151
|
-
|
|
153
|
+
four kernel types, including `'boxcar_discrete'` and `'gaussian_spectral'`.
|
|
152
154
|
`az_res_deg` must be a finite, positive scalar in degrees. For gapped data,
|
|
153
155
|
periodic azimuth also wraps the nearest-neighbor prefill fallback across the
|
|
154
156
|
sweep seam; range distances remain nonperiodic.
|
|
@@ -164,10 +166,35 @@ var = dct.dct_variance(data, width=10.0)
|
|
|
164
166
|
std = dct.dct_std(data, width=10.0)
|
|
165
167
|
```
|
|
166
168
|
|
|
169
|
+
Version 2.0.0 changes the default `'gaussian'` to the **full normalized
|
|
170
|
+
sampled Gaussian**, evaluated to floating-point precision with no fixed spatial
|
|
171
|
+
cutoff. Its spatial weights are nonnegative, so normalized means stay within the
|
|
172
|
+
supported data range and population variances are nonnegative in exact arithmetic.
|
|
173
|
+
This applies to Cartesian and both polar modes, including reflective/periodic
|
|
174
|
+
boundaries and gap prefill. For mixed statistical kernels, use `'boxcar_discrete'`
|
|
175
|
+
on boxcar axes to retain this property; fractional analytical `'boxcar'` kernels
|
|
176
|
+
can have signed weights.
|
|
177
|
+
|
|
178
|
+
**Migration from v1.0.0 to v2.0.0:** Gaussian results change, especially at narrow widths.
|
|
179
|
+
Use `kernel_type="gaussian_spectral"` to retain the previous
|
|
180
|
+
`exp(-0.5 * (omega * sigma)**2)` response. Both Gaussian types use
|
|
181
|
+
`sigma = effective_grid_width / sqrt(12)`. Sampling means the new kernel's actual
|
|
182
|
+
second moment need not equal `sigma**2`; very small widths approach identity.
|
|
183
|
+
Variance still uses a numerical nonnegativity clamp; that clamp cannot repair
|
|
184
|
+
signed-weight statistical estimates.
|
|
185
|
+
|
|
186
|
+
For hybrid kernels, replace each previous `'gaussian'` entry with
|
|
187
|
+
`'gaussian_spectral'` to reproduce earlier results. For example,
|
|
188
|
+
`('gaussian', 'boxcar_discrete')` becomes
|
|
189
|
+
`('gaussian_spectral', 'boxcar_discrete')`. Set this explicitly for every
|
|
190
|
+
smoothing/statistics call that must retain the old response; prefill and
|
|
191
|
+
fallback inherit the selected kernels. Calls that omit `kernel_type` use the
|
|
192
|
+
new sampled Gaussian.
|
|
193
|
+
|
|
167
194
|
### 4) Mixed Kernels for a 3D Volume
|
|
168
195
|
|
|
169
196
|
For a volume stored in **(z, y, x)** order, apply Gaussian smoothing vertically
|
|
170
|
-
and boxcar smoothing horizontally:
|
|
197
|
+
and discrete boxcar smoothing horizontally:
|
|
171
198
|
|
|
172
199
|
```python
|
|
173
200
|
rng = np.random.default_rng(42)
|
|
@@ -176,7 +203,7 @@ volume = rng.standard_normal((16, 32, 32)) # (z, y, x)
|
|
|
176
203
|
smoothed = dct.dct_smooth(
|
|
177
204
|
volume,
|
|
178
205
|
width=(3.0, 5.0, 5.0),
|
|
179
|
-
kernel_type=("gaussian", "
|
|
206
|
+
kernel_type=("gaussian", "boxcar_discrete", "boxcar_discrete"),
|
|
180
207
|
)
|
|
181
208
|
|
|
182
209
|
# The same kernels work with normalized statistics on data containing NaNs.
|
|
@@ -184,7 +211,7 @@ volume[5:7, 12:15, 12:15] = np.nan
|
|
|
184
211
|
mean = dct.dct_mean(
|
|
185
212
|
volume,
|
|
186
213
|
width=(3.0, 5.0, 5.0),
|
|
187
|
-
kernel_type=("gaussian", "
|
|
214
|
+
kernel_type=("gaussian", "boxcar_discrete", "boxcar_discrete"),
|
|
188
215
|
)
|
|
189
216
|
```
|
|
190
217
|
|
|
@@ -196,7 +223,89 @@ kernel string applies that kernel to every axis. Equal widths with different
|
|
|
196
223
|
kernel types can still produce anisotropic smoothing. Legacy polar smoothing
|
|
197
224
|
accepts a kernel pair, using the `(azimuth, range)` convention shown above.
|
|
198
225
|
|
|
199
|
-
### 5) N-D Polar
|
|
226
|
+
### 5) N-D Polar Smoothing (v1.0.0)
|
|
227
|
+
|
|
228
|
+
#### Which inputs are required?
|
|
229
|
+
|
|
230
|
+
Use `coordinates="polar"` to select the polar backend and supply `axis_types`
|
|
231
|
+
to enable explicit N-D geometry. You do **not** need to supply all three of
|
|
232
|
+
`axis_types`, `axis_coords`, and `width_units` in every call:
|
|
233
|
+
|
|
234
|
+
| Argument | Required for explicit N-D polar processing? |
|
|
235
|
+
|----------|--------------------------------------------|
|
|
236
|
+
| `axis_types` | **Yes:** one role per array axis, in array order. |
|
|
237
|
+
| `axis_coords` | Only where physical width conversion or geometry dependencies need them. |
|
|
238
|
+
| `width_units` | **No:** defaults to `"physical"`; use `"grid"` for widths in cells. |
|
|
239
|
+
|
|
240
|
+
With `width_units="grid"`, no coordinate arrays are required. Physical filtering
|
|
241
|
+
needs coordinates on selected, non-singleton axes. Physical angular filtering
|
|
242
|
+
also needs actual positive range coordinates; physical azimuth filtering needs
|
|
243
|
+
elevation coordinates when that role exists. These geometry dependencies remain
|
|
244
|
+
required even when range/elevation are unfiltered. Coordinate entries for batch
|
|
245
|
+
or unfiltered axes may otherwise be `None`; supplied coordinates are always
|
|
246
|
+
validated.
|
|
247
|
+
|
|
248
|
+
Omitting `axis_types` does **not** infer N-D geometry: it retains the legacy
|
|
249
|
+
2D `(azimuth, range)` mode described above.
|
|
250
|
+
|
|
251
|
+
#### Start with grid widths: no coordinates needed
|
|
252
|
+
|
|
253
|
+
This self-contained example filters a `(time, elevation, azimuth, range)` array
|
|
254
|
+
using widths in cells, with periodic azimuth and reflective boundaries elsewhere:
|
|
255
|
+
|
|
256
|
+
```python
|
|
257
|
+
import numpy as np
|
|
258
|
+
import dct_toolkit as dct
|
|
259
|
+
|
|
260
|
+
data = np.random.default_rng(42).standard_normal((3, 3, 36, 24))
|
|
261
|
+
data[0, 1, :3, 8:12] = np.nan
|
|
262
|
+
|
|
263
|
+
mean_grid = dct.dct_mean(
|
|
264
|
+
data,
|
|
265
|
+
width=(3.0, 3.0, 5.0, 7.0), # time, elevation, azimuth, range cells
|
|
266
|
+
coordinates="polar",
|
|
267
|
+
axis_types=("time", "elevation", "azimuth", "range"),
|
|
268
|
+
width_units="grid",
|
|
269
|
+
boundary=("reflective", "reflective", "periodic", "reflective"),
|
|
270
|
+
restore_input_nan=False, # retain supported estimates inside gaps
|
|
271
|
+
)
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
By default, `dct_mean` restores input NaNs. Setting `restore_input_nan=False`
|
|
275
|
+
keeps estimates in gaps where support exists; wholly missing independent domains
|
|
276
|
+
still remain NaN. To keep time slices independent, add `filter_axes=(1, 2, 3)`.
|
|
277
|
+
Still supply one positive width per array axis; the unfiltered time width is then
|
|
278
|
+
an unused placeholder.
|
|
279
|
+
|
|
280
|
+
#### How does this differ from Cartesian N-D?
|
|
281
|
+
|
|
282
|
+
Both backends already handle N-dimensional arrays. The polar extension adds
|
|
283
|
+
geometry and filtering controls, not just more dimensions:
|
|
284
|
+
|
|
285
|
+
| Feature | Cartesian N-D | Explicit polar N-D |
|
|
286
|
+
|---------|---------------|--------------------|
|
|
287
|
+
| Axis roles | Not specified | Named roles in array order |
|
|
288
|
+
| Width units | Grid cells | Grid or coordinate-based physical units |
|
|
289
|
+
| Angular widths | No angular metric | Physical widths adapt with range/elevation |
|
|
290
|
+
| Boundaries | Reflective on every axis | Reflective or periodic per axis |
|
|
291
|
+
| Filtered axes | All axes | `filter_axes`; batch axes excluded |
|
|
292
|
+
|
|
293
|
+
Cartesian calls need no axis metadata: `dct.dct_mean(data, width=(3, 3, 5, 7))`.
|
|
294
|
+
Convert physical Cartesian widths to cells yourself using each axis's spacing.
|
|
295
|
+
Polar physical angular widths additionally account for range and elevation, as
|
|
296
|
+
shown below; they are distances, not degrees or radians.
|
|
297
|
+
|
|
298
|
+
With grid widths, the same widths/kernels, every axis filtered, and all boundaries
|
|
299
|
+
reflective, **finite-data smoothing matches Cartesian smoothing** up to numerical
|
|
300
|
+
rounding. This equivalence concerns the smoothing operator, not nominal count
|
|
301
|
+
conventions or missing-data fallback rules.
|
|
302
|
+
|
|
303
|
+
The same polar geometry options work with `dct_smooth`, `dct_variance`, `dct_std`,
|
|
304
|
+
`dct_count`, and `dct_prefill`. The low-level `smooth_polar` uses `width_pixels=`
|
|
305
|
+
instead of `width=` and does not take `coordinates=`. See the
|
|
306
|
+
[API reference](docs/API_REFERENCE.md#dct_toolkitpolar) for full syntax and validation.
|
|
307
|
+
|
|
308
|
+
#### Physical widths with actual coordinates
|
|
200
309
|
|
|
201
310
|
For a regular array in **(time, elevation, azimuth, range)** order:
|
|
202
311
|
|
|
@@ -6,8 +6,10 @@
|
|
|
6
6
|
`dct_toolkit` provides DCT/FFT-based smoothing and normalized-convolution statistics
|
|
7
7
|
for data with gaps. Runtime dependencies are NumPy and SciPy only.
|
|
8
8
|
|
|
9
|
-
**Version:
|
|
10
|
-
|
|
9
|
+
**Version: v2.0.0.** The default Gaussian now uses the full normalized sampled
|
|
10
|
+
kernel. To retain pre-2.0 Gaussian results, select `'gaussian_spectral'` explicitly.
|
|
11
|
+
Geometry-aware N-D polar processing requires version 1.0.0 or newer. Existing 2D
|
|
12
|
+
polar calls retain their index-based width conventions.
|
|
11
13
|
|
|
12
14
|
## Scope
|
|
13
15
|
|
|
@@ -102,7 +104,7 @@ order. Azimuth width adapts with range:
|
|
|
102
104
|
azimuth spacing in radians. Range width is in range gates. Gaussian
|
|
103
105
|
`sigma = effective_width / sqrt(12)` on the selected axis. Filtering applies
|
|
104
106
|
azimuth first, then range; both periodic and reflective azimuth support all
|
|
105
|
-
|
|
107
|
+
four kernel types, including `'boxcar_discrete'` and `'gaussian_spectral'`.
|
|
106
108
|
`az_res_deg` must be a finite, positive scalar in degrees. For gapped data,
|
|
107
109
|
periodic azimuth also wraps the nearest-neighbor prefill fallback across the
|
|
108
110
|
sweep seam; range distances remain nonperiodic.
|
|
@@ -118,10 +120,35 @@ var = dct.dct_variance(data, width=10.0)
|
|
|
118
120
|
std = dct.dct_std(data, width=10.0)
|
|
119
121
|
```
|
|
120
122
|
|
|
123
|
+
Version 2.0.0 changes the default `'gaussian'` to the **full normalized
|
|
124
|
+
sampled Gaussian**, evaluated to floating-point precision with no fixed spatial
|
|
125
|
+
cutoff. Its spatial weights are nonnegative, so normalized means stay within the
|
|
126
|
+
supported data range and population variances are nonnegative in exact arithmetic.
|
|
127
|
+
This applies to Cartesian and both polar modes, including reflective/periodic
|
|
128
|
+
boundaries and gap prefill. For mixed statistical kernels, use `'boxcar_discrete'`
|
|
129
|
+
on boxcar axes to retain this property; fractional analytical `'boxcar'` kernels
|
|
130
|
+
can have signed weights.
|
|
131
|
+
|
|
132
|
+
**Migration from v1.0.0 to v2.0.0:** Gaussian results change, especially at narrow widths.
|
|
133
|
+
Use `kernel_type="gaussian_spectral"` to retain the previous
|
|
134
|
+
`exp(-0.5 * (omega * sigma)**2)` response. Both Gaussian types use
|
|
135
|
+
`sigma = effective_grid_width / sqrt(12)`. Sampling means the new kernel's actual
|
|
136
|
+
second moment need not equal `sigma**2`; very small widths approach identity.
|
|
137
|
+
Variance still uses a numerical nonnegativity clamp; that clamp cannot repair
|
|
138
|
+
signed-weight statistical estimates.
|
|
139
|
+
|
|
140
|
+
For hybrid kernels, replace each previous `'gaussian'` entry with
|
|
141
|
+
`'gaussian_spectral'` to reproduce earlier results. For example,
|
|
142
|
+
`('gaussian', 'boxcar_discrete')` becomes
|
|
143
|
+
`('gaussian_spectral', 'boxcar_discrete')`. Set this explicitly for every
|
|
144
|
+
smoothing/statistics call that must retain the old response; prefill and
|
|
145
|
+
fallback inherit the selected kernels. Calls that omit `kernel_type` use the
|
|
146
|
+
new sampled Gaussian.
|
|
147
|
+
|
|
121
148
|
### 4) Mixed Kernels for a 3D Volume
|
|
122
149
|
|
|
123
150
|
For a volume stored in **(z, y, x)** order, apply Gaussian smoothing vertically
|
|
124
|
-
and boxcar smoothing horizontally:
|
|
151
|
+
and discrete boxcar smoothing horizontally:
|
|
125
152
|
|
|
126
153
|
```python
|
|
127
154
|
rng = np.random.default_rng(42)
|
|
@@ -130,7 +157,7 @@ volume = rng.standard_normal((16, 32, 32)) # (z, y, x)
|
|
|
130
157
|
smoothed = dct.dct_smooth(
|
|
131
158
|
volume,
|
|
132
159
|
width=(3.0, 5.0, 5.0),
|
|
133
|
-
kernel_type=("gaussian", "
|
|
160
|
+
kernel_type=("gaussian", "boxcar_discrete", "boxcar_discrete"),
|
|
134
161
|
)
|
|
135
162
|
|
|
136
163
|
# The same kernels work with normalized statistics on data containing NaNs.
|
|
@@ -138,7 +165,7 @@ volume[5:7, 12:15, 12:15] = np.nan
|
|
|
138
165
|
mean = dct.dct_mean(
|
|
139
166
|
volume,
|
|
140
167
|
width=(3.0, 5.0, 5.0),
|
|
141
|
-
kernel_type=("gaussian", "
|
|
168
|
+
kernel_type=("gaussian", "boxcar_discrete", "boxcar_discrete"),
|
|
142
169
|
)
|
|
143
170
|
```
|
|
144
171
|
|
|
@@ -150,7 +177,89 @@ kernel string applies that kernel to every axis. Equal widths with different
|
|
|
150
177
|
kernel types can still produce anisotropic smoothing. Legacy polar smoothing
|
|
151
178
|
accepts a kernel pair, using the `(azimuth, range)` convention shown above.
|
|
152
179
|
|
|
153
|
-
### 5) N-D Polar
|
|
180
|
+
### 5) N-D Polar Smoothing (v1.0.0)
|
|
181
|
+
|
|
182
|
+
#### Which inputs are required?
|
|
183
|
+
|
|
184
|
+
Use `coordinates="polar"` to select the polar backend and supply `axis_types`
|
|
185
|
+
to enable explicit N-D geometry. You do **not** need to supply all three of
|
|
186
|
+
`axis_types`, `axis_coords`, and `width_units` in every call:
|
|
187
|
+
|
|
188
|
+
| Argument | Required for explicit N-D polar processing? |
|
|
189
|
+
|----------|--------------------------------------------|
|
|
190
|
+
| `axis_types` | **Yes:** one role per array axis, in array order. |
|
|
191
|
+
| `axis_coords` | Only where physical width conversion or geometry dependencies need them. |
|
|
192
|
+
| `width_units` | **No:** defaults to `"physical"`; use `"grid"` for widths in cells. |
|
|
193
|
+
|
|
194
|
+
With `width_units="grid"`, no coordinate arrays are required. Physical filtering
|
|
195
|
+
needs coordinates on selected, non-singleton axes. Physical angular filtering
|
|
196
|
+
also needs actual positive range coordinates; physical azimuth filtering needs
|
|
197
|
+
elevation coordinates when that role exists. These geometry dependencies remain
|
|
198
|
+
required even when range/elevation are unfiltered. Coordinate entries for batch
|
|
199
|
+
or unfiltered axes may otherwise be `None`; supplied coordinates are always
|
|
200
|
+
validated.
|
|
201
|
+
|
|
202
|
+
Omitting `axis_types` does **not** infer N-D geometry: it retains the legacy
|
|
203
|
+
2D `(azimuth, range)` mode described above.
|
|
204
|
+
|
|
205
|
+
#### Start with grid widths: no coordinates needed
|
|
206
|
+
|
|
207
|
+
This self-contained example filters a `(time, elevation, azimuth, range)` array
|
|
208
|
+
using widths in cells, with periodic azimuth and reflective boundaries elsewhere:
|
|
209
|
+
|
|
210
|
+
```python
|
|
211
|
+
import numpy as np
|
|
212
|
+
import dct_toolkit as dct
|
|
213
|
+
|
|
214
|
+
data = np.random.default_rng(42).standard_normal((3, 3, 36, 24))
|
|
215
|
+
data[0, 1, :3, 8:12] = np.nan
|
|
216
|
+
|
|
217
|
+
mean_grid = dct.dct_mean(
|
|
218
|
+
data,
|
|
219
|
+
width=(3.0, 3.0, 5.0, 7.0), # time, elevation, azimuth, range cells
|
|
220
|
+
coordinates="polar",
|
|
221
|
+
axis_types=("time", "elevation", "azimuth", "range"),
|
|
222
|
+
width_units="grid",
|
|
223
|
+
boundary=("reflective", "reflective", "periodic", "reflective"),
|
|
224
|
+
restore_input_nan=False, # retain supported estimates inside gaps
|
|
225
|
+
)
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
By default, `dct_mean` restores input NaNs. Setting `restore_input_nan=False`
|
|
229
|
+
keeps estimates in gaps where support exists; wholly missing independent domains
|
|
230
|
+
still remain NaN. To keep time slices independent, add `filter_axes=(1, 2, 3)`.
|
|
231
|
+
Still supply one positive width per array axis; the unfiltered time width is then
|
|
232
|
+
an unused placeholder.
|
|
233
|
+
|
|
234
|
+
#### How does this differ from Cartesian N-D?
|
|
235
|
+
|
|
236
|
+
Both backends already handle N-dimensional arrays. The polar extension adds
|
|
237
|
+
geometry and filtering controls, not just more dimensions:
|
|
238
|
+
|
|
239
|
+
| Feature | Cartesian N-D | Explicit polar N-D |
|
|
240
|
+
|---------|---------------|--------------------|
|
|
241
|
+
| Axis roles | Not specified | Named roles in array order |
|
|
242
|
+
| Width units | Grid cells | Grid or coordinate-based physical units |
|
|
243
|
+
| Angular widths | No angular metric | Physical widths adapt with range/elevation |
|
|
244
|
+
| Boundaries | Reflective on every axis | Reflective or periodic per axis |
|
|
245
|
+
| Filtered axes | All axes | `filter_axes`; batch axes excluded |
|
|
246
|
+
|
|
247
|
+
Cartesian calls need no axis metadata: `dct.dct_mean(data, width=(3, 3, 5, 7))`.
|
|
248
|
+
Convert physical Cartesian widths to cells yourself using each axis's spacing.
|
|
249
|
+
Polar physical angular widths additionally account for range and elevation, as
|
|
250
|
+
shown below; they are distances, not degrees or radians.
|
|
251
|
+
|
|
252
|
+
With grid widths, the same widths/kernels, every axis filtered, and all boundaries
|
|
253
|
+
reflective, **finite-data smoothing matches Cartesian smoothing** up to numerical
|
|
254
|
+
rounding. This equivalence concerns the smoothing operator, not nominal count
|
|
255
|
+
conventions or missing-data fallback rules.
|
|
256
|
+
|
|
257
|
+
The same polar geometry options work with `dct_smooth`, `dct_variance`, `dct_std`,
|
|
258
|
+
`dct_count`, and `dct_prefill`. The low-level `smooth_polar` uses `width_pixels=`
|
|
259
|
+
instead of `width=` and does not take `coordinates=`. See the
|
|
260
|
+
[API reference](docs/API_REFERENCE.md#dct_toolkitpolar) for full syntax and validation.
|
|
261
|
+
|
|
262
|
+
#### Physical widths with actual coordinates
|
|
154
263
|
|
|
155
264
|
For a regular array in **(time, elevation, azimuth, range)** order:
|
|
156
265
|
|
|
@@ -23,7 +23,7 @@ from .stats import (
|
|
|
23
23
|
dct_variance,
|
|
24
24
|
)
|
|
25
25
|
|
|
26
|
-
__version__ = "
|
|
26
|
+
__version__ = "2.0.0"
|
|
27
27
|
|
|
28
28
|
|
|
29
29
|
__all__ = [
|
|
@@ -80,8 +80,10 @@ def dct_smooth(
|
|
|
80
80
|
and ``az_res_deg`` must be a finite, positive real scalar.
|
|
81
81
|
Geometry and kernels are validated even for all-NaN inputs. The same
|
|
82
82
|
operator is used for prefill and final smoothing. Default kernel is
|
|
83
|
-
'gaussian'
|
|
84
|
-
|
|
83
|
+
the full normalized sampled 'gaussian', evaluated to floating-point
|
|
84
|
+
precision without a fixed spatial cutoff. Use 'gaussian_spectral' for
|
|
85
|
+
the v1.0.0 exponential response. ``min_effective_density`` and private
|
|
86
|
+
mean controls apply to prefill only and are not passed to the low-level smoother.
|
|
85
87
|
|
|
86
88
|
Returns
|
|
87
89
|
-------
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"""Full sampled Gaussian responses for reflective and periodic filters."""
|
|
2
|
+
|
|
3
|
+
import numpy as np
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def _sampled_gaussian_transfer(n: int, width: float, periodic: bool = False) -> np.ndarray:
|
|
7
|
+
"""Evaluate the normalized infinite sampled Gaussian to float64 precision.
|
|
8
|
+
|
|
9
|
+
Parameters
|
|
10
|
+
----------
|
|
11
|
+
n : int
|
|
12
|
+
Axis length.
|
|
13
|
+
width : float
|
|
14
|
+
Effective grid width, with ``sigma = width / sqrt(12)``.
|
|
15
|
+
periodic : bool, default=False
|
|
16
|
+
Use RFFT frequencies for a periodic axis, otherwise DCT-II frequencies.
|
|
17
|
+
|
|
18
|
+
Returns
|
|
19
|
+
-------
|
|
20
|
+
ndarray
|
|
21
|
+
Response of length ``n // 2 + 1`` (periodic) or ``n`` (reflective).
|
|
22
|
+
|
|
23
|
+
Notes
|
|
24
|
+
-----
|
|
25
|
+
For sigma < 1, sum the spatial samples directly. The omitted two-sided
|
|
26
|
+
mass beyond offset 9 is below 4e-22. For sigma >= 1, Poisson summation
|
|
27
|
+
gives an equivalent sum of frequency-shifted Gaussians. The nearest
|
|
28
|
+
three aliases suffice: omitted terms are below float64 relative precision
|
|
29
|
+
throughout the represented frequency interval [0, pi]. Neither evaluation
|
|
30
|
+
imposes a finite spatial support or allocates memory proportional to width.
|
|
31
|
+
"""
|
|
32
|
+
n_freq = n // 2 + 1 if periodic else n
|
|
33
|
+
sigma = width / np.sqrt(12)
|
|
34
|
+
if sigma == 0 or n_freq == 0:
|
|
35
|
+
return np.ones(n_freq)
|
|
36
|
+
omega = (2 if periodic else 1) * np.pi * np.arange(n_freq) / n
|
|
37
|
+
with np.errstate(over="ignore", under="ignore"):
|
|
38
|
+
if sigma < 1:
|
|
39
|
+
offsets = np.arange(1, 10, dtype=float)
|
|
40
|
+
weights = np.exp(-0.5 * (offsets / sigma) ** 2)
|
|
41
|
+
response = 1 + 2 * np.sum(
|
|
42
|
+
weights[:, None] * np.cos(offsets[:, None] * omega), axis=0
|
|
43
|
+
)
|
|
44
|
+
else:
|
|
45
|
+
response = np.zeros(n_freq)
|
|
46
|
+
for alias in (-1, 0, 1):
|
|
47
|
+
response += np.exp(-0.5 * ((omega + alias * 2 * np.pi) * sigma) ** 2)
|
|
48
|
+
response /= response[0]
|
|
49
|
+
response[0] = 1.0
|
|
50
|
+
return response
|
|
@@ -26,6 +26,6 @@ def normalize_kernel_types(kernel_type: KernelLike, ndim: int) -> Tuple[str, ...
|
|
|
26
26
|
for axis, kernel in enumerate(kernels):
|
|
27
27
|
if not isinstance(kernel, str):
|
|
28
28
|
raise ValueError(f"kernel_type entries must be strings, got {kernel!r} at axis {axis}")
|
|
29
|
-
if kernel not in ("boxcar", "boxcar_discrete", "gaussian"):
|
|
29
|
+
if kernel not in ("boxcar", "boxcar_discrete", "gaussian", "gaussian_spectral"):
|
|
30
30
|
raise ValueError(f"Unknown kernel type: {kernel!r} at axis {axis}")
|
|
31
31
|
return kernels
|
|
@@ -35,9 +35,9 @@ def smooth_cartesian(
|
|
|
35
35
|
and follows array axis order. For Gaussian kernels,
|
|
36
36
|
``sigma = width / sqrt(12)`` along the corresponding axis.
|
|
37
37
|
kernel_type : str or sequence of str, default='gaussian'
|
|
38
|
-
Kernel type ('boxcar', 'boxcar_discrete', 'gaussian').
|
|
39
|
-
to every axis. A sequence must have length ``data.ndim``
|
|
40
|
-
one kernel per axis in array order, independently of ``width``.
|
|
38
|
+
Kernel type ('boxcar', 'boxcar_discrete', 'gaussian', 'gaussian_spectral').
|
|
39
|
+
A string applies to every axis. A sequence must have length ``data.ndim``
|
|
40
|
+
and selects one kernel per axis in array order, independently of ``width``.
|
|
41
41
|
|
|
42
42
|
Returns
|
|
43
43
|
-------
|
|
@@ -55,6 +55,10 @@ def smooth_cartesian(
|
|
|
55
55
|
Boundaries are reflective on every axis. Equal widths with different
|
|
56
56
|
kernel types can still produce anisotropic smoothing. Input should be
|
|
57
57
|
finite; use ``dct_mean`` or ``dct_smooth`` for data containing NaNs.
|
|
58
|
+
The default ``gaussian`` is the full normalized sampled Gaussian evaluated
|
|
59
|
+
to floating-point precision, with no fixed spatial cutoff and nonnegative
|
|
60
|
+
spatial weights. ``gaussian_spectral`` retains the v1.0.0 exponential
|
|
61
|
+
response; its spatial weights can be signed.
|
|
58
62
|
|
|
59
63
|
Examples
|
|
60
64
|
--------
|
|
@@ -9,6 +9,9 @@ This module provides the low-level building blocks for DCT-based smoothing:
|
|
|
9
9
|
import numpy as np
|
|
10
10
|
import scipy.fft
|
|
11
11
|
|
|
12
|
+
from ._gaussian import _sampled_gaussian_transfer
|
|
13
|
+
|
|
14
|
+
|
|
12
15
|
def get_dct_transfer_function(n: int, kernel_type: str, width: float) -> np.ndarray:
|
|
13
16
|
"""
|
|
14
17
|
Generate the 1D DCT Transfer Function H[k].
|
|
@@ -18,11 +21,14 @@ def get_dct_transfer_function(n: int, kernel_type: str, width: float) -> np.ndar
|
|
|
18
21
|
n : int
|
|
19
22
|
Number of points.
|
|
20
23
|
kernel_type : str
|
|
21
|
-
'boxcar', 'boxcar_discrete', or '
|
|
24
|
+
'boxcar', 'boxcar_discrete', 'gaussian', or 'gaussian_spectral'.
|
|
25
|
+
'gaussian' uses the full nonnegative normalized sampled Gaussian,
|
|
26
|
+
evaluated to floating-point precision. 'gaussian_spectral' retains
|
|
27
|
+
the original exponential spectral response, whose spatial weights can be signed.
|
|
22
28
|
width : float
|
|
23
29
|
Kernel width.
|
|
24
30
|
- boxcar: full width
|
|
25
|
-
-
|
|
31
|
+
- both Gaussian types: equivalent boxcar width (sigma = width / sqrt(12))
|
|
26
32
|
|
|
27
33
|
Returns
|
|
28
34
|
-------
|
|
@@ -72,6 +78,9 @@ def get_dct_transfer_function(n: int, kernel_type: str, width: float) -> np.ndar
|
|
|
72
78
|
return H
|
|
73
79
|
|
|
74
80
|
elif kernel_type == 'gaussian':
|
|
81
|
+
return _sampled_gaussian_transfer(n, width)
|
|
82
|
+
|
|
83
|
+
elif kernel_type == 'gaussian_spectral':
|
|
75
84
|
# Gaussian: sigma = width / sqrt(12)
|
|
76
85
|
sigma = width / np.sqrt(12)
|
|
77
86
|
if sigma < 1e-9:
|
|
@@ -84,6 +93,7 @@ def get_dct_transfer_function(n: int, kernel_type: str, width: float) -> np.ndar
|
|
|
84
93
|
else:
|
|
85
94
|
raise ValueError(f"Unknown kernel type: {kernel_type}")
|
|
86
95
|
|
|
96
|
+
|
|
87
97
|
def dct_convolve_1d(data: np.ndarray, H: np.ndarray, axis: int = -1) -> np.ndarray:
|
|
88
98
|
"""
|
|
89
99
|
Apply 1D DCT-based convolution.
|
|
@@ -104,7 +114,9 @@ def dct_convolve_1d(data: np.ndarray, H: np.ndarray, axis: int = -1) -> np.ndarr
|
|
|
104
114
|
"""
|
|
105
115
|
# Validate shapes
|
|
106
116
|
if data.shape[axis] != len(H):
|
|
107
|
-
raise ValueError(
|
|
117
|
+
raise ValueError(
|
|
118
|
+
f"Transfer function length {len(H)} does not match data dimension {data.shape[axis]}"
|
|
119
|
+
)
|
|
108
120
|
|
|
109
121
|
# Broadcast H to data shape
|
|
110
122
|
shape = [1] * data.ndim
|