Metadata-Version: 2.4
Name: polar-high
Version: 1.5.1
Summary: Python library for building indexed linear and mixed-integer programs in polars; assembles into HiGHS, exportable as MPS.
Author-email: Juha Kiviluoma <juha.kiviluoma@nodal-tools.fi>
Maintainer-email: Juha Kiviluoma <juha.kiviluoma@nodal-tools.fi>
License: Apache-2.0
Project-URL: Homepage, https://github.com/nodal-tools/polar-high
Project-URL: Documentation, https://nodal-tools.github.io/polar-high/
Project-URL: Issues, https://github.com/nodal-tools/polar-high/issues
Project-URL: Changelog, https://github.com/nodal-tools/polar-high/blob/main/CHANGELOG.md
Keywords: optimization,linear-programming,lp,mip,highs,polars,edsl
Classifier: Development Status :: 5 - Production/Stable
Classifier: Intended Audience :: Science/Research
Classifier: License :: OSI Approved :: Apache Software License
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Scientific/Engineering :: Mathematics
Requires-Python: >=3.11
Description-Content-Type: text/markdown
License-File: LICENSE
License-File: NOTICE
Requires-Dist: highspy
Requires-Dist: polars
Requires-Dist: numpy
Provides-Extra: test
Requires-Dist: pytest; extra == "test"
Provides-Extra: lint
Requires-Dist: ruff>=0.6; extra == "lint"
Provides-Extra: docs
Requires-Dist: mkdocs-material; extra == "docs"
Requires-Dist: mkdocstrings[python]; extra == "docs"
Requires-Dist: mike; extra == "docs"
Requires-Dist: mkdocs-include-markdown-plugin; extra == "docs"
Provides-Extra: gurobi
Requires-Dist: gurobipy; extra == "gurobi"
Requires-Dist: scipy; extra == "gurobi"
Provides-Extra: copt
Requires-Dist: coptpy; extra == "copt"
Requires-Dist: scipy; extra == "copt"
Provides-Extra: cplex
Requires-Dist: cplex; extra == "cplex"
Provides-Extra: xpress
Requires-Dist: xpress; extra == "xpress"
Dynamic: license-file

# polar-high

[![PyPI](https://img.shields.io/pypi/v/polar-high.svg?v=1)](https://pypi.org/project/polar-high/)
[![Python versions](https://img.shields.io/pypi/pyversions/polar-high.svg?v=1)](https://pypi.org/project/polar-high/)
[![License](https://img.shields.io/pypi/l/polar-high.svg?v=1)](https://github.com/nodal-tools/polar-high/blob/main/LICENSE)
[![tests](https://img.shields.io/github/actions/workflow/status/nodal-tools/polar-high/test.yml?branch=main&label=tests)](https://github.com/nodal-tools/polar-high/actions/workflows/test.yml)
[![docs](https://img.shields.io/github/actions/workflow/status/nodal-tools/polar-high/docs.yml?branch=main&label=docs)](https://nodal-tools.fi/polar-high/)
[![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff)

A Python library for building and solving large linear and
mixed-integer optimisation programs, i.e. domain specific language
(DSL) for algebraic modelling. Variables and parameters are
[polars](https://pola.rs/) DataFrames, expressions are joined and
grouped lazily, and the matrix is assembled directly through
[HiGHS](https://highs.dev/) — or exported as MPS for any other LP/MIP
solver. The kernel is intentionally domain-free: it has no opinions
about energy systems, supply chains, or any specific application.

## Install

```bash
pip install polar-high
```

Requires Python 3.11+. HiGHS ships in `highspy`, no separate install.

## Quickstart

A tiny dispatch LP — wind + coal over three hours, minimise cost
subject to capacity and per-hour demand.

```python
import polars as pl

from polar_high import Param, Problem, Sum

p = Problem()

# Index sets — declared once, reused below
unit_index = pl.DataFrame({"unit": ["wind", "coal"]})
time_index = pl.DataFrame({"hour": [1, 2, 3]})

# Decision variable v_production[unit, hour] >= 0
composite_index = unit_index.join(time_index, how="cross")
v_production = p.add_var(
    "v_production",
    dims=("unit", "hour"),
    index=composite_index,
    lower=0.0,
)

# Operating cost per unit
cost = Param(
    ("unit",),
    pl.DataFrame({"unit": ["wind", "coal"], "value": [2.0, 8.0]}),
)

# Available capacity per unit per hour — built per-unit, then concatenated
cap_wind = time_index.with_columns(
    pl.lit("wind").alias("unit"),
    pl.Series("value", [3.0, 1.0, 4.0]),  # wind drops in hour 2
)
cap_coal = time_index.with_columns(
    pl.lit("coal").alias("unit"),
    pl.Series("value", [10.0, 10.0, 10.0]),
)
cap = Param(
    ("unit", "hour"),
    pl.concat([cap_wind, cap_coal]).select("unit", "hour", "value"),
)

# Demand per hour
demand = Param(
    ("hour",),
    time_index.with_columns(pl.Series("value", [5.0, 6.0, 4.0])),
)

# Minimise total cost
p.set_objective(cost * v_production, sense="min")

# v_production[unit, hour] <= cap[unit, hour]
p.add_cstr(
    "capacity",
    over=composite_index,
    lhs_terms={"production": v_production},
    sense="<=",
    rhs_terms={"cap": cap},
)

# Σ_unit v_production[unit, hour] == demand[hour]
p.add_cstr(
    "demand_balance",
    over=time_index,
    lhs_terms={"production": Sum(v_production, over=("unit",))},
    sense="==",
    rhs_terms={"demand": demand},
)

sol = p.solve()
print(f"objective: {sol.obj}")  # 72.0
print(sol.value("v_production"))
```

The same code lives at [`tests/fixtures/quickstart_example.py`](tests/fixtures/quickstart_example.py) and is executed in the test suite, so README and docs stay in sync.

## Enum dtype handling

polars 1.40 treats `pl.Enum` as a nominal type and refuses to join
two columns when the Enums carry different categorical vocabularies
— even if one's categories are a strict subset of the other's. That
strict semantics gets in the way of typical LP-DSL workflows, where
one `Param` may live on a subset of an axis (e.g. only the units
with a finite capacity) while another lives on the full axis.

polar-high auto-aligns Enum-typed join keys at every internal
`.join()` site:

- **same Enum on both sides** — no change.
- **one side's categories ⊆ the other's** — the narrower side is
  up-cast to the wider Enum dtype (with `strict=False`; any values
  outside the wider vocab become null, which inner / left joins
  drop or surface naturally).
- **Enum vs `pl.Utf8` / `pl.String`** — the string side is cast to
  the Enum dtype.
- **neither vocab is a subset of the other** — `ValueError` with
  actionable guidance (cast to `pl.Utf8` or build a union Enum).

No try/except shims, no Utf8 fallbacks for the disjoint case. The
behaviour is implemented by a generic helper that knows nothing
about specific axis names or vocab contents. See
`tests/test_enum_dtype_align.py` for the contract.

## Documentation

Full docs at **<https://nodal-tools.fi/polar-high/>** — published
with MkDocs + [mike](https://github.com/jimporter/mike) for per-version
reads.

- **[Concepts](https://nodal-tools.fi/polar-high/latest/concepts/)** —
  the indexed-frame mental model (`Var`, `Param`, `Sum`, `Where`,
  `Lag`, broadcasting/join semantics).
- **[Guide](https://nodal-tools.fi/polar-high/latest/guide/warm-starting/)** —
  warm-starting, Lagrangian decomposition, performance tuning,
  debugging.
- **[API reference](https://nodal-tools.fi/polar-high/latest/reference/api/)** —
  autogenerated from docstrings.
- **[Compare](https://nodal-tools.fi/polar-high/latest/compare/alternatives/)** —
  how polar-high relates to Pyomo, JuMP, gurobipy, linopy, and
  GNU MathProg.

Build locally: `pip install -e ".[docs]" && mkdocs serve`.

## Used by

polar-high is the build engine behind the
[FlexTool](https://github.com/irena-flextool/flextool) energy-system
modelling toolkit (still in dev branch only, 7.5.2026 situation).
FlexTool's fleet of system tests (from earlier
GNU MathProg to HiGHS implementation) has been used to test
polar-high in real modelling use cases. In addition
polar-high kernel has its own set of unit and system tests.

## Created by

polar-high was created by **Juha Kiviluoma** of **Nodal-Tools** using
**Claude Opus**.

## License

Apache-2.0 — see [LICENSE](LICENSE) and [NOTICE](NOTICE).
Changelog: [CHANGELOG.md](CHANGELOG.md).
