Metadata-Version: 2.4
Name: primalsolver
Version: 0.1.2
Summary: PrimalSolver: dependency-free convex optimization (LP/QP/SOCP/SDP/exp-power/MIP) in C99, callable from Python via ctypes.
Author: Gaetano Minardi
License: Apache-2.0
Project-URL: Homepage, https://github.com/c-vision/Primal
Keywords: optimization,linear-programming,quadratic-programming,socp,sdp,conic,mip,ctypes
Classifier: Programming Language :: C
Classifier: Programming Language :: Python :: 3
Classifier: License :: OSI Approved :: Apache Software License
Classifier: Topic :: Scientific/Engineering :: Mathematics
Requires-Python: >=3.8
Description-Content-Type: text/markdown
License-File: LICENSE
Dynamic: license-file

# primalsolver

Python bindings for **PrimalSolver** — a dependency-free convex optimization
solver in C99 (LP / QP / SOCP / SDP / exp-power / MIP).

The package ships a pre-built shared library (`libprimal.dylib` / `.so` / `.dll`)
inside the wheel and calls it through `ctypes`: no C toolchain is needed to
install, and the whole public API is exposed.

> Upstream C library: <https://github.com/c-vision/Primal>. This package treats
> its sources as read-only input and never modifies them.

```python
from primalsolver import Model, BK, SOLSTA

with Model(maxcon=2, maxvar=2) as m:
    m.obj([0, 1], [-3.0, -2.0])           # min -3 x0 - 2 x1
    m.a_ij(0, 0, 1.0); m.a_ij(0, 1, 1.0)  # x0 +  x1 <= 4
    m.a_ij(1, 0, 1.0); m.a_ij(1, 1, 3.0)  # x0 + 3x1 <= 6
    m.con_bounds(0, BK.UP, up=4.0)
    m.con_bounds(1, BK.UP, up=6.0)
    m.var_bounds(0, BK.RA, 0.0, 3.0)      # 0 <= x0 <= 3
    m.var_bounds(1, BK.RA, 0.0, 3.0)
    r = m.solve()

assert r.solsta == SOLSTA.OPTIMAL
print(r.x, r.objective)                    # [3.0, 1.0] -11.0
```

## Requirements

- Python >= 3.8, **no runtime dependencies** other than the standard library.
- A platform wheel is provided per OS/arch (macOS, Linux, Windows). On a platform
  without a wheel you can build from source (see below).

## Install

```sh
pip install primalsolver
```

Verify:

```sh
python -c "import primalsolver as ps; print(ps.version())"
```

## Quickstart

Build and solve a model in code (above), or solve a **model file** — MPS, CPLEX
LP, OPF and CBF are auto-detected by content/extension:

```python
from primalsolver import Model, SOLSTA

with Model() as m:
    m.read("model.mps")
    r = m.solve()

print(r.solsta, r.x, r.objective)
```

The high-level `Model` covers the common path. Everything else is reachable
through the generated low-level binding.

## Full API (all 547 functions)

`tools/gen_bindings.py` parses the C header `primal.h` and emits
`src/primalsolver/_bindings.py` with a ctypes signature for **every** `PRIMAL_*`
function. `_native.py` applies them and re-exports them, so the entire surface is
available:

```python
import primalsolver as ps

ps.PRIMAL_getdualray(...)      # every PRIMAL_* is available at package level
ps.FUNCTIONS                    # name -> ctypes function object (547)
ps.MISSING                      # declared in the header but absent in the lib (empty = OK)
ps._native.optimize             # short aliases (PRIMAL_ removed) used by Model
```

Handles (`PRIMALenv_t` / `PRIMALtask_t`) are opaque C pointers: the functions
that create/destroy them take a *pointer to the handle*, so pass
`ctypes.byref(...)`. Callback arguments are exposed as `c_void_p`; the two
variadic functions (`PRIMAL_echotask`, `PRIMAL_echoenv`) are exposed without
`argtypes` (ctypes cannot type C varargs) — pass explicit ctypes objects for the
extra arguments.

Regenerate the binding **after any change to `primal.h`**:

```sh
python tools/gen_bindings.py
```

## What is shipped

- **Wheel**: only the `primalsolver` package — `__init__.py`, `_native.py`, the
  generated `_bindings.py`, the compiled library in `_lib/`, plus `README.md` and
  `LICENSE`.
- **Python examples** live in `../python_examples/` (tracked in the C repository),
  not in this package. No test files are shipped.

## Building from source

The C sources are **read-only input**; this project never modifies them. `setup.py`
compiles the shared library into `src/primalsolver/_lib/` before packaging.

Source directory resolution, in order:

1. `$PRIMAL_C_SRC` — an explicit C checkout;
2. `csrc/` — a vendored snapshot (`python sync_csrc.py`), used by the sdist/CI;
3. `..` — the parent directory, i.e. the live C repository (default).

`csrc/` is generated by `sync_csrc.py` and is **gitignored**: the C sources have
a single home in the C repository. CI fetches them from
`github.com/c-vision/Primal` and regenerates `csrc/` before building.

Development install:

```sh
python -m venv .venv && . .venv/bin/activate
pip install -U pip setuptools wheel
pip install -e .
python -c "import primalsolver as ps; print(ps.version())"
```

Build a wheel locally (needs only `setuptools` + `wheel`, no `build` package):

```sh
python -m pip wheel . --no-build-isolation --no-deps -w dist
```

> The wheel bundles a native library, so it is **platform-specific**:
> `py3-none-<platform>` (e.g. `py3-none-macosx_11_0_arm64`), never `none-any`.

## Building the distribution wheels (all platforms)

Config lives in `[tool.cibuildwheel]` (`pyproject.toml`); `cibuildwheel`
compiles inside each target environment (required for manylinux) and runs
`auditwheel` / `delocate`. In CI the C sources are fetched from
`github.com/c-vision/Primal` and vendored with `sync_csrc.py` before building
(see `.github/workflows/wheels.yml`).

```sh
pip install cibuildwheel
python -m cibuildwheel --output-dir dist        # current platform
python -m cibuildwheel --platform <os> --output-dir dist
```

Notes:

- macOS: set `MACOSX_DEPLOYMENT_TARGET` (e.g. `11.0`) for older systems and
  codesign the dylib (`codesign -s -`); `cibuildwheel` handles `delocate`.
- Windows: built with mingw-w64 (`before-all = "choco install -y mingw"`) and
  `-static` so the DLL is standalone.
- Linux: `-pthread` is linked (manylinux glibc < 2.34 keeps pthread in
  `libpthread`).

## Release procedure

1. **Generate the vendored snapshot** (makes the sdist self-contained; it is
   gitignored, not committed):

   ```sh
   python sync_csrc.py
   ```

2. **Regenerate the binding** if `primal.h` changed:

   ```sh
   python tools/gen_bindings.py
   ```

3. **Bump the version** — the single source of truth is `__version__` in
   `src/primalsolver/__init__.py` (`pyproject.toml` reads it via
   `dynamic = ["version"]`).

4. **Check locally**:

   ```sh
   pip install -e .
   python -c "import primalsolver as ps; print(ps.version())"
   python -m pip wheel . --no-build-isolation --no-deps -w dist
   ```

5. **Commit and tag** (in the Python repository, never the C one):

   ```sh
   git add -A && git commit -m "release vX.Y.Z"
   git tag vX.Y.Z
   git push origin main --tags
   ```

6. **Publish**: pushing a **tag** `vX.Y.Z` triggers
   `.github/workflows/wheels.yml`, which builds the wheels + sdist and uploads
   them as artifacts. Publishing to PyPI runs when a **GitHub Release** is
   published for that tag, using the repository secret `PYPI_API_TOKEN` (token
   auth).

   Prerequisites (one-time): add `PYPI_API_TOKEN` in the repo (Settings →
   Secrets and variables → Actions) with the PyPI API token.

   Manual/fallback publish (e.g. TestPyPI):

   ```sh
   pip install build twine
   python -m build
   twine check dist/*
   twine upload --repository testpypi dist/*   # then: twine upload dist/*
   ```

## Repository layout

```
pyproject.toml          # metadata + cibuildwheel config
setup.py                # compiles the shared lib; forces the platform wheel tag
MANIFEST.in             # sdist contents (vendored csrc, no prebuilt binaries)
sync_csrc.py            # vendored C-source snapshot -> csrc/
tools/gen_bindings.py   # primal.h -> src/primalsolver/_bindings.py (547 fns)
csrc/                   # C-source snapshot (generated by sync_csrc.py; gitignored)
src/primalsolver/       # the installed package (__init__, _native, _bindings, _lib)
.github/workflows/wheels.yml
```

## License

Apache-2.0, same as PrimalSolver. See `LICENSE`.
