csar 0.1.1__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.
@@ -0,0 +1,41 @@
1
+ name: test
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+ branches: [main]
8
+
9
+ concurrency:
10
+ group: test-${{ github.ref }}
11
+ cancel-in-progress: true
12
+
13
+ jobs:
14
+ test:
15
+ name: py${{ matrix.python-version }} on ${{ matrix.os }}
16
+ runs-on: ${{ matrix.os }}
17
+ # Non-editable so `uv sync` builds + installs csar as the wheel ships it
18
+ # (no meson-python rebuild-on-import hook, which needs build isolation off).
19
+ env:
20
+ UV_NO_EDITABLE: "1"
21
+ strategy:
22
+ fail-fast: false
23
+ matrix:
24
+ os: [ubuntu-latest, macos-latest, windows-latest]
25
+ python-version: ["3.11", "3.12", "3.13", "3.14"]
26
+
27
+ steps:
28
+ - uses: actions/checkout@v6
29
+
30
+ - uses: astral-sh/setup-uv@v8.1.0
31
+ with:
32
+ python-version: ${{ matrix.python-version }}
33
+ cache-suffix: test
34
+
35
+ - uses: extractions/setup-just@v4
36
+
37
+ - name: Install
38
+ run: uv sync
39
+
40
+ - name: Test + coverage
41
+ run: just ci-test
@@ -0,0 +1,172 @@
1
+ name: wheels
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+ branches: [main]
8
+ release:
9
+ types: [published]
10
+
11
+ concurrency:
12
+ group: wheels-${{ github.ref }}
13
+ # Don't cancel a release run mid-publish, but for branch pushes and
14
+ # PRs the latest commit should win — old runs just block the queue.
15
+ cancel-in-progress: ${{ github.event_name != 'release' }}
16
+
17
+ jobs:
18
+ sdist:
19
+ name: build sdist
20
+ runs-on: ubuntu-latest
21
+ steps:
22
+ - uses: actions/checkout@v6
23
+
24
+ - uses: astral-sh/setup-uv@v8.1.0
25
+ with:
26
+ python-version: "3.13"
27
+ cache-suffix: wheels-sdist
28
+
29
+ - name: Build sdist
30
+ run: uv build --sdist
31
+
32
+ - name: Test sdist round-trip
33
+ run: |
34
+ uv venv test-env
35
+ uv pip install --python test-env/bin/python --no-cache dist/*.tar.gz pytest numpy
36
+ test-env/bin/pytest -o addopts=
37
+
38
+ - uses: actions/upload-artifact@v6
39
+ with:
40
+ name: sdist
41
+ path: dist/*.tar.gz
42
+ compression-level: 0
43
+
44
+ # Ask cibuildwheel which wheels it would build for each (platform,
45
+ # arch) given our [tool.cibuildwheel] config, then emit a JSON
46
+ # matrix that the wheels job consumes via `fromJson()`. Saves
47
+ # hand-maintaining a 30-row include list as Python versions evolve.
48
+ discover:
49
+ name: discover build identifiers
50
+ runs-on: ubuntu-latest
51
+ outputs:
52
+ matrix: ${{ steps.set.outputs.matrix }}
53
+ steps:
54
+ - uses: actions/checkout@v6
55
+ - uses: actions/setup-python@v6
56
+ with:
57
+ python-version: "3.13"
58
+ # Keep this version in sync with `pypa/cibuildwheel@vX.Y.Z`
59
+ # below — GHA's `uses:` doesn't accept expression interpolation,
60
+ # so the literal has to live in two places.
61
+ - run: pip install cibuildwheel==3.4.1
62
+ - id: set
63
+ shell: bash
64
+ run: |
65
+ python <<'PY' >> $GITHUB_OUTPUT
66
+ import json
67
+ import subprocess
68
+
69
+ # (runner image, cibuildwheel platform name, arch).
70
+ targets = [
71
+ ("ubuntu-latest", "linux", "x86_64"),
72
+ ("ubuntu-24.04-arm", "linux", "aarch64"),
73
+ ("macos-latest", "macos", "arm64"),
74
+ ("windows-latest", "windows", "AMD64"),
75
+ ]
76
+
77
+ include = []
78
+ for runner, platform, arch in targets:
79
+ ids = subprocess.run(
80
+ ["cibuildwheel",
81
+ f"--platform={platform}",
82
+ f"--archs={arch}",
83
+ "--print-build-identifiers"],
84
+ capture_output=True, text=True, check=True,
85
+ ).stdout.split()
86
+ for ident in ids:
87
+ include.append({"os": runner, "only": ident})
88
+
89
+ print(f"matrix={json.dumps({'include': include})}")
90
+ PY
91
+
92
+ wheels:
93
+ # One job per build identifier (matrix expanded from `discover`)
94
+ # so wall-clock approaches max(per-wheel build time) instead of
95
+ # sum. Free for public repos on GitHub Actions runner-minutes.
96
+ name: ${{ matrix.only }}
97
+ needs: discover
98
+ runs-on: ${{ matrix.os }}
99
+ strategy:
100
+ fail-fast: false
101
+ matrix: ${{ fromJson(needs.discover.outputs.matrix) }}
102
+
103
+ steps:
104
+ - uses: actions/checkout@v6
105
+
106
+ # cibuildwheel uses uv as the build frontend; on Linux it
107
+ # bootstraps uv inside the manylinux container, but on macOS /
108
+ # Windows it expects uv on the host PATH. Skip the host install
109
+ # on Linux jobs — saves ~5s × N Linux runners of runner-minutes.
110
+ - if: runner.os != 'Linux'
111
+ uses: astral-sh/setup-uv@v8.1.0
112
+
113
+ # Pin uv's cache to C: on Windows so hardlinks work.
114
+ #
115
+ # setup-uv defaults UV_CACHE_DIR to ${RUNNER_TEMP}/setup-uv-cache,
116
+ # which on GitHub-hosted Windows runners is D:\a\_temp\... .
117
+ # cibuildwheel's `build[uv]` frontend creates Python's isolated
118
+ # build env via tempfile.gettempdir(), which on Windows resolves
119
+ # to C:\Users\runneradmin\AppData\Local\Temp\... . NTFS doesn't
120
+ # support hardlinks across drives, so uv falls back to full file
121
+ # copy ("Failed to hard link ... os error 17; falling back to
122
+ # copy"). For build-isolation envs containing a chunky toolchain
123
+ # like ziglang (~50MB, hundreds of small files, PE binaries that
124
+ # Defender deep-scans), that's ~48s per wheel install vs ~8s
125
+ # when hardlinking works.
126
+ #
127
+ # Pinning UV_CACHE_DIR to %LOCALAPPDATA% (always on C:) puts the
128
+ # cache on the same drive as the build env and restores
129
+ # hardlinking.
130
+ #
131
+ # Tracking upstream (acknowledged but not fixed; eifinger himself
132
+ # tried in setup-uv#451 and reverted in #460):
133
+ # https://github.com/astral-sh/setup-uv/issues/227
134
+ # https://github.com/astral-sh/setup-uv/issues/371
135
+ # https://github.com/astral-sh/setup-uv/pull/451
136
+ - if: runner.os == 'Windows'
137
+ shell: pwsh
138
+ run: |
139
+ $cache = Join-Path $env:LOCALAPPDATA 'uv-cache'
140
+ "UV_CACHE_DIR=$cache" | Out-File -FilePath $env:GITHUB_ENV -Append
141
+
142
+ - uses: pypa/cibuildwheel@v3.4.1
143
+ with:
144
+ only: ${{ matrix.only }}
145
+
146
+ - uses: actions/upload-artifact@v6
147
+ with:
148
+ name: wheels-${{ matrix.only }}
149
+ path: wheelhouse/*.whl
150
+ compression-level: 0
151
+
152
+ # Publish to PyPI on a published GitHub release, via OIDC trusted
153
+ # publishing (no API tokens). Gated by the `pypi` environment configured
154
+ # on this repo. Runs only on release:published, so pushes/PRs never upload.
155
+ to-pypi:
156
+ name: publish to PyPI
157
+ needs: [sdist, wheels]
158
+ if: github.event_name == 'release' && github.event.action == 'published'
159
+ runs-on: ubuntu-latest
160
+ environment:
161
+ name: pypi
162
+ url: https://pypi.org/p/csar
163
+ permissions:
164
+ id-token: write # required for OIDC trusted publishing
165
+ steps:
166
+ - uses: actions/download-artifact@v6
167
+ with:
168
+ pattern: "{wheels-*,sdist}"
169
+ merge-multiple: true
170
+ path: dist
171
+
172
+ - uses: pypa/gh-action-pypi-publish@release/v1
csar-0.1.1/.gitignore ADDED
@@ -0,0 +1,32 @@
1
+ # Python
2
+ .venv/
3
+ __pycache__/
4
+ *.pyc
5
+ *.pyo
6
+ .pytest_cache/
7
+ .mypy_cache/
8
+ .ruff_cache/
9
+ .coverage
10
+
11
+ # Build artifacts
12
+ build/
13
+ dist/
14
+ *.egg-info/
15
+
16
+ # Bundled static library (produced by the zig build)
17
+ src/csar/libcsar.*
18
+
19
+ # Zig build artifacts
20
+ zig-out/
21
+ .zig-cache/
22
+ zig-pkg/
23
+
24
+ # uv lockfile — opt in if you want it tracked
25
+ uv.lock
26
+
27
+ # Script outputs (generated plots), at any depth (incl. explorations/out/)
28
+ scripts/**/out/
29
+
30
+ # OS
31
+ .DS_Store
32
+ .ipynb_checkpoints/
csar-0.1.1/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 AJ Friend
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.
csar-0.1.1/PKG-INFO ADDED
@@ -0,0 +1,215 @@
1
+ Metadata-Version: 2.4
2
+ Name: csar
3
+ Version: 0.1.1
4
+ Summary: Conic spherical aspect-ratio solver: tightest enclosing cone of a point set on the unit sphere.
5
+ Keywords: conic,spherical,aspect-ratio,cone,geometry,geodesy,geospatial,optimization,zig
6
+ Author-Email: AJ Friend <ajfriend@gmail.com>
7
+ License-Expression: MIT
8
+ License-File: LICENSE
9
+ Classifier: Development Status :: 3 - Alpha
10
+ Classifier: Intended Audience :: Developers
11
+ Classifier: Intended Audience :: Science/Research
12
+ Classifier: Operating System :: MacOS
13
+ Classifier: Operating System :: Microsoft :: Windows
14
+ Classifier: Operating System :: POSIX :: Linux
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Programming Language :: Python :: 3.13
19
+ Classifier: Programming Language :: Python :: 3.14
20
+ Classifier: Programming Language :: Python :: Implementation :: CPython
21
+ Classifier: Programming Language :: Zig
22
+ Classifier: Topic :: Scientific/Engineering :: GIS
23
+ Classifier: Topic :: Scientific/Engineering :: Mathematics
24
+ Project-URL: Homepage, https://github.com/ajfriend/csar_py
25
+ Project-URL: Repository, https://github.com/ajfriend/csar_py
26
+ Project-URL: Issues, https://github.com/ajfriend/csar_py/issues
27
+ Project-URL: Upstream Zig package, https://github.com/ajfriend/csar_zig
28
+ Requires-Python: >=3.11
29
+ Requires-Dist: numpy>=1.24
30
+ Provides-Extra: plot
31
+ Requires-Dist: matplotlib>=3; extra == "plot"
32
+ Description-Content-Type: text/markdown
33
+
34
+ # csar: Conic Spherical Aspect Ratio
35
+
36
+ Python bindings for [`csar_zig`](https://github.com/ajfriend/csar_zig),
37
+ a spherical aspect-ratio solver. Given a point set on the unit sphere,
38
+ it finds the tightest ellipsoidal cone enclosing the points and returns
39
+ the cone's axis ratio.
40
+
41
+ A thin Cython binding over a small C-ABI shim that links the upstream
42
+ `csar` Zig package as a static archive — no separate shared library
43
+ ships in the wheel.
44
+
45
+ ## Install
46
+
47
+ Not on PyPI yet — install directly from GitHub. Point pip/uv at the git
48
+ URL, either the latest `main` or a tagged release:
49
+
50
+ ```sh
51
+ # latest main
52
+ pip install git+https://github.com/ajfriend/csar_py.git
53
+ uv pip install git+https://github.com/ajfriend/csar_py.git
54
+
55
+ # a specific tagged release
56
+ pip install git+https://github.com/ajfriend/csar_py.git@v0.1.0
57
+ uv pip install git+https://github.com/ajfriend/csar_py.git@v0.1.0
58
+ ```
59
+
60
+ For the optional plotting helper (`csar.plot_cone`), add the `plot` extra:
61
+
62
+ ```sh
63
+ pip install "csar[plot] @ git+https://github.com/ajfriend/csar_py.git"
64
+ ```
65
+
66
+ That path triggers a source build: meson-python pulls the Zig toolchain
67
+ from the `ziglang` PyPI wheel (`python -m ziglang build`), compiles the
68
+ upstream `csar_zig` package into a static archive — fetched over the
69
+ network from the URL pinned in `src/zig/build.zig.zon` — then cythonizes
70
+ `src/cython/_cy.pyx` and links the result against it. No host-level Zig
71
+ or Cython install is required (Python 3.11+).
72
+
73
+ ### Local development
74
+
75
+ ```sh
76
+ just test # reinstall + run the test suite
77
+ just reinstall # force a clean rebuild of the Zig extension
78
+ ```
79
+
80
+ ## Usage
81
+
82
+ ```python
83
+ import csar
84
+
85
+ # A sequence of points. Each point is (lat, lng) in degrees by
86
+ # default; pass geo='vec3' to give unit (x, y, z) triples instead.
87
+ pts = [
88
+ (0.0, 0.0),
89
+ (0.0, 90.0),
90
+ (90.0, 0.0),
91
+ ]
92
+
93
+ r = csar.solve(pts)
94
+ if isinstance(r, csar.Converged):
95
+ print(r.aspect_ratio) # cross-section axis ratio (>= 1)
96
+ print(r.Q[:, 0]) # unit cone axis (x, y, z) — first column of Q
97
+ ```
98
+
99
+ Any list/tuple of points works; a NumPy array is also accepted and is
100
+ read as an `(N, k)` array whose **rows are points** (`k` = 2 for the
101
+ `latlng`/`lonlat` families, 3 for `'vec3'`). The `geo` argument picks
102
+ the convention: `'latlng'` (default, h3's `(lat, lng)` order),
103
+ `'lonlat'` (GeoJSON's `(lon, lat)` order), their `_deg`/`_rad`
104
+ variants, or `'vec3'`.
105
+
106
+ Objects implementing `__geo_interface__` (shapely, geopandas, geojson,
107
+ h3 `LatLngPoly`/`LatLngMultiPoly`, …) can be passed directly — their
108
+ vertices are read as GeoJSON `(lon, lat)` degrees, so `geo` is ignored:
109
+
110
+ ```python
111
+ import geopandas as gpd
112
+
113
+ gdf = gpd.read_file('countries.geojson')
114
+ for name, geom in zip(gdf['ADMIN'], gdf.geometry):
115
+ r = csar.solve(geom) # shapely geometry via __geo_interface__
116
+ if isinstance(r, csar.Converged):
117
+ print(name, r.aspect_ratio)
118
+ ```
119
+
120
+ `MultiPoint`, `LineString`, `Polygon` (exterior ring), `MultiPolygon`,
121
+ and a `Feature` wrapping one of those are supported.
122
+
123
+ To visualize the fit, `csar.plot_cone(result, geom)` draws the outline
124
+ gnomonic-projected at the cone axis with the enclosing ellipse overlaid
125
+ — defaulting to north-up, scaled to metres, with labelled axes (needs
126
+ matplotlib — `pip install csar[plot]`):
127
+
128
+ ```python
129
+ csar.plot_cone(r, geom, title='Chile') # a finished figure from one call
130
+ ```
131
+
132
+ `scripts/states/` and `scripts/countries/` are full end-to-end examples.
133
+
134
+ `solve` runs all of this through `csar.to_vec3(points, geo=...)`, which
135
+ returns the `(N, 3)` array of unit vectors the solver actually sees.
136
+ Call it directly to inspect how your input maps onto the sphere:
137
+
138
+ ```python
139
+ csar.to_vec3([(0, 0), (0, 90), (90, 0)])
140
+ # array([[1., 0., 0.],
141
+ # [0., 1., 0.],
142
+ # [0., 0., 1.]])
143
+ ```
144
+
145
+ ## Outcomes
146
+
147
+ `solve` returns one of three outcome types — `Converged`, `Infeasible`,
148
+ or `DidNotConverge` (collectively `Outcome`) — mirroring the Zig
149
+ `Outcome` tagged union. Each carries **only** the fields meaningful for
150
+ its outcome, so you dispatch on the type rather than guarding nullable
151
+ fields:
152
+
153
+ ```python
154
+ match csar.solve(pts):
155
+ case csar.Converged() as c:
156
+ use(c.aspect_ratio, c.Q) # certified cone
157
+ case csar.Infeasible() as i:
158
+ handle(i.residual) # no enclosing cone exists
159
+ case csar.DidNotConverge() as d:
160
+ retry(d.gap, d.outer_iters) # hit the iteration cap
161
+ ```
162
+
163
+ On a `Converged`, `sigma` is the `(3,)` eigenvalue array and `Q` the
164
+ `(3, 3)` eigenbasis (column `i` pairs with `sigma[i]`; column 0 is the
165
+ axis), so the enclosing ellipsoid matrix is
166
+ `A = c.Q @ np.diag(c.sigma) @ c.Q.T`. `DidNotConverge` exposes the same
167
+ `sigma`/`Q`/`gap` for diagnostics but, being uncertified, deliberately
168
+ has no `aspect_ratio`. See the docstrings in `src/csar/__init__.py` for
169
+ the full field reference.
170
+
171
+ ## Solver paths
172
+
173
+ `solve` picks its solver via `method=`: `'auto'` (the default) resolves
174
+ to the library's recommended method — currently `'trust'`, a
175
+ trust-region descent that converges on every input family constructed
176
+ to date, including the wide/elongated inputs the original solver
177
+ structurally cannot. `'alternating'` is that original solver, kept for
178
+ continuity and for large dense near-circular inputs where it can still
179
+ be faster. The outcome's `.method` records the concrete path that ran.
180
+
181
+ ## Layout
182
+
183
+ ```
184
+ .
185
+ ├── pyproject.toml — meson-python config, package metadata
186
+ ├── meson.build — drives Zig static-archive build + Cython compile
187
+ ├── justfile — reinstall / test / wheel / examples / clean
188
+ ├── src/
189
+ │ ├── cython/_cy.pyx — Cython binding, exposes _cy.solve
190
+ │ ├── csar/
191
+ │ │ ├── __init__.py — gathers the public API (solve, to_vec3, plot_cone, Outcome…)
192
+ │ │ ├── convert.py — input → (N, 3) unit vectors: to_vec3, geo-interface
193
+ │ │ ├── outcomes.py — Converged/Infeasible/DidNotConverge + build()
194
+ │ │ ├── plot.py — plot_cone (optional; needs matplotlib)
195
+ │ │ └── solver.py — solve(): convert → _cy.solve → build
196
+ │ └── zig/
197
+ │ ├── build.zig — produces libcsar.{a,lib} (static archive)
198
+ │ ├── build.zig.zon — pins the csar_zig dependency
199
+ │ └── c_api.zig — pub export fn csar_solve
200
+ ├── scripts/ — examples (run via `just states|countries`)
201
+ │ ├── states/ — US-state aspect ratios (geopandas + csar)
202
+ │ └── countries/ — country aspect ratios (geopandas + csar)
203
+ └── tests/test_bindings.py
204
+ ```
205
+
206
+ ## History
207
+
208
+ `csar` was previously developed as
209
+ [`skar_py`](https://github.com/ajfriend/skar_py) and forked into this fresh
210
+ repository under a new name, since the `skar` name was already taken on PyPI.
211
+ The `skar_py` repository is preserved as-is for its history and provenance.
212
+
213
+ ## License
214
+
215
+ MIT — see [LICENSE](LICENSE).
@@ -0,0 +1,24 @@
1
+ # Changelog
2
+
3
+ Notable changes to csar. Terse by design — each entry points to the PR or
4
+ commit that carries the full detail.
5
+
6
+ ## [0.1.1]
7
+
8
+ - First PyPI release. Enable OIDC trusted-publishing upload in the `wheels`
9
+ workflow (`to-pypi` job, runs on a published GitHub release). No functional
10
+ change from 0.1.0 — the 0.1.0 tag predates the publish job, so this is the
11
+ first version pushed to PyPI.
12
+
13
+ ## [0.1.0]
14
+
15
+ - Initial release of `csar` (conic spherical aspect ratio): Python bindings
16
+ for the [`csar_zig`](https://github.com/ajfriend/csar_zig) solver. Given a
17
+ point set on the unit sphere, `csar.solve` returns the tightest enclosing
18
+ ellipsoidal cone as a typed `Converged` / `Infeasible` / `DidNotConverge`
19
+ outcome; `method=` selects the solver path (`'auto'` default, resolving to
20
+ the trust-region method). Ships as a Cython extension with the upstream Zig
21
+ solver statically linked — no separate shared library in the wheel.
22
+ Continues the prototype previously developed as
23
+ [`skar_py`](https://github.com/ajfriend/skar_py), preserved as-is for its
24
+ history and provenance.