Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,31 @@
# Changelog

## solaris-1.1.0 - 2026-08-23

The pluggable quantum layer. Developers of quantum algorithms routinely run
their code on classical simulators while hardware is scarce - that is what
libraries like QuTiP and Qiskit Aer are for. Solaris 1.0.0 carried its own
such simulation (the amplitude table inside each shared contract); this
release refactors it behind a ports-and-adapters interface so the ledger no
longer cares whose mathematics holds the wave.

- `labs/engine/`: the WaveEngine port (prepare / apply1 / cnot /
branch_weight / project - deliberately no `measure()`: the hash draw and
the double-entry settlement decision stay on the ledger side) with four
adapters: `table` (the original arithmetic, DEFAULT, stdlib-only),
`numpy`, `qutip`, `qiskit` (Aer statevector), plus a `broken` negative
control for the harness.
- `labs/qm_battery.py --engine <name>` (or `SOLARIS_ENGINE`): the nine-check
QM battery, engine-parameterized. All four adapters reproduce the frozen
reference `labs/qm_tests.py` byte-identically (hash-fed deterministic
draws make the comparison exact); the broken control fails 4/9 checks and
its CHSH drops to ~1.9, below even the classical bound - the |a| law is
not almost-quantum, it is worse than classical.
- Every physical mechanism is unchanged from implementation 0; the full
150-row battery was re-sat for this version (`tests/results/solaris-1.1.0.md`,
same 147 pass / 3 pre-registered red).
- Optional backends install: `pip install -r requirements-engines.txt`.

## Unreleased

Repository restructure (2026-08-21): `model/` added (version card + the three
Expand Down
4 changes: 2 additions & 2 deletions CITATION.cff
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ authors:
- family-names: Masteika
given-names: Marijus
orcid: "https://orcid.org/0009-0004-2464-3008"
version: solaris-1.0.0
date-released: 2026-08-20
version: solaris-1.1.0
date-released: 2026-08-23
license: MIT
repository-code: "https://github.com/questforentropy/iceberg-model"
doi: 10.5281/zenodo.22046196
Expand Down
21 changes: 20 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ truth for the version triplet, with the three layers described beside it:

```
pip install -r requirements.txt
python labs/run_exams.py all --record solaris-1.0.0 --expect-red GR-34,GR-35,GR-36
python labs/run_exams.py all --record solaris-1.1.0 --expect-red GR-34,GR-35,GR-36
```

Expected: **150 rows - 147 pass and exactly 3 red.** The red rows (GR-34..36)
Expand All @@ -36,6 +36,25 @@ never softened. Version-independent row definitions live in `tests/definitions/`
the generated record - status, declared criterion, and measured detail per row -
in `tests/results/`. Individual sets run by name: `python labs/run_exams.py QM GR`.

### Swap the quantum engine (Solaris 1.1.0)

The amplitude arithmetic sits behind a small port (`labs/engine/`), so the wave
storage is swappable. The nine-check QM battery runs on any of four backends and
must reproduce the frozen reference `labs/qm_tests.py` byte-for-byte:

```
python labs/qm_battery.py # default: table (stdlib-only)
pip install -r requirements-engines.txt # then:
python labs/qm_battery.py --engine numpy
python labs/qm_battery.py --engine qutip
python labs/qm_battery.py --engine qiskit # Aer statevector
```

Draws are hash-fed and deterministic, so conformance is exact - identical output,
not statistically similar. `--engine broken` runs the negative control (weights
|a| instead of |a|²): it fails 4 of 9 checks, demonstrating the battery detects a
wrong engine.

All numbers - like every experiment behind them - were produced with AI
assistance and are under continuing verification and validation; the
one-command reproduction exists precisely so any reader can check any number
Expand Down
37 changes: 37 additions & 0 deletions labs/engine/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# Solaris 1.1.0 - the pluggable quantum layer (ports & adapters).
#
# The ledger (domain.py Contract: hash draws + settlement decisions) talks to the wave
# storage only through the WaveEngine port (port.py). Adapters are selected here, lazily:
# a missing library only disables its own name, never the package.
#
# Certified: all four engines
# reproduce the Solaris 1.0.0 qm_tests.py battery BYTE-IDENTICALLY (see CHANGELOG 1.1.0); the "broken" negative
# control (|a| instead of |a|^2) is caught by the same differential.
#
# Selection: qm_battery.py --engine <name>, or the SOLARIS_ENGINE env var. Default: table
# (our reference implementation - stdlib-only, the semantics every other adapter must match).

import os

DEFAULT_ENGINE = "table"
ENGINE_NAMES = ("table", "numpy", "qutip", "qiskit", "broken")


def select_engine(name=None):
name = name or os.environ.get("SOLARIS_ENGINE") or DEFAULT_ENGINE
if name == "table":
from .table import TableEngine
return TableEngine()
if name == "numpy":
from .numpy_sv import NumpyEngine
return NumpyEngine()
if name == "qutip":
from .qutip_qobj import QutipEngine
return QutipEngine()
if name == "qiskit":
from .qiskit_sv import QiskitEngine
return QiskitEngine()
if name == "broken": # negative control - harness self-test only, never physics
from .broken import BrokenEngine
return BrokenEngine()
raise ValueError("unknown engine %r (expected one of %s)" % (name, ", ".join(ENGINE_NAMES)))
17 changes: 17 additions & 0 deletions labs/engine/broken.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# Solaris 1.1.0 - BrokenEngine: negative control adapter (harness self-test only).
# When plugged in, the battery fails 4/9 checks and CHSH falls to ~1.9 - below even the classical bound.
#
# TableEngine with exactly one defect: branch_weight returns p1 ** 0.5 instead of p1
# (the "1-signer" law, known wrong under the double-entry / |alpha|**2 ruling). project()
# is inherited unchanged - it recomputes the TRUE p1 from ref.amps itself (not the broken
# value handed to the domain), so the state stays a valid normalized vector; only the
# accept/reject THRESHOLD the domain compares its draw against is wrong. This isolates the
# defect to a single method.

from .table import TableEngine


class BrokenEngine(TableEngine):
def branch_weight(self, ref, q):
p1 = super().branch_weight(ref, q)
return p1 ** 0.5
71 changes: 71 additions & 0 deletions labs/engine/domain.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
# Solaris 1.1.0 - domain.py: Contract (seed + draw counter, hash-fed measurement decisions)
# Verified byte-identical to the reference battery (see CHANGELOG 1.1.0).
#
# Contract mirrors QContract's public method surface (labs/qm_tests.py,
# repo root, READ-ONLY) exactly, but never touches amplitudes directly - every state read or
# mutation goes through the five WaveEngine port methods on self.engine. H, u01, and the gate
# matrices are IMPORTED (read-only) from the original module, never copied.
#
# measure_z/measure_angle/measure_pauli reproduce QContract's operation order verbatim:
# weight <- port.branch_weight, bit <- domain draw (u01(H(seed,"draw",n))), project <-
# port.project, (un)rotations via port.apply1 in the same order as the original methods.
# Draw counter semantics: one draw per z-measurement, counter starts at 0, incremented
# BEFORE use (self.draws += 1 then hash) - matches QContract._draw exactly.

import sys
import math
from pathlib import Path

_QM_TESTS_DIR = Path(__file__).resolve().parents[1]
if not (_QM_TESTS_DIR / "qm_tests.py").exists():
raise RuntimeError("qm_tests.py not found at %s - repo layout assumption broken" % _QM_TESTS_DIR)
sys.path.insert(0, str(_QM_TESTS_DIR))
import qm_tests # noqa: E402 (READ-ONLY import, never modified)
from qm_tests import H, u01, RY, RZ, HAD, S_GATE, SDG # noqa: E402,F401


class Contract:
"""Domain object: seed + draw counter + a bound (engine, ref) pair.
No amplitude arithmetic lives here - every state operation is a port call."""

def __init__(self, seed, engine, ref):
self.seed = seed
self.engine = engine
self.ref = ref
self.draws = 0

def _draw(self):
self.draws += 1
return u01(H(self.seed, "draw", self.draws))

def apply1(self, U, q):
self.engine.apply1(self.ref, U, q)

def cnot(self, c, t):
self.engine.cnot(self.ref, c, t)

def measure_z(self, q):
# GLOBAL UPDATE (via port): weight read, hash draw, projection - same order as
# QContract.measure_z, split across the port boundary.
p1 = self.engine.branch_weight(self.ref, q)
bit = 1 if self._draw() < p1 else 0
self.engine.project(self.ref, q, bit)
return -1 if bit else 1

def measure_angle(self, q, theta):
self.apply1(RY(-theta), q)
r = self.measure_z(q)
self.apply1(RY(theta), q)
return r

def measure_pauli(self, q, which):
if which == "Z":
return self.measure_z(q)
if which == "X":
return self.measure_angle(q, math.pi / 2)
self.apply1(SDG, q)
self.apply1(HAD, q)
r = self.measure_z(q)
self.apply1(HAD, q)
self.apply1(S_GATE, q)
return r
67 changes: 67 additions & 0 deletions labs/engine/numpy_sv.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
# Solaris 1.1.0 - NumpyEngine: NumPy statevector adapter.
#
# complex128 ndarray state, vectorized apply1/cnot/branch_weight/project. NO numpy.random
# anywhere - the only entropy in this lab is the domain's u01(H(seed,"draw",n)), never
# touched by this file. Index sets are built once per call from Python range() (deterministic,
# not RNG) so the branch masks are constructed in the SAME index order as TableEngine's
# generator expressions - kept for the closest possible float agreement, not required for
# correctness (the conformance bar is byte-identical battery stdout; a near-threshold
# fence covers float-order edge cases).

import math
import numpy as np


class NpStateRef:
__slots__ = ("amps",)

def __init__(self, amps):
self.amps = np.array([complex(a) for a in amps], dtype=np.complex128)


class NumpyEngine:
def prepare(self, amps):
return NpStateRef(amps)

def apply1(self, ref, U, q):
step = 1 << q
a = ref.amps
n = len(a)
idx0 = np.array([i for i in range(n) if not (i & step)], dtype=np.int64)
idx1 = idx0 | step
a0 = a[idx0].copy()
a1 = a[idx1].copy()
a[idx0] = U[0][0] * a0 + U[0][1] * a1
a[idx1] = U[1][0] * a0 + U[1][1] * a1

def cnot(self, ref, c, t):
cs, ts = 1 << c, 1 << t
a = ref.amps
n = len(a)
idx = np.array([i for i in range(n) if (i & cs) and not (i & ts)], dtype=np.int64)
jdx = idx | ts
old_i = a[idx].copy()
old_j = a[jdx].copy()
a[idx] = old_j
a[jdx] = old_i

def _mask(self, ref, q):
step = 1 << q
n = len(ref.amps)
return np.array([bool(i & step) for i in range(n)], dtype=bool)

def branch_weight(self, ref, q):
mask = self._mask(ref, q)
return float(np.sum(np.abs(ref.amps[mask]) ** 2))

def project(self, ref, q, bit):
mask = self._mask(ref, q)
p1 = float(np.sum(np.abs(ref.amps[mask]) ** 2))
norm = math.sqrt(p1 if bit else 1 - p1)
keep = mask if bit else ~mask
new = np.where(keep, ref.amps / norm, 0j).astype(np.complex128)
ref.amps = new

def overlap(self, ref, amps):
b = np.array([complex(x) for x in amps], dtype=np.complex128)
return complex(np.vdot(ref.amps, b))
45 changes: 45 additions & 0 deletions labs/engine/port.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# Solaris 1.1.0 - The WaveEngine Port (interface only, no logic).
#
# WaveEngine: holds the wave: answers SMALL questions; never draws, never decides.
# Concrete adapters (table_engine.py, broken_engine.py, numpy_engine.py) implement this
# structurally (duck typing via typing.Protocol) - no inheritance required.
#
# Five port methods used by the domain/battery: prepare, apply1, cnot, branch_weight, project.
# `overlap` exists for probe-stage diagnostics only (never called from domain.py or the
# battery's measurement path).
#
# Deliberately absent: measure() (would re-import Born sampling), get_amplitudes() /
# branch iteration (the "hand me your entire contents" question the design note forbids).

from typing import Protocol, Sequence, Any, runtime_checkable

StateRef = Any # opaque per-adapter handle; callers never introspect its attributes


@runtime_checkable
class WaveEngine(Protocol):
"""Port. Structural interface - adapters need not inherit this class."""

def prepare(self, amps: Sequence[complex]) -> StateRef:
"""Contract genesis from an explicit branch-amplitude list."""
...

def apply1(self, ref: StateRef, U, q: int) -> None:
"""Apply a single-qubit unitary U to qubit q of ref, in place."""
...

def cnot(self, ref: StateRef, c: int, t: int) -> None:
"""Apply CNOT(control=c, target=t) to ref, in place."""
...

def branch_weight(self, ref: StateRef, q: int) -> float:
"""|branch|**2 for qubit q's '1' branch. A norm read. NO sampling."""
...

def project(self, ref: StateRef, q: int, bit: int) -> None:
"""Keep the chosen branch, renormalize. The caller (domain) already drew."""
...

def overlap(self, ref: StateRef, amps: Sequence[complex]) -> complex:
"""<ref|amps> - diagnostics only (probe / sanity checks), never domain logic."""
...
Loading
Loading