Source code for quantum_mind.circuits.builders

"""Qiskit circuits for the quantum-like model families.

Every builder returns ``(circuit, decode)``, where ``decode(counts)`` turns measurement counts into the
same probability vectors that the analytic model predicts, so circuit and model can be compared
directly.

**Forms.** ``'dynamic'`` circuits use mid-circuit measurement and reset (IBM hardware, Aer).
``'deferred'`` circuits copy each measurement to a fresh ancilla and read everything at the end (the
deferred-measurement principle); they run on hardware without mid-circuit measurement (for example ion
traps on Amazon Braket) and on OpenQASM 3 simulators.

**Binary projective measurement.** To ask "is the state in the subspace of projector P (rank r)?", a
unitary V maps that subspace onto the first r computational basis states, an ancilla is flipped for
each of those basis states (multi-controlled X), only the ancilla is measured, and V is undone. The
system is never measured directly, so the post-measurement state follows the Lüders rule.

Examples
--------
>>> from quantum_mind.families.order_effects import QuantumOrderModel
>>> from quantum_mind.circuits import order_effects_circuit, run
>>> model = QuantumOrderModel(a=0.8, b=1.2, g=0.3)
>>> qc, decode = order_effects_circuit(model, 'AB')            # doctest: +SKIP
>>> decode(run(qc, 'aer', shots=20000)).round(2)               # doctest: +SKIP
"""
from __future__ import annotations

import itertools
import numpy as np
from .._optional import require
require('qiskit')
from qiskit import QuantumCircuit, QuantumRegister, ClassicalRegister
from qiskit.circuit.library import UnitaryGate, StatePreparation, MCXGate

__all__ = ['subspace_unitary', 'projective_sequence_circuit', 'order_effects_circuit', 'qlbn_circuit', 'walk_circuit',
           'belief_circuit', 'n_qubits']


[docs] def n_qubits(d): r"""Number of qubits needed for a dimension. Parameters ---------- d : int Hilbert-space dimension. Returns ------- int :math:`\lceil \log_2 d \rceil`, at least 1. """ return max(1, int(np.ceil(np.log2(d))))
def _pad(v, n): """Pad a state vector with zeros to length 2^n.""" out = np.zeros(2 ** n, complex); out[:len(v)] = v; return out
[docs] def subspace_unitary(P, n): """Unitary that maps the range of a projector onto the first basis states. Parameters ---------- P : numpy.ndarray ``d x d`` projector with ``d <= 2**n``. n : int Number of qubits; padding dimensions are completed to a full unitary. Returns ------- V : numpy.ndarray ``2**n x 2**n`` unitary whose first r rows are an orthonormal basis (conjugated) of range(P), so it maps range(P) onto span(``|0>``, ..., ``|r-1>``). r : int Rank of P. """ d = P.shape[0]; D = 2 ** n w, V = np.linalg.eigh((P + P.conj().T) / 2) B = V[:, w > 0.5] # columns: basis of the subspace (d x r) r = B.shape[1] M = np.zeros((D, D), complex); M[:d, :r] = B rng = np.random.default_rng(12345) rest = rng.normal(size=(D, D - r)) + 1j * rng.normal(size=(D, D - r)) Q, _ = np.linalg.qr(np.hstack([M[:, :r], rest])) Q[:, :r] = M[:, :r] # keep the exact basis (QR may change phases) # re-orthonormalise the completion against the exact basis for k in range(r, D): v = Q[:, k] - Q[:, :k] @ (Q[:, :k].conj().T @ Q[:, k]); Q[:, k] = v / np.linalg.norm(v) return Q.conj().T, r
def _flag(qc, sysq, anc, r): """Flip the ancilla for each of the first r basis states of the system register.""" for k in range(r): qc.append(MCXGate(len(sysq), ctrl_state=k), list(sysq) + [anc])
[docs] def projective_sequence_circuit(psi, projectors, order, form='dynamic'): """Circuit for a sequence of yes/no questions. Parameters ---------- psi : array_like State of length d (normalised here). projectors : dict ``{name: d x d "yes" projector}``. order : sequence of str Questions in the order asked. form : {'dynamic', 'deferred'}, optional Mid-circuit measurement, or deferred measurement on fresh ancillas. Returns ------- circuit : qiskit.QuantumCircuit decode : callable ``decode(counts)`` -> ``{answer tuple: probability}`` with 1 = yes, in the order asked. """ psi = np.asarray(psi, complex); psi = psi / np.linalg.norm(psi) n = n_qubits(len(psi)) s = QuantumRegister(n, 'sys'); m = len(order) a = QuantumRegister(1 if form == 'dynamic' else m, 'anc'); c = ClassicalRegister(m, 'ans') qc = QuantumCircuit(s, a, c) qc.append(StatePreparation(_pad(psi, n)), s) for i, q in enumerate(order): V, r = subspace_unitary(projectors[q], n) anc = a[0] if form == 'dynamic' else a[i] qc.append(UnitaryGate(V, label='V_%s' % q), s) _flag(qc, s, anc, r) qc.append(UnitaryGate(V.conj().T, label='V_%s^+' % q), s) if form == 'dynamic': qc.measure(anc, c[i]); qc.reset(anc) if form == 'deferred': for i in range(m): qc.measure(a[i], c[i]) def decode(counts): tot = sum(counts.values()); out = {k: 0.0 for k in itertools.product((1, 0), repeat=m)} for bits, k in counts.items(): b = bits.replace(' ', '')[::-1] # Qiskit prints the highest classical bit first out[tuple(int(b[i]) for i in range(m))] += k / tot return out return qc, decode
def _order_cells(probs): """Answer-sequence probabilities as [yy, yn, ny, nn].""" return np.array([probs[(1, 1)], probs[(1, 0)], probs[(0, 1)], probs[(0, 0)]])
[docs] def order_effects_circuit(model, order='AB', form='dynamic'): """Circuit for a question-order model. Parameters ---------- model : QuantumOrderModel, QuantumOrderModel4D or ProjectiveQuestionModel The model. order : {'AB', 'BA'}, optional Question order. form : {'dynamic', 'deferred'}, optional Circuit form. Returns ------- circuit : qiskit.QuantumCircuit decode : callable ``decode(counts)`` -> ``[yy, yn, ny, nn]``. """ pm = model.as_projective() if hasattr(model, 'as_projective') else model qc, dec = projective_sequence_circuit(pm.psi, pm.projectors, tuple(order), form) return qc, (lambda counts: _order_cells(dec(counts)))
[docs] def qlbn_circuit(net, query, evidence=None, phases=None): r"""Quantum-like Bayesian network inference as a circuit. The amplitudes :math:`\sqrt{P(v, e, h)}\,e^{i\theta_h}` are loaded with ``StatePreparation``; the hidden register goes through Hadamard gates and is post-selected on 0, which sums the amplitudes over the hidden configurations. Parameters ---------- net : BayesNet query : str evidence : dict, optional phases : array_like, optional Returns ------- circuit : qiskit.QuantumCircuit decode : callable ``decode(counts)`` -> distribution of the query. """ from ..families.qlbn.models import _hidden evidence = evidence or {} vals = net.variables[query]; hid = _hidden(net, query, evidence) configs = list(net.configurations(hid)); H = len(configs) phases = np.zeros(H) if phases is None else np.asarray(phases, float) nq, nh = n_qubits(len(vals)), (n_qubits(H) if H > 1 else 0) amps = np.zeros(2 ** (nq + nh), complex) for iv, v in enumerate(vals): for ih, (h, th) in enumerate(zip(configs, phases)): amps[iv + (ih << nq)] = np.sqrt(net.joint({**evidence, **h, query: v})) * np.exp(1j * th) amps /= np.linalg.norm(amps) qc = QuantumCircuit(nq + nh, nq + nh) qc.append(StatePreparation(amps), range(nq + nh)) for k in range(nh): qc.h(nq + k) qc.measure(range(nq + nh), range(nq + nh)) def decode(counts): acc = np.zeros(len(vals)) for bits, k in counts.items(): x = int(bits.replace(' ', ''), 2) if x >> nq == 0 and (x & (2 ** nq - 1)) < len(vals): acc[x & (2 ** nq - 1)] += k return acc / acc.sum() if acc.sum() else acc return qc, decode
[docs] def walk_circuit(model, t): """Quantum-walk circuit at time t. State preparation, :math:`U(t) = e^{-iHt}` as a unitary gate on the padded space, and measurement of every qubit. Parameters ---------- model : QuantumWalk t : float Returns ------- circuit : qiskit.QuantumCircuit decode : callable ``decode(counts)`` -> distribution over the model's response categories. """ from scipy.linalg import expm N = model.N; n = n_qubits(N); D = 2 ** n Hp = np.zeros((D, D), complex); Hp[:N, :N] = model.H qc = QuantumCircuit(n, n) qc.append(StatePreparation(_pad(model.psi0 / np.linalg.norm(model.psi0), n)), range(n)) qc.append(UnitaryGate(expm(-1j * Hp * t), label='U(t)'), range(n)) qc.measure(range(n), range(n)) def decode(counts): p = np.zeros(N); tot = sum(counts.values()) for bits, k in counts.items(): x = int(bits.replace(' ', ''), 2) if x < N: p[x] += k / tot return np.array([p[b].sum() for b in model.bins]) return qc, decode
[docs] def belief_circuit(model, events, queries, form='dynamic'): """Circuit for an :class:`~quantum_mind.families.dynamics.OpenSystemBelief` over a sequence of events. Dephasing of strength gamma is applied by an ancilla that triggers Z with probability gamma / 2. Parameters ---------- model : OpenSystemBelief events : sequence of int queries : sequence of int form : {'dynamic', 'deferred'}, optional Returns ------- circuit : qiskit.QuantumCircuit decode : callable ``decode(counts)`` -> P(yes) at the last query. """ queries = list(queries); nq = len(queries) s = QuantumRegister(1, 'sys') if form == 'dynamic': anc = QuantumRegister(1, 'deph'); qa = None else: anc = QuantumRegister(len(events), 'deph'); qa = QuantumRegister(nq, 'query') c = ClassicalRegister(nq, 'ans') qc = QuantumCircuit(s, anc, c) if qa is None else QuantumCircuit(s, anc, qa, c) qc.ry(model.phi0, s[0]) theta = 2 * np.arcsin(np.sqrt(model.gamma / 2)); k = 0 for t, e in enumerate(events): qc.ry(-model.a_pos if e else model.a_neg, s[0]) a = anc[0] if form == 'dynamic' else anc[t] if model.gamma > 0: qc.ry(theta, a); qc.cz(a, s[0]) if form == 'dynamic': qc.reset(a) if t in queries: if form == 'dynamic': qc.measure(s[0], c[k]) else: qc.cx(s[0], qa[k]) k += 1 if form == 'deferred': for j in range(nq): qc.measure(qa[j], c[j]) def decode(counts): tot = sum(counts.values()) return sum(v for b, v in counts.items() if b.replace(' ', '')[0] == '0') / tot # |0> = yes return qc, decode