astro-ndslice 0.2__tar.gz → 0.4__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.
- {astro-ndslice-0.2 → astro_ndslice-0.4}/.gitignore +8 -0
- astro_ndslice-0.4/CHANGELOG.md +55 -0
- astro_ndslice-0.4/PKG-INFO +134 -0
- astro_ndslice-0.4/README.md +72 -0
- astro_ndslice-0.4/RELEASING.md +94 -0
- astro_ndslice-0.4/astro_ndslice/__init__.py +28 -0
- astro_ndslice-0.4/astro_ndslice/lists.py +192 -0
- astro_ndslice-0.4/astro_ndslice/offset.py +560 -0
- astro_ndslice-0.4/astro_ndslice/slices.py +418 -0
- {astro-ndslice-0.2 → astro_ndslice-0.4}/astro_ndslice/tests/test_list.py +27 -5
- astro_ndslice-0.4/astro_ndslice/tests/test_offset.py +718 -0
- astro_ndslice-0.4/astro_ndslice/tests/test_slices.py +248 -0
- astro_ndslice-0.4/pyproject.toml +80 -0
- astro_ndslice-0.4/uv.lock +587 -0
- astro-ndslice-0.2/PKG-INFO +0 -69
- astro-ndslice-0.2/README.md +0 -14
- astro-ndslice-0.2/astro_ndslice/__init__.py +0 -3
- astro-ndslice-0.2/astro_ndslice/lists.py +0 -175
- astro-ndslice-0.2/astro_ndslice/offset.py +0 -414
- astro-ndslice-0.2/astro_ndslice/slices.py +0 -241
- astro-ndslice-0.2/astro_ndslice/tests/test_offset.py +0 -354
- astro-ndslice-0.2/astro_ndslice/tests/test_slices.py +0 -183
- astro-ndslice-0.2/astro_ndslice.egg-info/PKG-INFO +0 -69
- astro-ndslice-0.2/astro_ndslice.egg-info/SOURCES.txt +0 -20
- astro-ndslice-0.2/astro_ndslice.egg-info/dependency_links.txt +0 -1
- astro-ndslice-0.2/astro_ndslice.egg-info/not-zip-safe +0 -1
- astro-ndslice-0.2/astro_ndslice.egg-info/requires.txt +0 -1
- astro-ndslice-0.2/astro_ndslice.egg-info/top_level.txt +0 -1
- astro-ndslice-0.2/pyproject.toml +0 -5
- astro-ndslice-0.2/setup.cfg +0 -47
- astro-ndslice-0.2/setup.py +0 -4
- {astro-ndslice-0.2 → astro_ndslice-0.4}/LICENSE +0 -0
- {astro-ndslice-0.2 → astro_ndslice-0.4}/astro_ndslice/tests/__init__.py +0 -0
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## v0.4 (2026-11-12)
|
|
4
|
+
|
|
5
|
+
### Fixes
|
|
6
|
+
- **Image placement:** slice tuples work directly as NumPy indices. Canvas and
|
|
7
|
+
overlap sizes use the same rounded offsets, including half-pixel shifts.
|
|
8
|
+
- `offsets2slice(..., fits_convention=False)` returned lists of slices, causing
|
|
9
|
+
`array[indices[i]]` to raise `IndexError`. Now returns usable tuples.
|
|
10
|
+
- `offseted_shape(shapes=[(3,), (3,)], offsets=[(0,), (1.5,)])` returned `(4,)`.
|
|
11
|
+
Now returns `(5,)`, matching pixel placement.
|
|
12
|
+
- `offseted_shape(..., method="inner")` returned `(0,)` for touching images.
|
|
13
|
+
Now raises `ValueError` when there are no shared pixels.
|
|
14
|
+
- `offseted_shape(shapes=[(2.5,)], offsets=[(0,)])` silently rounded to `(2,)`.
|
|
15
|
+
Now raises `ValueError`.
|
|
16
|
+
- **FITS conversion:** reverse slices retain their selected pixels;
|
|
17
|
+
unrepresentable slices raise `ValueError`.
|
|
18
|
+
- `slice(None, None, -1)` became `[:1]`, selecting only the first pixel.
|
|
19
|
+
Now becomes `[:1:-1]`, preserving the full reversal.
|
|
20
|
+
- `slice(2, 2)` became `[3:2]`, selecting two pixels instead of none.
|
|
21
|
+
Now raises `ValueError` when converting to FITS.
|
|
22
|
+
- **WCS and headers:** coordinate arrays work. The default `ignore_ltm=False` applies
|
|
23
|
+
LTM scaling with a matrix solve; singular or invalid transforms raise
|
|
24
|
+
`ValueError`, including under `python -O`.
|
|
25
|
+
- `loc_target=np.array([2, 3])` raised an ambiguous-truth `ValueError`.
|
|
26
|
+
Now accepts coordinate arrays.
|
|
27
|
+
- `loc_target="center"` with unknown `WCS.pixel_shape` used zero dimensions.
|
|
28
|
+
Now raises `ValueError`.
|
|
29
|
+
- `LTV1=6, LTM1_1=2` returned `[6]` by default, ignoring scaling. Now returns
|
|
30
|
+
`[3.]`; `ignore_ltm=True` retains the raw value. Nonidentity LTM matrices
|
|
31
|
+
rejected by the earlier validation-only fix are now supported.
|
|
32
|
+
- **Input helpers:** `listify()` broadcasts multiple inputs consistently;
|
|
33
|
+
`slicefy()` accepts generators; `bezel2slice(None)` returns full slices.
|
|
34
|
+
- **Development:** explicit exports preserve public names. Development
|
|
35
|
+
dependencies include Astropy and isort.
|
|
36
|
+
- **Releases:** Hatchling builds explicit package contents. CI tests installed
|
|
37
|
+
distributions. A matching `v*` tag push publishes the tested files to PyPI
|
|
38
|
+
after version, tag, changelog, lint, and test checks pass, then creates the
|
|
39
|
+
GitHub release from this changelog.
|
|
40
|
+
|
|
41
|
+
### Migration checklist
|
|
42
|
+
|
|
43
|
+
1. **LTM:** add `ignore_ltm=True` to retain raw LTV differences. The new default
|
|
44
|
+
applies LTM correction and rejects invalid transforms.
|
|
45
|
+
2. **Image indices:** replace whole tuples from `offsets2slice()`; their elements
|
|
46
|
+
are no longer mutable.
|
|
47
|
+
3. **Geometry:** use matching shapes/offsets, nonnegative integer sizes, and finite
|
|
48
|
+
offsets. Empty overlap raises `ValueError`; half-pixel canvas sizes may change.
|
|
49
|
+
4. **FITS slices:** keep negative bounds and empty slices in Python form with
|
|
50
|
+
`fits_convention=False`.
|
|
51
|
+
5. **WCS centers:** set `WCS.pixel_shape`, pass coordinates, or use `"origin"`.
|
|
52
|
+
|
|
53
|
+
Also: `listify(..., scalar2list=False)` affects only single-input calls;
|
|
54
|
+
`slicefy()` rejects empty iterable rules.
|
|
55
|
+
Details: [offsets](astro_ndslice/offset.py), [slices](astro_ndslice/slices.py).
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: astro-ndslice
|
|
3
|
+
Version: 0.4
|
|
4
|
+
Summary: Convenience tools for slicing images & calculating offsets before combinations (focusing on converting astronomical FITS/IRAF and python conventions)
|
|
5
|
+
Project-URL: Source, https://github.com/ysBach/astro-ndslice
|
|
6
|
+
Project-URL: Tracker, https://github.com/ysBach/astro-ndslice/issues
|
|
7
|
+
Author-email: "Yoonsoo P. Bach" <ysbach20766@gmail.com>
|
|
8
|
+
License: BSD 3-Clause License
|
|
9
|
+
|
|
10
|
+
Copyright (c) 2023, Yoonsoo P. Bach
|
|
11
|
+
|
|
12
|
+
Redistribution and use in source and binary forms, with or without
|
|
13
|
+
modification, are permitted provided that the following conditions are met:
|
|
14
|
+
|
|
15
|
+
1. Redistributions of source code must retain the above copyright notice, this
|
|
16
|
+
list of conditions and the following disclaimer.
|
|
17
|
+
|
|
18
|
+
2. Redistributions in binary form must reproduce the above copyright notice,
|
|
19
|
+
this list of conditions and the following disclaimer in the documentation
|
|
20
|
+
and/or other materials provided with the distribution.
|
|
21
|
+
|
|
22
|
+
3. Neither the name of the copyright holder nor the names of its
|
|
23
|
+
contributors may be used to endorse or promote products derived from
|
|
24
|
+
this software without specific prior written permission.
|
|
25
|
+
|
|
26
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
|
27
|
+
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
28
|
+
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
29
|
+
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
|
30
|
+
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
|
31
|
+
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
|
32
|
+
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
|
33
|
+
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
|
34
|
+
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
35
|
+
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
36
|
+
License-File: LICENSE
|
|
37
|
+
Keywords: astronomy,data reduction,image analysis
|
|
38
|
+
Classifier: Development Status :: 4 - Beta
|
|
39
|
+
Classifier: Intended Audience :: Education
|
|
40
|
+
Classifier: Intended Audience :: Science/Research
|
|
41
|
+
Classifier: License :: OSI Approved :: BSD License
|
|
42
|
+
Classifier: Natural Language :: English
|
|
43
|
+
Classifier: Operating System :: OS Independent
|
|
44
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
45
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
46
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
47
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
48
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
49
|
+
Classifier: Topic :: Education
|
|
50
|
+
Classifier: Topic :: Scientific/Engineering :: Astronomy
|
|
51
|
+
Classifier: Topic :: Scientific/Engineering :: Image Processing
|
|
52
|
+
Requires-Python: >=3.10
|
|
53
|
+
Requires-Dist: numpy>=1.23
|
|
54
|
+
Provides-Extra: dev
|
|
55
|
+
Requires-Dist: astropy>=5.3.3; extra == 'dev'
|
|
56
|
+
Requires-Dist: isort; extra == 'dev'
|
|
57
|
+
Requires-Dist: pytest; extra == 'dev'
|
|
58
|
+
Requires-Dist: ruff; extra == 'dev'
|
|
59
|
+
Provides-Extra: full
|
|
60
|
+
Requires-Dist: astropy>=5.3.3; extra == 'full'
|
|
61
|
+
Description-Content-Type: text/markdown
|
|
62
|
+
|
|
63
|
+
# astro-ndslice
|
|
64
|
+
|
|
65
|
+
[](https://zenodo.org/badge/latestdoi/601666411)
|
|
66
|
+
|
|
67
|
+
Slice NumPy arrays, convert FITS/IRAF sections, and align images by pixel offsets.
|
|
68
|
+
|
|
69
|
+
## Install
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
pip install astro-ndslice
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
Python >= 3.10.
|
|
76
|
+
|
|
77
|
+
For astronomers who need WCS/header offset helpers:
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
pip install "astro-ndslice[full]"
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
## Slice an image
|
|
84
|
+
|
|
85
|
+
```python
|
|
86
|
+
import numpy as np
|
|
87
|
+
from astro_ndslice import slicefy
|
|
88
|
+
|
|
89
|
+
data = np.arange(100).reshape(10, 10)
|
|
90
|
+
cutout = data[slicefy("[2:5,3:7]")] # Same as data[2:7, 1:5].
|
|
91
|
+
trimmed = data[slicefy(1)] # Trim one pixel from each edge - bezeling.
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
* **FITS sections** use **1-based, inclusive bounds in x, y order**.
|
|
95
|
+
* **NumPy** uses **0-based, exclusive stops in row, column order**.
|
|
96
|
+
* `slicefy()` assumes FITS strings
|
|
97
|
+
* `slice_from_string()` assumes Python strings unless you pass `fits_convention=True`.
|
|
98
|
+
|
|
99
|
+
## Align images
|
|
100
|
+
|
|
101
|
+
```python
|
|
102
|
+
import numpy as np
|
|
103
|
+
from astro_ndslice import offseted_shape, offsets2slice
|
|
104
|
+
|
|
105
|
+
images = [np.ones((3, 4)), np.full((2, 3), 2)]
|
|
106
|
+
shapes = [image.shape for image in images] # NumPy axis order.
|
|
107
|
+
offsets = [(0, 0), (2, 1)] # x, y offsets.
|
|
108
|
+
_, shape = offseted_shape(shapes, offsets)
|
|
109
|
+
indices = offsets2slice(shapes, offsets)
|
|
110
|
+
stack = np.full((len(images), *shape), np.nan)
|
|
111
|
+
for image, index in zip(images, indices):
|
|
112
|
+
stack[index] = image
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
Both helpers round relative offsets to pixel positions. Use `method="inner"`
|
|
116
|
+
to find the region shared by every image; no shared pixels raises `ValueError`.
|
|
117
|
+
|
|
118
|
+
## Function guide
|
|
119
|
+
|
|
120
|
+
| Task | Functions |
|
|
121
|
+
| --- | --- |
|
|
122
|
+
| Normalize scalars and iterables | `is_list_like`, `listify`, `ndfy` |
|
|
123
|
+
| Parse sections and trim edges | `slicefy`, `slice_from_string`, `slice_to_string`, `bezel2slice` |
|
|
124
|
+
| Place images and find overlap | `regularize_offsets`, `offseted_shape`, `offsets2slice` |
|
|
125
|
+
| Read WCS or physical offsets | `calc_offset_wcs`, `calc_offset_physical` (requires Astropy) |
|
|
126
|
+
|
|
127
|
+
`calc_offset_physical()` corrects LTV translation terms
|
|
128
|
+
for LTM scaling in shared physical-coordinate units. For example,
|
|
129
|
+
`LTV1=6, LTM1_1=2` gives `3` along x; `ignore_ltm=True` gives the raw value `6`.
|
|
130
|
+
This preserves the LTV sign; differing LTM matrices require resampling for
|
|
131
|
+
image alignment.
|
|
132
|
+
|
|
133
|
+
See [CHANGELOG.md](CHANGELOG.md) for return-type changes and stricter validation.
|
|
134
|
+
For version bumps and PyPI publishing, see [RELEASING.md](RELEASING.md).
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# astro-ndslice
|
|
2
|
+
|
|
3
|
+
[](https://zenodo.org/badge/latestdoi/601666411)
|
|
4
|
+
|
|
5
|
+
Slice NumPy arrays, convert FITS/IRAF sections, and align images by pixel offsets.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
pip install astro-ndslice
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Python >= 3.10.
|
|
14
|
+
|
|
15
|
+
For astronomers who need WCS/header offset helpers:
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
pip install "astro-ndslice[full]"
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Slice an image
|
|
22
|
+
|
|
23
|
+
```python
|
|
24
|
+
import numpy as np
|
|
25
|
+
from astro_ndslice import slicefy
|
|
26
|
+
|
|
27
|
+
data = np.arange(100).reshape(10, 10)
|
|
28
|
+
cutout = data[slicefy("[2:5,3:7]")] # Same as data[2:7, 1:5].
|
|
29
|
+
trimmed = data[slicefy(1)] # Trim one pixel from each edge - bezeling.
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
* **FITS sections** use **1-based, inclusive bounds in x, y order**.
|
|
33
|
+
* **NumPy** uses **0-based, exclusive stops in row, column order**.
|
|
34
|
+
* `slicefy()` assumes FITS strings
|
|
35
|
+
* `slice_from_string()` assumes Python strings unless you pass `fits_convention=True`.
|
|
36
|
+
|
|
37
|
+
## Align images
|
|
38
|
+
|
|
39
|
+
```python
|
|
40
|
+
import numpy as np
|
|
41
|
+
from astro_ndslice import offseted_shape, offsets2slice
|
|
42
|
+
|
|
43
|
+
images = [np.ones((3, 4)), np.full((2, 3), 2)]
|
|
44
|
+
shapes = [image.shape for image in images] # NumPy axis order.
|
|
45
|
+
offsets = [(0, 0), (2, 1)] # x, y offsets.
|
|
46
|
+
_, shape = offseted_shape(shapes, offsets)
|
|
47
|
+
indices = offsets2slice(shapes, offsets)
|
|
48
|
+
stack = np.full((len(images), *shape), np.nan)
|
|
49
|
+
for image, index in zip(images, indices):
|
|
50
|
+
stack[index] = image
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Both helpers round relative offsets to pixel positions. Use `method="inner"`
|
|
54
|
+
to find the region shared by every image; no shared pixels raises `ValueError`.
|
|
55
|
+
|
|
56
|
+
## Function guide
|
|
57
|
+
|
|
58
|
+
| Task | Functions |
|
|
59
|
+
| --- | --- |
|
|
60
|
+
| Normalize scalars and iterables | `is_list_like`, `listify`, `ndfy` |
|
|
61
|
+
| Parse sections and trim edges | `slicefy`, `slice_from_string`, `slice_to_string`, `bezel2slice` |
|
|
62
|
+
| Place images and find overlap | `regularize_offsets`, `offseted_shape`, `offsets2slice` |
|
|
63
|
+
| Read WCS or physical offsets | `calc_offset_wcs`, `calc_offset_physical` (requires Astropy) |
|
|
64
|
+
|
|
65
|
+
`calc_offset_physical()` corrects LTV translation terms
|
|
66
|
+
for LTM scaling in shared physical-coordinate units. For example,
|
|
67
|
+
`LTV1=6, LTM1_1=2` gives `3` along x; `ignore_ltm=True` gives the raw value `6`.
|
|
68
|
+
This preserves the LTV sign; differing LTM matrices require resampling for
|
|
69
|
+
image alignment.
|
|
70
|
+
|
|
71
|
+
See [CHANGELOG.md](CHANGELOG.md) for return-type changes and stricter validation.
|
|
72
|
+
For version bumps and PyPI publishing, see [RELEASING.md](RELEASING.md).
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
# Release astro-ndslice
|
|
2
|
+
|
|
3
|
+
**Push a matching `vX.Y.Z` tag to validate, publish to PyPI, and create the
|
|
4
|
+
GitHub release.** Manual workflow runs validate only and never upload.
|
|
5
|
+
|
|
6
|
+
| Event | Checks | Publication |
|
|
7
|
+
| --- | --- | --- |
|
|
8
|
+
| Push to `main` or pull request | CI and installed distributions | None |
|
|
9
|
+
| Manual **Release → Run workflow** | Same checks, plus dated release notes | None |
|
|
10
|
+
| Matching `v{version}` tag push | Same checks, plus tag match | PyPI, then GitHub release |
|
|
11
|
+
|
|
12
|
+
Do not create the GitHub release by hand. The workflow creates it after the
|
|
13
|
+
PyPI upload succeeds, with the matching `CHANGELOG.md` section as its notes.
|
|
14
|
+
|
|
15
|
+
## One-time setup
|
|
16
|
+
|
|
17
|
+
1. In GitHub **Settings → Environments**, create an environment named `pypi`.
|
|
18
|
+
Restrict its deployment to tags that match `v*`.
|
|
19
|
+
2. In [PyPI Publishing settings](https://pypi.org/manage/project/astro-ndslice/settings/publishing/),
|
|
20
|
+
add (or edit) the trusted publisher:
|
|
21
|
+
|
|
22
|
+
| Setting | Value |
|
|
23
|
+
| --- | --- |
|
|
24
|
+
| Owner / repository | `ysBach` / `astro-ndslice` |
|
|
25
|
+
| Workflow | `release.yml` |
|
|
26
|
+
| Environment | `pypi` |
|
|
27
|
+
|
|
28
|
+
Only the upload job receives `id-token: write`; no API token is needed.
|
|
29
|
+
See [PyPI's setup guide](https://docs.pypi.org/trusted-publishers/adding-a-publisher/).
|
|
30
|
+
|
|
31
|
+
## Each release
|
|
32
|
+
|
|
33
|
+
Use one Bash/zsh session in the repository root, with `uv` and authenticated `gh`.
|
|
34
|
+
Commit or merge the intended changes onto `main` first.
|
|
35
|
+
|
|
36
|
+
1. **Set the version.** Replace `1.2.3` with the intended release version.
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
release_version=1.2.3
|
|
40
|
+
git switch main &&
|
|
41
|
+
uv version "$release_version" --no-sync
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
`pyproject.toml` is the version source; this also updates `uv.lock` without
|
|
45
|
+
syncing any environment. No separate Python version constant is maintained.
|
|
46
|
+
|
|
47
|
+
2. **Date the release notes.** Rename `## Unreleased` in `CHANGELOG.md` to
|
|
48
|
+
`## v1.2.3 (YYYY-MM-DD)`, using the version above and actual release date.
|
|
49
|
+
Keep this release's bullets below it. Validate with the project `.venv`:
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
uv sync --locked --python 3.13 &&
|
|
53
|
+
uv run --no-sync python .github/scripts/check_release.py \
|
|
54
|
+
--release --tag "v$release_version"
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
3. **Commit and push.**
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
git add pyproject.toml uv.lock CHANGELOG.md &&
|
|
61
|
+
git commit -m "chore: prepare release $release_version" &&
|
|
62
|
+
git push origin main
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
4. **Wait for CI** on that commit in
|
|
66
|
+
[GitHub Actions](https://github.com/ysBach/astro-ndslice/actions).
|
|
67
|
+
Optional rehearsal: `gh workflow run release.yml --ref main`. It runs the
|
|
68
|
+
release checks but never uploads.
|
|
69
|
+
|
|
70
|
+
5. **Tag that commit and push the tag.** The tag must be exactly
|
|
71
|
+
`v{version}`, for example `v1.2.3`. The older `v.0.2` style fails validation.
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
git tag -a "v$release_version" -m "Release $release_version" &&
|
|
75
|
+
git push origin "v$release_version"
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
Check the **Release** run in Actions, the version on
|
|
79
|
+
[PyPI](https://pypi.org/project/astro-ndslice/), and the new GitHub release.
|
|
80
|
+
|
|
81
|
+
## Validation
|
|
82
|
+
|
|
83
|
+
- Package and lockfile versions must match. Releases also require a matching
|
|
84
|
+
`v{version}` tag and a dated, nonempty changelog entry.
|
|
85
|
+
- Hatchling builds one universal wheel and one sdist with `uv build --no-sources`.
|
|
86
|
+
- CI tests installed wheels on Python 3.10–3.14 and the sdist on Python 3.13,
|
|
87
|
+
outside the checkout. Astropy tests, docstrings, formatting, and lint must pass.
|
|
88
|
+
- Publishing uploads those tested artifacts, without rebuilding. The sdist
|
|
89
|
+
includes tests and public docs; local files and workflows are excluded.
|
|
90
|
+
|
|
91
|
+
If nothing uploaded, fix the cause and rerun failed jobs. `uv publish
|
|
92
|
+
--check-url` skips files that PyPI already has, so a rerun after a partial
|
|
93
|
+
upload sends only the missing files. Published files cannot be replaced;
|
|
94
|
+
source fixes need a new version and tag. Do not move a published tag.
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
from . import lists, offset, slices
|
|
2
|
+
from .lists import is_list_like, listify, ndfy
|
|
3
|
+
from .offset import (
|
|
4
|
+
calc_offset_physical,
|
|
5
|
+
calc_offset_wcs,
|
|
6
|
+
offseted_shape,
|
|
7
|
+
offsets2slice,
|
|
8
|
+
regularize_offsets,
|
|
9
|
+
)
|
|
10
|
+
from .slices import bezel2slice, slice_from_string, slice_to_string, slicefy
|
|
11
|
+
|
|
12
|
+
__all__ = [
|
|
13
|
+
"lists",
|
|
14
|
+
"offset",
|
|
15
|
+
"slices",
|
|
16
|
+
"is_list_like",
|
|
17
|
+
"listify",
|
|
18
|
+
"ndfy",
|
|
19
|
+
"regularize_offsets",
|
|
20
|
+
"offseted_shape",
|
|
21
|
+
"offsets2slice",
|
|
22
|
+
"calc_offset_wcs",
|
|
23
|
+
"calc_offset_physical",
|
|
24
|
+
"slice_from_string",
|
|
25
|
+
"slice_to_string",
|
|
26
|
+
"slicefy",
|
|
27
|
+
"bezel2slice",
|
|
28
|
+
]
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
"""Normalize scalars and iterables to lists."""
|
|
2
|
+
|
|
3
|
+
from collections import abc
|
|
4
|
+
from collections.abc import Callable
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
import numpy as np
|
|
8
|
+
|
|
9
|
+
__all__ = [
|
|
10
|
+
"is_list_like",
|
|
11
|
+
"listify",
|
|
12
|
+
"ndfy",
|
|
13
|
+
]
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def is_list_like(*objs, allow_sets: bool = True, func: Callable = all) -> bool:
|
|
17
|
+
"""Check whether inputs are iterable, excluding strings and scalar arrays.
|
|
18
|
+
|
|
19
|
+
Parameters
|
|
20
|
+
----------
|
|
21
|
+
*objs : object
|
|
22
|
+
Objects to check.
|
|
23
|
+
allow_sets : bool, optional
|
|
24
|
+
Count sets as list-like. Default: `True`.
|
|
25
|
+
func : callable, optional
|
|
26
|
+
Combine results with `all` (default), `any`, or another callable.
|
|
27
|
+
|
|
28
|
+
Returns
|
|
29
|
+
-------
|
|
30
|
+
bool or object
|
|
31
|
+
Result from `func`; `all` and `any` return a boolean. Strings, bytes,
|
|
32
|
+
and zero-dimensional arrays are not list-like. Other iterables,
|
|
33
|
+
including dictionaries, are.
|
|
34
|
+
|
|
35
|
+
Notes
|
|
36
|
+
-----
|
|
37
|
+
Adapted from pandas to accept multiple inputs and a combining function.
|
|
38
|
+
https://github.com/pandas-dev/pandas/blob/bdb00f2d5a12f813e93bc55cdcd56dcb1aae776e/pandas/_libs/lib.pyx#L1026
|
|
39
|
+
|
|
40
|
+
Timing on MBP 14" [2024, macOS 26.6, M4Pro(8P+4E/G20c/N16c/48G)]
|
|
41
|
+
(2026-09-07; CPython 3.13.11, NumPy 2.5.2)::
|
|
42
|
+
|
|
43
|
+
is_list_like("asdfaer.fits") 0.188 +/- 0.001 us
|
|
44
|
+
|
|
45
|
+
Mean +/- std. dev. per call (`timeit`, 7 runs, 2,000,000 loops each).
|
|
46
|
+
"""
|
|
47
|
+
|
|
48
|
+
def _is_list_like(obj):
|
|
49
|
+
return (
|
|
50
|
+
isinstance(obj, abc.Iterable)
|
|
51
|
+
# we do not count strings/unicode/bytes as list-like
|
|
52
|
+
and not isinstance(obj, (str, bytes))
|
|
53
|
+
# exclude zero-dimensional numpy arrays, effectively scalars
|
|
54
|
+
and not (isinstance(obj, np.ndarray) and obj.ndim == 0)
|
|
55
|
+
# exclude sets if allow_sets is False
|
|
56
|
+
and not (allow_sets is False and isinstance(obj, abc.Set))
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
if func is all:
|
|
60
|
+
for obj in objs:
|
|
61
|
+
if not _is_list_like(obj):
|
|
62
|
+
return False
|
|
63
|
+
return True
|
|
64
|
+
if func is any:
|
|
65
|
+
for obj in objs:
|
|
66
|
+
if _is_list_like(obj):
|
|
67
|
+
return True
|
|
68
|
+
return False
|
|
69
|
+
|
|
70
|
+
return func(_is_list_like(obj) for obj in objs)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def listify(*objs: Any, scalar2list: bool = True, none2list: bool = False) -> Any:
|
|
74
|
+
"""Convert inputs to lists of the same length.
|
|
75
|
+
|
|
76
|
+
Parameters
|
|
77
|
+
----------
|
|
78
|
+
*objs : object
|
|
79
|
+
One or more inputs. Iterables become lists; strings count as scalars.
|
|
80
|
+
With multiple inputs, length-one lists and scalars are repeated to
|
|
81
|
+
match the longest input.
|
|
82
|
+
scalar2list : bool, optional
|
|
83
|
+
Wrap a single scalar in a list. Ignored for multiple inputs and
|
|
84
|
+
iterable inputs. Default: `True`.
|
|
85
|
+
none2list : bool, optional
|
|
86
|
+
Return ``[None]`` for a single `None`, instead of ``[]``.
|
|
87
|
+
With multiple inputs, `None` always becomes a length-one list before
|
|
88
|
+
broadcasting. Default: `False`.
|
|
89
|
+
|
|
90
|
+
Returns
|
|
91
|
+
-------
|
|
92
|
+
list or object
|
|
93
|
+
Normalized lists, or the original single scalar when `scalar2list`
|
|
94
|
+
is `False`.
|
|
95
|
+
|
|
96
|
+
Raises
|
|
97
|
+
------
|
|
98
|
+
ValueError
|
|
99
|
+
If no inputs are given or input lengths cannot be broadcast.
|
|
100
|
+
|
|
101
|
+
Notes
|
|
102
|
+
-----
|
|
103
|
+
Timing on MBP 14" [2024, macOS 26.6, M4Pro(8P+4E/G20c/N16c/48G)]
|
|
104
|
+
(2026-09-07; CPython 3.13.11, NumPy 2.5.2)::
|
|
105
|
+
|
|
106
|
+
listify([12]) 0.331 +/- 0.003 us
|
|
107
|
+
listify("asdf") 0.249 +/- 0.001 us
|
|
108
|
+
listify("asdf", scalar2list=False) 0.238 +/- 0.001 us
|
|
109
|
+
|
|
110
|
+
Mean +/- std. dev. per call (`timeit`, 7 runs, 1,000,000 loops each).
|
|
111
|
+
|
|
112
|
+
Examples
|
|
113
|
+
--------
|
|
114
|
+
>>> listify([1, 2], "x", None)
|
|
115
|
+
[[1, 2], ['x', 'x'], [None, None]]
|
|
116
|
+
>>> listify(3, scalar2list=False)
|
|
117
|
+
3
|
|
118
|
+
"""
|
|
119
|
+
|
|
120
|
+
if len(objs) == 1:
|
|
121
|
+
obj = objs[0]
|
|
122
|
+
if obj is None:
|
|
123
|
+
return [None] if none2list else []
|
|
124
|
+
if is_list_like(obj):
|
|
125
|
+
return list(obj)
|
|
126
|
+
return [obj] if scalar2list else obj
|
|
127
|
+
|
|
128
|
+
objlists = [list(obj) if is_list_like(obj) else [obj] for obj in objs]
|
|
129
|
+
lengths = [len(obj) for obj in objlists]
|
|
130
|
+
length = max(lengths)
|
|
131
|
+
for objl in objlists:
|
|
132
|
+
if len(objl) not in [1, length]:
|
|
133
|
+
raise ValueError(f"Each input must be 1 or max(lengths)={length}.")
|
|
134
|
+
|
|
135
|
+
return [obj * length if len(obj) == 1 else obj for obj in objlists]
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def ndfy(item, length: int | None = None, default: Any = None) -> list:
|
|
139
|
+
"""Normalize a list, replace `None`, and repeat a single item as needed.
|
|
140
|
+
|
|
141
|
+
Parameters
|
|
142
|
+
----------
|
|
143
|
+
item : object
|
|
144
|
+
Scalar or iterable to normalize.
|
|
145
|
+
length : int, optional
|
|
146
|
+
Target length; a single item repeats, other lengths must match.
|
|
147
|
+
`None` keeps the input length, or one for a scalar.
|
|
148
|
+
default : object, optional
|
|
149
|
+
Replacement for a `None` input or top-level element. Default: `None`.
|
|
150
|
+
|
|
151
|
+
Returns
|
|
152
|
+
-------
|
|
153
|
+
list
|
|
154
|
+
Normalized values. Nested inputs are preserved, not transposed.
|
|
155
|
+
|
|
156
|
+
Raises
|
|
157
|
+
------
|
|
158
|
+
ValueError
|
|
159
|
+
If the input length is neither one nor the requested length.
|
|
160
|
+
|
|
161
|
+
Notes
|
|
162
|
+
-----
|
|
163
|
+
Repetition reuses references; nested mutable values are not copied.
|
|
164
|
+
|
|
165
|
+
Timing on MBP 14" [2024, macOS 26.6, M4Pro(8P+4E/G20c/N16c/48G)]
|
|
166
|
+
(2026-09-07; CPython 3.13.11, NumPy 2.4.6)::
|
|
167
|
+
|
|
168
|
+
ndfy(1, length=2) 0.356 +/- 0.004 us
|
|
169
|
+
ndfy([1, None], default=0) 0.426 +/- 0.010 us
|
|
170
|
+
|
|
171
|
+
Mean +/- std. dev. per call (`timeit`, 7 runs;
|
|
172
|
+
1,000,000/500,000 loops in row order).
|
|
173
|
+
|
|
174
|
+
Examples
|
|
175
|
+
--------
|
|
176
|
+
>>> ndfy(None, length=2, default=0)
|
|
177
|
+
[0, 0]
|
|
178
|
+
>>> ndfy([[1, 2]], length=2)
|
|
179
|
+
[[1, 2], [1, 2]]
|
|
180
|
+
"""
|
|
181
|
+
item = [default if i is None else i for i in listify(item, none2list=True)]
|
|
182
|
+
item_length = len(item)
|
|
183
|
+
|
|
184
|
+
if (length is None) or (item_length == length):
|
|
185
|
+
return item
|
|
186
|
+
elif item_length != 1:
|
|
187
|
+
raise ValueError(
|
|
188
|
+
f"`len(item)` must be 1 or `length`(={length}). Now it is {item_length}."
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
# Now, item_length == 1
|
|
192
|
+
return item * length
|