occupation_encoding¶
- occupation_encoding(op, spec, num_qubits=None)¶
Map a
BosonOperatorto aSparseObservable.- Parameters:
op – the bosonic operator to map.
spec – the
LevelSpecwith the cutoff of every mode and the encoding that lays the occupation levels out on qubits. There is deliberately no default cutoff; see Truncation below. See Choosing an encoding for the encoding.num_qubits – the number of qubits of the result. This may exceed what the modes need, in which case the surplus qubits are idle and sit at the end. If omitted (
None), the result has exactly as many qubits as the modes need.
- Returns:
The mapped qubit operator. Duplicate terms are merged as the result is assembled, with a tolerance of \(10^{-15}\) relative to the largest coefficient, so the result is typically already simplified. It is not guaranteed to be in any particular order, nor to be free of terms below that tolerance. Call
simplify()if you need the canonical form. This does not affect the operator that the result represents.- Raises:
ValueError – if
spechas one cutoff per mode andopacts on a mode beyond the last, or ifnum_qubitsis smaller than the register the modes need.
Definition
This is the mapper for the occupation (Fock) basis: level \(n\) of a mode is the state with \(n\) bosons. Each bosonic mode is truncated to \(d = n_\text{max} + 1\) occupation levels, where \(n_\text{max}\) is its cutoff in
spec, and laid out on consecutive qubits. Mode \(m\) starts at the offset the spec assigns it, which is the number of qubits of the modes before it, so modes with different cutoffs may occupy a different number of qubits. A spec with a single shared cutoff is applied to themin_num_modes()modes of the operator; a spec with one cutoff per mode keeps all of its modes. How the \(d\) levels are placed on a mode’s qubits is the choice of theLevelEncoding.Under the unary encodings the ladder operator takes the form
\[b^\dagger = \sum_{n=0}^{d-2} \sqrt{n+1} \, \lvert 1 \rangle\langle 0 \rvert_{n+1} \otimes \lvert 0 \rangle\langle 1 \rvert_{n} \, ,\]each term moving the single excitation up one level. The two unary encodings differ in what happens on the other qubits of the mode. With
LevelEncoding.UNARY_UNPROJECTEDthey are left alone, so each term has exactly two \(X\)/\(Y\) factors, on the qubits of levels \(n\) and \(n+1\). WithLevelEncoding.UNARY_PROJECTEDthe mapped operator also keeps the one-hot constraint, by putting a projector \(\lvert 0 \rangle\langle 0 \rvert\) on each of the other qubits of the mode. A term then touches all \(d\) qubits of the mode, although only two of its factors are not diagonal, and it costs more to synthesize. The two are equal on the one-hot subspace.Truncation
A bosonic mode has an infinite-dimensional Fock space, so it must be truncated before it fits on qubits. Truncation is lossy in a specific, quantifiable way: writing \(\bar{b}\) for the truncated operator,
\[[\bar{b}, \bar{b}^\dagger] = 1 - d \, \lvert d-1 \rangle\langle d-1 \rvert \, ,\]that is, the canonical commutator holds on every level except the highest. [1] A consequence worth spelling out is that the span of \(\{\bar{b}^\dagger, \bar{b}\}\) is not closed under the commutator, from which Somma et al. conclude that no direct mapping of the bosonic algebra onto the spin-1/2 algebra exists: a bosonic encoding maps states (and operators acting within the truncated subspace), not an algebra.
Warning
Mapping does not commute with normal ordering. For an operator whose normal ordering requires a contraction,
occupation_encoding(op, ...)andoccupation_encoding(op.normal_ordered(), ...)are different qubit operators, differing by exactly the \(d \lvert d-1 \rangle\langle d-1 \rvert\) defect above. For example \(b b^\dagger\) truncates to a top diagonal entry of \(0\) (\(b^\dagger\) sends the top level out of the retained space, so there is nothing left for \(b\) to act on), whereas the algebraically equal \(b^\dagger b + 1\) gives \(d\). The two agree on every other level.This is inherent, not an implementation artifact: normal ordering is an exact identity in the untruncated Fock space, so truncating before versus after applying it gives different operators. Decide once whether your Hamiltonian is defined in normal-ordered form and stay consistent.
Warning
The cutoff of every mode must bound the occupation reachable in that mode by the operator being simulated, not merely that of the initial state. A two-mode beamsplitter acting on \(\lvert 1, 1 \rangle\) populates \(\lvert 2, 0 \rangle\), so a cutoff of 1 is insufficient for that state even though neither mode starts above occupation 1.
BosonOperator.mode_occupations()reports how far a single application of the operator can raise each mode, andLevelSpec.truncation()compares that with the cutoffs of a spec. Because it is a rise, the cutoff of a mode needs to be at least its initial occupation plus that bound, and even then it is a lower bound and not a guarantee. The beamsplitter above reports a bound of one in each mode, yet needs a cutoff of two for \(\lvert 1, 1 \rangle\).Choosing an encoding
There is no universally best
LevelEncoding, so all of them are offered on an equal footing; the trade-offs are described in its documentation. For bosonic modes, the unary encodings give ladder terms with only two non-diagonal factors, at a cost of \(d\) qubits per mode, while the two compact encodings need only \(\lceil \log_2 d \rceil\) qubits but produce terms with more non-diagonal factors. Of the two unary encodings, the unprojected one is the cheaper to synthesize and is undefined outside the one-hot subspace; the projected one keeps a projector on every idle qubit.Usage
The number operator \(n_0 = b^\dagger_0 b_0\) is diagonal, so it maps to a weighted sum of occupation projectors:
>>> from qiskit_bosons.mappers import LevelSpec >>> from qiskit_bosons.mappers.library import occupation_encoding >>> from qiskit_bosons.operators import BosonOperator, ann, cre >>> n0 = BosonOperator.from_dict({(cre(0), ann(0)): 1.0}) >>> obs = occupation_encoding(n0, LevelSpec(2, "unary_projected")) >>> obs.num_qubits 3 >>> terms = sorted(obs.to_sparse_list()) >>> [(label, round(coeff.real, 6)) for label, _, coeff in terms] [('001', 2.0), ('010', 1.0)]
Under the unary encodings, level \(n\) of the mode is qubit \(n\). In a label,
1is the projector \(\lvert 1 \rangle\langle 1 \rvert\) and0is \(\lvert 0 \rangle\langle 0 \rvert\), so the first term is \(2\) times the projector onto level 2 and the second is the projector onto level 1, each with \(\lvert 0 \rangle\langle 0 \rvert\) on the other qubits.The compact encodings place the same operator on fewer qubits, and the encoding may be given by its name or as a
LevelEncoding:>>> from qiskit_bosons.mappers import LevelEncoding >>> gray = occupation_encoding(n0, LevelSpec(3, LevelEncoding.GRAY)) >>> gray.num_qubits 2 >>> occupation_encoding(n0, LevelSpec(3, "gray")) == gray True
Modes may have different cutoffs, and so occupy a different number of qubits. Here mode 0 keeps five levels (three qubits) and mode 1 keeps three (two qubits), and a wider register leaves the surplus qubits idle:
>>> n01 = BosonOperator.from_dict({(cre(0), ann(0)): 1.0, (cre(1), ann(1)): 1.0}) >>> spec = LevelSpec([4, 2], "gray") >>> occupation_encoding(n01, spec).num_qubits 5 >>> occupation_encoding(n01, spec, num_qubits=8).num_qubits 8