Metadata-Version: 2.1
Name: optimalq
Version: 1.1.0
Summary: QAOA (Quantum Approximate Optimization Algorithm) toolkit — problem encoding, ansatz construction, and classical optimization, simulated via mps_xtrap
Author-email: OscInspire <oscinspire@gmail.com>
License: LicenseRef-optimalQ-BUSL-1.1
Project-URL: Source, https://pypi.org/project/optimalQ/
Project-URL: Dependency, https://pypi.org/project/mps-xtrap/
Keywords: qaoa,quantum optimization,quantum computing,ising,qubo,combinatorial optimization,mps,matrix-product-states
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: License :: Other/Proprietary License
Classifier: Topic :: Scientific/Engineering :: Physics
Classifier: Topic :: Scientific/Engineering :: Mathematics
Classifier: Intended Audience :: Science/Research
Classifier: Operating System :: OS Independent
Requires-Python: >=3.9
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: mps-xtrap >=2.4.0
Requires-Dist: numpy >=1.21
Requires-Dist: scipy >=1.7
Provides-Extra: dev
Requires-Dist: pytest >=7.0 ; extra == 'dev'

# optimalQ

**optimalQ** is a QAOA (Quantum Approximate Optimization Algorithm) toolkit.
It owns all problem-specific logic - mapping your problem onto an Ising
Hamiltonian, building the QAOA ansatz circuit, and running the classical
outer-loop optimization - and delegates every actual quantum circuit
simulation to **[mps_xtrap](https://pypi.org/project/mps-xtrap/)**, an
MPS-based quantum circuit simulator with Richardson extrapolation, built
and maintained by OscInspire.

> **optimalQ writes the algorithm. mps_xtrap runs the circuits.**
> optimalQ never reimplements or bundles a simulator - every circuit
> evaluation during optimization calls into mps_xtrap's `MPSSimulator` (or
> `MultiChiRunner`, for Richardson-extrapolated accuracy) and reads back an
> expectation value or a sampled bitstring. mps_xtrap, in turn, never sees
> problem-specific code - it only receives gate circuits.

---

## Install

```bash
pip install optimalQ
```

optimalQ depends on `mps-xtrap>=2.4.0`, which will be installed
automatically from PyPI. If you already have a local `mps_xtrap` wheel or
sdist (any version satisfying `>=2.4.0`), install it first and pip will
detect the requirement is already satisfied:

```bash
pip install /path/to/mps_xtrap-<version>-py3-none-any.whl  # or the .tar.gz sdist
pip install optimalQ
```

---

## Quick start

Every problem class is available through the top-level dispatcher,
`optimalQ.solve(problem_type, **kwargs)`, or directly via
`optimalQ.problems.solve_<name>(...)`.

### Generic combinatorial search

```python
import optimalQ

def cost(bits):
    # minimize: prefer exactly one of bits[0], bits[1] set
    return -(bits[0] + bits[1] - 2 * bits[0] * bits[1])

result = optimalQ.solve(
    "search",
    cost_function=cost,
    n_variables=2,
    bond_dim=32,       # fixed MPS bond dimension
)
print(result.best_bitstring, result.best_value)
```

### Integer factoring

```python
result = optimalQ.solve(
    "factoring",
    N=15,
    p=3,               # QAOA layers - deeper circuits reach lower energies
    bond_dim=32,
    maxiter=150,
)
print(result.extra["p"], result.extra["q"])   # e.g. 3, 5
```

### Extremal eigenvalue estimation

```python
import numpy as np

A = np.array([[2.0, 0.0], [0.0, -1.0]])
result = optimalQ.solve(
    "eigenvalue",
    A=A,
    largest=False,     # smallest eigenvalue
    n_bits=5,
    bond_dim=32,
)
print(result.best_value, result.extra["eigenvector"])
```

### Linear algebra (least squares)

```python
A = np.array([[2.0, 0.0], [0.0, 1.0]])
b = np.array([4.0, 2.0])
result = optimalQ.solve("linalg", A=A, b=b, bond_dim=32)
print(result.extra["x"])
```

### Linear regression

```python
result = optimalQ.solve("regression", X=X, y=y, fit_intercept=True, bond_dim=32)
print(result.extra["coefficients"], result.extra["intercept"], result.extra["r_squared"])
```

---

## Controlling simulation quality: bond dimension and Richardson extrapolation

optimalQ never hardcodes a bond dimension or reinterprets mps_xtrap's
controls - every `solve(...)` call accepts simulation kwargs that are
forwarded to mps_xtrap **unchanged**, so you get the full benefit of
mps_xtrap's bond-dimension control and multilevel Richardson extrapolation.

**Fixed bond dimension** - one simulation per circuit evaluation:

```python
optimalQ.solve("search", cost_function=f, n_variables=6, bond_dim=64)
```

**Richardson-extrapolated simulation** - several bond dimensions per
circuit evaluation (`mps_xtrap.MultiChiRunner` + `mps_xtrap.SweepConfig`),
extrapolated toward the infinite-bond-dimension limit:

```python
optimalQ.solve(
    "search", cost_function=f, n_variables=6,
    base_chi=16, scaling_factor=2, n_levels=3,   # chi = [16, 32, 64]
)
```

Every `QAOAResult` reports `energy_uncertainty` (0.0 in fixed-chi mode; the
Richardson uncertainty estimate in extrapolated mode) and `energy_reliable`
- whether mps_xtrap's own convergence diagnostics passed.

### Extrapolation is an accuracy/cost trade at fixed base_chi, not a speedup

A common (reasonable) intuition is: *"use several small, cheap bond
dimensions instead of one large, expensive one, and let extrapolation make
up the accuracy."* Richardson extrapolation does buy you accuracy this
way - a `[16, 32, 64]` sweep can match or beat a fixed `chi=128` run in
accuracy - but each circuit evaluation still runs the circuit once per
bond dimension in the sweep, so extrapolated mode costs *more* wall-clock
time per evaluation than a single fixed-chi run at the sweep's smallest
`base_chi`, not less.

The real caveat is **`base_chi` still has to be large enough for the
circuit's actual entanglement.** QAOA circuits entangle quickly with
circuit depth (`p`) and problem size (qubit count). If `base_chi` is too
small for a given circuit, the truncation error at each `chi` in the sweep
doesn't yet follow the smooth power-law decay Richardson extrapolation
assumes, extrapolation can *amplify* noise rather than cancel it, and
mps_xtrap will report `is_reliable=False` for the affected observables
(surfaced as `QAOAResult.energy_reliable`). optimalQ logs a heuristic
warning when `base_chi` looks small relative to qubit count, and will
report `energy_reliable=False` if mps_xtrap's own diagnostics fail - treat
either as a signal to raise `base_chi`, not lower it further.

---

## Numerical stability: coefficient normalization

QAOA's cost layer turns every Ising coefficient directly into a gate
rotation angle: `angle = 2 * gamma * coefficient`. Some of optimalQ's
problem encodings can produce Hamiltonians with very large coefficients
- for example, `factoring`'s `(N - p*q)^2` objective grows roughly with
`N^2` and compounds further through quadratization, and `linalg`/
`regression`'s `||Ax - b||^2` objective scales with the squared magnitude
of `A`'s entries. Once coefficients reach into the thousands or beyond,
the resulting gate angles can run into the millions or billions of
radians. At that scale, floating-point evaluation of `sin`/`cos` inside
the gate has no meaningful precision left - the gate that actually gets
applied is effectively arbitrary, independent of bond dimension, `p`, or
any other simulation setting, and circuit simulation can fail outright
(e.g. `numpy.linalg.LinAlgError: SVD did not converge`) or silently return
meaningless results.

**optimalQ handles this automatically.** Every `solve(...)` call
normalizes its Hamiltonian - rescaling `h`/`J` so the largest coefficient
is `1.0` - before building any QAOA circuit, via `IsingModel.normalized()`
under the hood in `QAOAEngine`. This is a pure rescaling: dividing every
coefficient by the same positive constant does not move the Hamiltonian's
minimizer, so the problem being solved is unchanged. `QAOAResult.energy`
and `energy_uncertainty` are always reported back in the original,
un-normalized problem's units - normalization is invisible to callers
except that circuit evaluations no longer break at large problem scales.

```python
# Normalization is on by default - nothing to configure for the common case
result = optimalQ.solve("factoring", N=323, p=1, bond_dim=16)

# Disable it only if you're already normalizing coefficients yourself,
# or deliberately studying the unnormalized numerical behavior
result = optimalQ.solve("factoring", N=15, normalize=False)
```

You can also inspect or perform this rescaling directly:

```python
from optimalQ.encoding import IsingModel

model = IsingModel(n=2, h={0: 1e11, 1: -5e10}, J={(0, 1): 2e10})
print(model.max_abs_coeff())        # 1e11

normalized_model, norm_factor = model.normalized()
print(normalized_model.max_abs_coeff())   # 1.0
print(norm_factor)                        # 1e11
```

**What normalization does *not* fix:** it only addresses the numerical
range of gate angles. It has no effect on how expensive a circuit is to
simulate - qubit count, circuit depth (`p`), and how densely connected
the problem's coefficient graph is (which drives SWAP-chain overhead for
non-adjacent two-qubit gates) are unrelated to coefficient magnitude, and
still determine whether a given problem is practical to simulate at all.
A normalized 150+ qubit, densely-connected Hamiltonian will no longer
crash - but it can still be far too slow to optimize in practice.

---

## Architecture

```
Your problem (search / factoring / eigenvalue / linalg / regression)
        |
        v
optimalQ.problems.*        - problem -> Ising Hamiltonian
        |  (h, J, offset)
        v
optimalQ.encoding          - Ising <-> QUBO, fixed-point binary encoding,
        |                     quadratization of higher-order terms
        v
optimalQ.core.QAOAEngine   - QAOA ansatz construction (Circuit objects),
        |                     scipy.optimize outer loop, result decoding
        |
        |   circuit-in, expectation-value/bitstring-out
        v
mps_xtrap.MPSSimulator / MultiChiRunner   - all quantum circuit simulation
```

- **optimalQ** owns: problem -> Hamiltonian mapping, ansatz construction,
  classical parameter optimization, result decoding. It has no notion of
  matrix product states, bond dimensions, or SVD truncation beyond passing
  a user's chosen simulation config through unmodified.
- **mps_xtrap** owns: everything about actually running a quantum circuit
  - gate application, SVD truncation, GPU acceleration, gate fusion,
  measurement, and Richardson extrapolation. It has no notion of "search
  problem" or "factoring" - it only ever sees `Circuit` objects and
  returns numbers (expectation values, probabilities, sampled counts).

This split means optimalQ's QAOA logic is entirely reusable against any
future mps_xtrap simulation backend, and mps_xtrap's simulation core is
entirely reusable by any future non-QAOA circuit workload.

---

## API Reference

### Top-level dispatcher

```python
optimalQ.solve(problem_type: str, **kwargs) -> QAOAResult
```

`problem_type` is one of `"search"`, `"factoring"`, `"eigenvalue"`,
`"linalg"`, `"regression"`. All problems accept these common kwargs:

| kwarg | default | meaning |
|---|---|---|
| `bond_dim` | 64 | Fixed MPS bond dimension (mutually exclusive with the extrapolation kwargs below) |
| `base_chi` | - | Smallest bond dimension in a Richardson sweep; setting this enables extrapolated mode |
| `scaling_factor` | 2.0 | Growth factor between successive bond dimensions |
| `n_levels` | 3 | Number of bond dimensions in the sweep |
| `alpha` | None | Known truncation-error exponent (estimated from data if omitted) |
| `device` | `'cpu'` | `'cpu'` or `'cuda'`, forwarded to mps_xtrap |
| `fusion` | False | Gate fusion strategy, forwarded to `MPSSimulator` |
| `shots` | 1024 | Measurement shots used for final bitstring decoding |
| `p` | 1 | Number of QAOA layers |
| `optimizer` | `'COBYLA'` | `scipy.optimize.minimize` method |
| `maxiter` | 100-150 | Classical optimizer iteration budget |
| `seed` | None | RNG seed |
| `normalize` | True | Rescale Hamiltonian coefficients (max magnitude -> 1.0) before circuit construction, to keep QAOA gate angles numerically stable regardless of problem scale; see [Numerical stability](#numerical-stability-coefficient-normalization) below. Doesn't change results, only simulation numerics. |

### `optimalQ.core`

- **`SimulationConfig`** - encapsulates the fixed-chi vs. Richardson
  extrapolation choice described above. `.expectation_terms(circuit, h, J)`
  runs a circuit and returns `(energy, uncertainty)`; `.sample_bitstring(...)`
  draws final measurement shots.
- **`build_qaoa_circuit(ising, gammas, betas) -> mps_xtrap.Circuit`** -
  builds a p-layer QAOA ansatz: cost layer (`rz` for linear Ising terms,
  `zz` for quadratic terms) alternating with a transverse-field mixer
  (`rx` on every qubit).
- **`QAOAEngine(ising, sim_config, p, optimizer, maxiter, seed, normalize=True)`**
  - runs the classical optimization loop; `.run() -> QAOAResult`. When
  `normalize` is True (default), internally rescales `ising`'s
  coefficients via `IsingModel.normalized()` before building any circuit,
  and rescales resulting energies back into `ising`'s original units - see
  [Numerical stability](#numerical-stability-coefficient-normalization).
  The `ising` attribute always holds the original, un-normalized model.
- **`QAOAResult`** - `best_bitstring`, `best_value`, `energy`,
  `energy_uncertainty`, `energy_reliable`, `params`, `n_qubits`, `counts`,
  `optimizer_result`, `extra` (problem-specific decoded fields).
  `.summary()` returns a formatted string.

### `optimalQ.encoding`

- **`IsingModel(n, h, J, offset)`** - Ising Hamiltonian container;
  `.energy(spins)`, `.energy_bits(bits)`, `.to_qubo()`,
  `IsingModel.from_qubo(qubo, n, offset)`, `.max_abs_coeff()` (largest
  |h_i| or |J_ij|), `.normalized(target_scale=1.0) -> (IsingModel,
  norm_factor)` (rescaled copy plus the divisor used - see
  [Numerical stability](#numerical-stability-coefficient-normalization)).
- **`FixedPointEncoding(n_bits, lo, hi)`** - encodes one continuous
  variable as an `n_bits`-bit unsigned fixed-point fraction over `[lo,
  hi]`. Default 4-6 bits per variable is the typical resolution/qubit-count
  trade-off; fully user-overridable via `n_bits`.
- **`VectorEncoding(n_vars, n_bits, lo, hi, bounds)`** - encodes a vector
  of continuous variables, each with its own `FixedPointEncoding`, into
  one flat bitstring. Used by `eigenvalue`, `linalg`, and `regression`.
- **`quadratize(terms, n) -> (qubo, n_total)`** - reduces a higher-order
  (cubic+) pseudo-Boolean polynomial to quadratic (QUBO) form via
  Rosenberg's substitution, introducing auxiliary variables as needed.
  Used by `factoring`, whose `(N - p*q)^2` objective is quartic in the raw
  bit variables.
- **`qubo_from_polynomial(poly, n) -> (qubo, offset, n_total)`** -
  convenience wrapper splitting a polynomial's constant term from its
  higher-order part before quadratizing.

### `optimalQ.problems`

- **`solve_search(cost_function=None, n_variables=None, qubo=None, ising=None, ...)`**
  - generic binary combinatorial optimization. Accepts a user cost
  function (fitted to an exact QUBO by enumeration, `n_variables <= 20`),
  precomputed QUBO coefficients, or a precomputed Ising model directly.
- **`solve_factoring(N, n_bits_p=None, n_bits_q=None, ...)`** - integer
  factorization via the multiplication-table polynomial `(N - p*q)^2`,
  quadratized to a QUBO. Returns `extra['p']`, `extra['q']`,
  `extra['product']`.
- **`solve_eigenvalue(A, largest=False, n_bits=5, value_range=(-1,1), penalty_strength=5.0, ...)`**
  - smallest/largest eigenvalue of a real symmetric matrix via a
  penalized, binary-encoded Rayleigh quotient. Returns
  `extra['eigenvector']`.
- **`solve_linalg(A, b, n_bits=5, value_range=(-10,10), ...)`** - least
  squares solve of `Ax = b`. Returns `extra['x']`.
- **`solve_regression(X, y, fit_intercept=True, n_bits=5, value_range=(-10,10), ...)`**
  - ordinary least-squares linear regression. Returns
  `extra['coefficients']`, `extra['intercept']`, `extra['r_squared']`.

---

## A note on QAOA optimization quality

QAOA is a variational algorithm: with a shallow circuit (`p=1` or `p=2`)
and a bounded classical optimizer budget, it is not guaranteed to find the
true optimum on every run, and COBYLA's result can vary meaningfully
across random seeds. If a result looks suboptimal, try increasing `p`,
raising `maxiter`, or running with a few different `seed` values - this is
standard QAOA behavior, not specific to optimalQ's implementation.

---

## Licensing

optimalQ is licensed under the **Business Source License 1.1** - see
[`LICENSE`](./LICENSE). Non-commercial use is free; commercial use
requires a separate license from OscInspire (contact
[oscinspire@gmail.com](mailto:oscinspire@gmail.com)). On 2029-01-01 the
license converts to plain MIT.

optimalQ depends on **[mps_xtrap](https://pypi.org/project/mps-xtrap/)**,
also by OscInspire, as its quantum circuit simulation backend, licensed
independently under its own BUSL-1.1 terms (same non-commercial-free /
commercial-by-arrangement structure).
