Metadata-Version: 2.4
Name: csar
Version: 0.1.1
Summary: Conic spherical aspect-ratio solver: tightest enclosing cone of a point set on the unit sphere.
Keywords: conic,spherical,aspect-ratio,cone,geometry,geodesy,geospatial,optimization,zig
Author-Email: AJ Friend <ajfriend@gmail.com>
License-Expression: MIT
License-File: LICENSE
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Developers
Classifier: Intended Audience :: Science/Research
Classifier: Operating System :: MacOS
Classifier: Operating System :: Microsoft :: Windows
Classifier: Operating System :: POSIX :: Linux
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Classifier: Programming Language :: Python :: Implementation :: CPython
Classifier: Programming Language :: Zig
Classifier: Topic :: Scientific/Engineering :: GIS
Classifier: Topic :: Scientific/Engineering :: Mathematics
Project-URL: Homepage, https://github.com/ajfriend/csar_py
Project-URL: Repository, https://github.com/ajfriend/csar_py
Project-URL: Issues, https://github.com/ajfriend/csar_py/issues
Project-URL: Upstream Zig package, https://github.com/ajfriend/csar_zig
Requires-Python: >=3.11
Requires-Dist: numpy>=1.24
Provides-Extra: plot
Requires-Dist: matplotlib>=3; extra == "plot"
Description-Content-Type: text/markdown

# csar: Conic Spherical Aspect Ratio

Python bindings for [`csar_zig`](https://github.com/ajfriend/csar_zig),
a spherical aspect-ratio solver. Given a point set on the unit sphere,
it finds the tightest ellipsoidal cone enclosing the points and returns
the cone's axis ratio.

A thin Cython binding over a small C-ABI shim that links the upstream
`csar` Zig package as a static archive — no separate shared library
ships in the wheel.

## Install

Not on PyPI yet — install directly from GitHub. Point pip/uv at the git
URL, either the latest `main` or a tagged release:

```sh
# latest main
pip install git+https://github.com/ajfriend/csar_py.git
uv pip install git+https://github.com/ajfriend/csar_py.git

# a specific tagged release
pip install git+https://github.com/ajfriend/csar_py.git@v0.1.0
uv pip install git+https://github.com/ajfriend/csar_py.git@v0.1.0
```

For the optional plotting helper (`csar.plot_cone`), add the `plot` extra:

```sh
pip install "csar[plot] @ git+https://github.com/ajfriend/csar_py.git"
```

That path triggers a source build: meson-python pulls the Zig toolchain
from the `ziglang` PyPI wheel (`python -m ziglang build`), compiles the
upstream `csar_zig` package into a static archive — fetched over the
network from the URL pinned in `src/zig/build.zig.zon` — then cythonizes
`src/cython/_cy.pyx` and links the result against it. No host-level Zig
or Cython install is required (Python 3.11+).

### Local development

```sh
just test       # reinstall + run the test suite
just reinstall  # force a clean rebuild of the Zig extension
```

## Usage

```python
import csar

# A sequence of points. Each point is (lat, lng) in degrees by
# default; pass geo='vec3' to give unit (x, y, z) triples instead.
pts = [
    (0.0,  0.0),
    (0.0, 90.0),
    (90.0, 0.0),
]

r = csar.solve(pts)
if isinstance(r, csar.Converged):
    print(r.aspect_ratio)  # cross-section axis ratio (>= 1)
    print(r.Q[:, 0])       # unit cone axis (x, y, z) — first column of Q
```

Any list/tuple of points works; a NumPy array is also accepted and is
read as an `(N, k)` array whose **rows are points** (`k` = 2 for the
`latlng`/`lonlat` families, 3 for `'vec3'`). The `geo` argument picks
the convention: `'latlng'` (default, h3's `(lat, lng)` order),
`'lonlat'` (GeoJSON's `(lon, lat)` order), their `_deg`/`_rad`
variants, or `'vec3'`.

Objects implementing `__geo_interface__` (shapely, geopandas, geojson,
h3 `LatLngPoly`/`LatLngMultiPoly`, …) can be passed directly — their
vertices are read as GeoJSON `(lon, lat)` degrees, so `geo` is ignored:

```python
import geopandas as gpd

gdf = gpd.read_file('countries.geojson')
for name, geom in zip(gdf['ADMIN'], gdf.geometry):
    r = csar.solve(geom)            # shapely geometry via __geo_interface__
    if isinstance(r, csar.Converged):
        print(name, r.aspect_ratio)
```

`MultiPoint`, `LineString`, `Polygon` (exterior ring), `MultiPolygon`,
and a `Feature` wrapping one of those are supported.

To visualize the fit, `csar.plot_cone(result, geom)` draws the outline
gnomonic-projected at the cone axis with the enclosing ellipse overlaid
— defaulting to north-up, scaled to metres, with labelled axes (needs
matplotlib — `pip install csar[plot]`):

```python
csar.plot_cone(r, geom, title='Chile')  # a finished figure from one call
```

`scripts/states/` and `scripts/countries/` are full end-to-end examples.

`solve` runs all of this through `csar.to_vec3(points, geo=...)`, which
returns the `(N, 3)` array of unit vectors the solver actually sees.
Call it directly to inspect how your input maps onto the sphere:

```python
csar.to_vec3([(0, 0), (0, 90), (90, 0)])
# array([[1., 0., 0.],
#        [0., 1., 0.],
#        [0., 0., 1.]])
```

## Outcomes

`solve` returns one of three outcome types — `Converged`, `Infeasible`,
or `DidNotConverge` (collectively `Outcome`) — mirroring the Zig
`Outcome` tagged union. Each carries **only** the fields meaningful for
its outcome, so you dispatch on the type rather than guarding nullable
fields:

```python
match csar.solve(pts):
    case csar.Converged() as c:
        use(c.aspect_ratio, c.Q)      # certified cone
    case csar.Infeasible() as i:
        handle(i.residual)            # no enclosing cone exists
    case csar.DidNotConverge() as d:
        retry(d.gap, d.outer_iters)   # hit the iteration cap
```

On a `Converged`, `sigma` is the `(3,)` eigenvalue array and `Q` the
`(3, 3)` eigenbasis (column `i` pairs with `sigma[i]`; column 0 is the
axis), so the enclosing ellipsoid matrix is
`A = c.Q @ np.diag(c.sigma) @ c.Q.T`. `DidNotConverge` exposes the same
`sigma`/`Q`/`gap` for diagnostics but, being uncertified, deliberately
has no `aspect_ratio`. See the docstrings in `src/csar/__init__.py` for
the full field reference.

## Solver paths

`solve` picks its solver via `method=`: `'auto'` (the default) resolves
to the library's recommended method — currently `'trust'`, a
trust-region descent that converges on every input family constructed
to date, including the wide/elongated inputs the original solver
structurally cannot. `'alternating'` is that original solver, kept for
continuity and for large dense near-circular inputs where it can still
be faster. The outcome's `.method` records the concrete path that ran.

## Layout

```
.
├── pyproject.toml          — meson-python config, package metadata
├── meson.build             — drives Zig static-archive build + Cython compile
├── justfile                — reinstall / test / wheel / examples / clean
├── src/
│   ├── cython/_cy.pyx      — Cython binding, exposes _cy.solve
│   ├── csar/
│   │   ├── __init__.py     — gathers the public API (solve, to_vec3, plot_cone, Outcome…)
│   │   ├── convert.py      — input → (N, 3) unit vectors: to_vec3, geo-interface
│   │   ├── outcomes.py     — Converged/Infeasible/DidNotConverge + build()
│   │   ├── plot.py         — plot_cone (optional; needs matplotlib)
│   │   └── solver.py       — solve(): convert → _cy.solve → build
│   └── zig/
│       ├── build.zig       — produces libcsar.{a,lib} (static archive)
│       ├── build.zig.zon   — pins the csar_zig dependency
│       └── c_api.zig       — pub export fn csar_solve
├── scripts/                — examples (run via `just states|countries`)
│   ├── states/             — US-state aspect ratios (geopandas + csar)
│   └── countries/          — country aspect ratios (geopandas + csar)
└── tests/test_bindings.py
```

## History

`csar` was previously developed as
[`skar_py`](https://github.com/ajfriend/skar_py) and forked into this fresh
repository under a new name, since the `skar` name was already taken on PyPI.
The `skar_py` repository is preserved as-is for its history and provenance.

## License

MIT — see [LICENSE](LICENSE).
