Metadata-Version: 2.4
Name: pyblackwell
Version: 0.1.1
Summary: Blackwell approachability primitives for finite vector-payoff games.
Author: Zsolt Döme
License-Expression: MIT
Project-URL: Homepage, https://gitlab.com/domezsolt/pyblackwell
Project-URL: Repository, https://gitlab.com/domezsolt/pyblackwell
Project-URL: Issues, https://gitlab.com/domezsolt/pyblackwell/-/issues
Keywords: blackwell-approachability,online-learning,game-theory,vector-payoffs,calibration
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Science/Research
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Topic :: Scientific/Engineering :: Mathematics
Requires-Python: >=3.10
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: numpy>=1.24
Requires-Dist: scipy>=1.10
Requires-Dist: typing-extensions>=4.8
Provides-Extra: cvx
Requires-Dist: cvxpy>=1.4; extra == "cvx"
Provides-Extra: torch
Requires-Dist: torch>=2.2; extra == "torch"
Provides-Extra: jax
Requires-Dist: jax>=0.4; extra == "jax"
Requires-Dist: jaxlib>=0.4; extra == "jax"
Provides-Extra: diffopt
Requires-Dist: cvxpylayers>=1.0; extra == "diffopt"
Requires-Dist: jaxopt>=0.8; extra == "diffopt"
Provides-Extra: rl
Requires-Dist: gymnasium>=0.29; extra == "rl"
Provides-Extra: sklearn
Requires-Dist: scikit-learn>=1.3; extra == "sklearn"
Provides-Extra: plot
Requires-Dist: matplotlib>=3.7; extra == "plot"
Provides-Extra: pandas
Requires-Dist: pandas>=2.0; extra == "pandas"
Provides-Extra: docs
Requires-Dist: mkdocs>=1.5; extra == "docs"
Requires-Dist: mkdocs-material>=9.5; extra == "docs"
Requires-Dist: mkdocstrings[python]>=0.24; extra == "docs"
Provides-Extra: test
Requires-Dist: pytest>=7.4; extra == "test"
Requires-Dist: pytest-cov>=4.1; extra == "test"
Requires-Dist: hypothesis>=6.88; extra == "test"
Provides-Extra: benchmarks
Requires-Dist: pytest-benchmark>=4.0; extra == "benchmarks"
Provides-Extra: release
Requires-Dist: build>=1.2; extra == "release"
Requires-Dist: twine>=5.0; extra == "release"
Provides-Extra: dev
Requires-Dist: pytest>=7.4; extra == "dev"
Requires-Dist: pytest-cov>=4.1; extra == "dev"
Requires-Dist: hypothesis>=6.88; extra == "dev"
Requires-Dist: mkdocs>=1.5; extra == "dev"
Requires-Dist: mkdocs-material>=9.5; extra == "dev"
Requires-Dist: mkdocstrings[python]>=0.24; extra == "dev"
Requires-Dist: pytest-benchmark>=4.0; extra == "dev"
Requires-Dist: build>=1.2; extra == "dev"
Requires-Dist: twine>=5.0; extra == "dev"
Requires-Dist: ruff>=0.4; extra == "dev"
Requires-Dist: mypy>=1.8; extra == "dev"
Requires-Dist: pre-commit>=3.6; extra == "dev"
Provides-Extra: all
Requires-Dist: cvxpy>=1.4; extra == "all"
Requires-Dist: torch>=2.2; extra == "all"
Requires-Dist: jax>=0.4; extra == "all"
Requires-Dist: jaxlib>=0.4; extra == "all"
Requires-Dist: cvxpylayers>=1.0; extra == "all"
Requires-Dist: jaxopt>=0.8; extra == "all"
Requires-Dist: gymnasium>=0.29; extra == "all"
Requires-Dist: scikit-learn>=1.3; extra == "all"
Requires-Dist: matplotlib>=3.7; extra == "all"
Requires-Dist: pandas>=2.0; extra == "all"
Requires-Dist: mkdocs>=1.5; extra == "all"
Requires-Dist: mkdocs-material>=9.5; extra == "all"
Requires-Dist: mkdocstrings[python]>=0.24; extra == "all"
Requires-Dist: pytest>=7.4; extra == "all"
Requires-Dist: pytest-cov>=4.1; extra == "all"
Requires-Dist: hypothesis>=6.88; extra == "all"
Requires-Dist: pytest-benchmark>=4.0; extra == "all"
Requires-Dist: build>=1.2; extra == "all"
Requires-Dist: twine>=5.0; extra == "all"
Requires-Dist: ruff>=0.4; extra == "all"
Requires-Dist: mypy>=1.8; extra == "all"
Requires-Dist: pre-commit>=3.6; extra == "all"
Dynamic: license-file

# PyBlackwell

PyBlackwell provides reusable Blackwell approachability primitives for finite
vector-payoff games, target-set geometry, minimax action selection, and
convergence certificates.

The classical theorem-facing path is the finite-game solver with bounded
vector payoffs, closed convex targets, full-information or expected-payoff
updates, and exact or tolerance-controlled projection and minimax oracles.
Wrappers for sampled calibration, bandit feedback, and RL-style workflows
report diagnostics, but their guarantees require the additional assumptions
stated in the theory notes.

The base install uses NumPy and SciPy only:

```bash
python -m pip install pyblackwell
```

## Quickstart

```python
import numpy as np

from pyblackwell import (
    BlackwellApproachability,
    MatrixGame,
    PointTarget,
    convergence_summary,
    summary_to_log_record,
)

payoffs = np.array(
    [
        [[-1.0], [1.0]],
        [[1.0], [-1.0]],
    ]
)

game = MatrixGame(payoffs)
target = PointTarget([0.0])
solver = BlackwellApproachability(game=game, target=target)

for env_action in [0, 1] * 50:
    strategy = solver.strategy()
    payoff = game.payoff(strategy, env_action)
    solver.update(payoff, env_action=env_action)

print(solver.summary())
print(summary_to_log_record(solver.summary()))
print(solver.certificate().to_dict())

diagnostics = convergence_summary(solver.trace(), distance_tolerance=1e-6)
print(diagnostics.to_dict())
```

Trace diagnostics can be summarized without optional dependencies.

For long runs where only aggregate diagnostics are needed, construct solvers
with `summary_only=True`. The solver still updates `summary()` and
`certificate()`, while `trace()` returns empty histories instead of retaining
per-iteration arrays.

Install `pyblackwell[plot]` to use Matplotlib diagnostics such as
`plot_convergence_summary`, `plot_distance_trace`, and
`plot_payoff_averages_2d`.

## Feedback Modes

Explicit feedback helpers distinguish full-information payoff tables from
bandit observations:

```python
from pyblackwell import BanditFeedbackMode, FullInformationFeedbackMode

full = FullInformationFeedbackMode(expected_actions=2, expected_dim=1)
full_feedback = full.observe([[1.0], [0.0]])
print(full_feedback.diagnostics.to_dict())

bandit = BanditFeedbackMode(n_actions=2, exploration=0.1, rng=0)
action, probabilities = bandit.sample([0.75, 0.25])
bandit_feedback = bandit.observe(action, [1.0])
print(probabilities)
print(bandit_feedback.diagnostics.to_dict())
```

Bandit diagnostics report the sampling probability, exploration setting, and
importance-weight variance proxy so estimator caveats are visible in logs.

## Command-line utilities

Packaged example configurations can be run without writing a Python script:

```bash
pyblackwell-example matching-pennies-point
```

Game-theoretic reduction examples are packaged too:

```bash
pyblackwell-example external-regret-orthant
pyblackwell-example correlated-equilibrium-deviation-regret
```

Use `pyblackwell-example --list` to show available example names. The command
prints JSON with the run summary, metadata, and final certificate. Runtime
options can override the packaged solver, target set, iteration count,
tolerance, seed, and output directory:

```bash
pyblackwell-example matching-pennies-point --solver oco --target-set ball \
  --iterations 50 --tolerance 1e-6 --seed 42 --output-dir runs
```

When `--output-dir` is provided, the command writes `summary.json` in that
directory. Add `--save-trace` to also write a compressed `trace.npz` archive
with the same run metadata.

Saved `.npz` traces can be inspected from the command line:

```bash
pyblackwell-trace-summary path/to/trace.npz
```

The command prints JSON with run metadata, history lengths, distance and
residual diagnostics, and final payoff averages.

Benchmark configurations can be run and summarized without importing Python
helpers:

```bash
pyblackwell-benchmark run --mode smoke --iterations 20 --output-dir runs/bench
pyblackwell-benchmark run --mode research --group box-target
pyblackwell-benchmark summarize runs/bench/benchmark_results.json
```

Benchmark filtering accepts repeatable `--include`, `--exclude`, and `--group`
options. The command prints JSON and writes `benchmark_results.json` when
`--output-dir` is provided.

## Showcase experiment

The showcase compares target-set approachability against scalar, fixed-policy,
no-regret, and empirical-rate baselines on a multi-objective box target,
external regret, and binary calibration task:

```bash
OMP_NUM_THREADS=1 MKL_NUM_THREADS=1 OPENBLAS_NUM_THREADS=1 \
python experiments/showcase_0_1_0.py --profile smoke \
  --output-dir runs/showcase_smoke
```

Use `--profile full` for the longer single-CPU run. Use `--profile extended`
for the broader run with more seeds, more iterations, additional baselines, and
sparse distance-curve output configured for the same three-hour budget:

```bash
OMP_NUM_THREADS=1 MKL_NUM_THREADS=1 OPENBLAS_NUM_THREADS=1 \
python experiments/showcase_0_1_0.py --profile extended \
  --output-dir runs/showcase_extended
```

Add `--seed-variation` when seeds should sample perturbed game instances and
late-shift calibration streams instead of repeating the deterministic fixture:

```bash
OMP_NUM_THREADS=1 MKL_NUM_THREADS=1 OPENBLAS_NUM_THREADS=1 \
python experiments/showcase_0_1_0.py --profile extended --seed-variation \
  --output-dir runs/showcase_extended_seed_variation
```

The command prints a compact JSON summary and writes `manifest.json`,
`summary.json`, `summary.md`, `results.jsonl`, and `distance_curves.csv` into
the output directory. The primary metric is final distance to the task's target
set; per-run calibration diagnostics are kept in `results.jsonl`. Use
`--curve-stride N` to thin curve rows for custom long runs.

Representative seed-varied extended result:

```text
profile: extended
seed_variation: true
seeds: 20
iterations per task: 10,000
calibration grid: 15
curve stride: 20
recorded runtime: 1004.9 seconds
```

| Task | PyBlackwell | Best baseline | Comparison | Certificate |
| --- | ---: | ---: | --- | --- |
| Binary calibration | 0.000227565 | 0.000369325 (`ema_alpha_0_05`) | 38.4% lower | yes |
| Box target | 0.000009844 | 0.152752 (`scalar_reward_1`) | 99.99% lower | yes |
| External regret | 0.003623337 | 0.001816885 (`hedge_eta_0_1`) | 99.4% higher | yes |

Lower is better; values are mean final distances to the target set. Positive
comparisons mean PyBlackwell is closer to the target than the best baseline;
the external-regret row is the exception after the regret-sign correction,
where the tuned Hedge baseline has the lower finite-horizon distance. The
seed-varied run produces spread in the box-target and external-regret tasks.
Binary calibration PyBlackwell final distance is identical across the sampled
late-shift streams, while several calibration baselines vary.

The public API is exported from `pyblackwell`. A compatibility module named
`approachability` re-exports the same core objects for examples that follow the
original implementation plan. The API reference documents which module import
paths are stable, which are experimental, and which implementation modules are
private.

## Scope

PyBlackwell is not a general extensive-form game solver or a generic convex
optimization toolkit. Its core object is an approachable target set, with
projection, separation, support, repeated-game solvers, and explicit numerical
certificates. Extensive-form integrations should stay adapter-based: external
game solvers own tree construction, CFR or sequence-form updates, and
equilibrium claims, while PyBlackwell can receive validated payoff,
strategy-regret, and diagnostic records.

## Development

```bash
make check
make package-check
```

`make package-check` builds the source distribution and wheel, runs strict
metadata validation on both artifacts, then installs the wheel into an isolated
environment and imports the package.

The base package avoids importing optional CVXPY, PyTorch, JAX, pandas,
Matplotlib, CVXPYLayers, JAXopt, scikit-learn, and Gymnasium dependencies at
import time.
CVXPY-backed convex and SDP target projections remain behind the `cvx` extra;
differentiable-optimization projection adapters remain behind the `diffopt`
extra; other optional integrations should remain behind extras and lazy
imports.
