Trees and Fenwick Trees
Why trees?
The Z-chain in Jordan-Wigner grows linearly because it uses a linear data structure. The key insight behind better encodings: use a tree to share parity information, cutting depth to $O(\log n)$.
Fenwick Trees (Binary Indexed Trees)
The Bravyi-Kitaev encoding is built on a Fenwick tree. FockMap provides a purely functional, immutable implementation:
open System.Numerics
open Encodings
open Encodings.TreeEncoding
open Encodings.MajoranaEncoding
open Encodings.JordanWigner
open Encodings.BravyiKitaev
open Encodings.Hamiltonian
let occupations = [| 1; 0; 1; 1; 0; 1; 0; 1 |]
let tree = FenwickTree.ofArray (^^^) 0 occupations
// Prefix query: XOR of elements 0..3
FenwickTree.prefixQuery tree 3
// Point query: just element 5
FenwickTree.pointQuery tree 5
// Immutable update (returns a new tree):
let tree' = FenwickTree.update tree 2 0
The Fenwick structure defines the BK index sets:
FenwickTree.updateSet 8 3 // U(3) — which qubits to update
FenwickTree.paritySet 3 // P(3) — parity contributors
FenwickTree.occupationSet 3 // Occ(3) — occupation encoding
FenwickTree.remainderSet 3 // R(3) = P(3) \ Occ(3)
Encoding trees: choose your shape
FockMap generalises beyond Fenwick trees to arbitrary ternary tree shapes (the path-based encoder supports at most three children per node):
let linear = linearTree 8 // Chain → recovers Jordan-Wigner
let binary = balancedBinaryTree 8 // Balanced binary → O(log₂ n)
let ternary = balancedTernaryTree 8 // Balanced ternary → O(log₃ n)
let vlasov = vlasovTree 8 // Complete ternary (Vlasov) → O(log₃ n)
Walk any tree:
let binTree = balancedBinaryTree 8
treeAncestors binTree 5 // path from node 5 toward root
treeDescendants binTree 1 // all descendants
treeChildren binTree 1 // direct children only
Two frameworks for tree-based encoding
Framework 1 — Index sets (rooted star trees only):
⚠️ The generic
treeEncodingSchemeconstruction is CAR-valid only for rooted star trees. A census over all rooted labelled trees finds exactlynof then^(n−1)trees satisfy the anticommutation relations forn = 3..6— precisely the stars. Fenwick, chain, and balanced binary/ternary trees do not satisfy CAR under this construction. The call below runs and returns Pauli strings, but forbalancedBinaryTree 8(a non-star) those operators are not a valid encoding — it is shown only to illustrate the API surface. For valid tree encodings use Framework 2 (path-based) below, and for JW/BK/Parity usejordanWignerTerms/bravyiKitaevTerms/parityTerms.
// Valid index-set encoding: use one of the canonical schemes. For a general
// tree topology, prefer the path-based Framework 2 below.
let scheme = jordanWignerScheme
encodeOperator scheme Raise 2u 8u
Framework 2 — Path-based ternary tree (any ternary tree):
Constructs Pauli strings directly from root-to-leg paths using X/Y/Z link labels. This is the approach from Jiang et al. and the Bonsai paper:
let terTree = balancedTernaryTree 8
let links = computeLinks terTree // assign X/Y/Z labels
let legs = allLegs links // enumerate all legs
let pairs = pairLegs terTree links // pair legs per mode
// Full encoding in one call:
let result = encodeWithTernaryTree terTree Raise 2u 8u
The Vlasov tree: a different ternary shape
The balanced ternary tree (balancedTernaryTree) uses midpoint-split
indexing: the root is in the middle, and children are spread across
three roughly equal partitions. The Vlasov tree (vlasovTree) uses
level-order (breadth-first) indexing instead: node 0 is the root,
and children of node $j$ are $3j+1, 3j+2, 3j+3$.
This is based on Vlasov’s Clifford-algebraic construction (arXiv:1904.09912), where Clifford algebra generators are defined recursively on a complete ternary tree.
Both achieve $O(\log_3 n)$ weight, but they distribute weight differently across modes:
let n = 8u
printfn "%-5s %-8s %-8s" "Mode" "TerTree" "Vlasov"
for j in 0u .. n-1u do
let weight (encode : EncoderFn) =
let terms = (encode Raise j n).DistributeCoefficient
terms.SummandTerms
|> Array.map (fun t ->
t.Signature |> Seq.filter (fun c -> c <> 'I') |> Seq.length)
|> Array.max
printfn "%-5d %-8d %-8d" j (weight ternaryTreeTerms) (weight vlasovTreeTerms)
Output:
Mode TerTree Vlasov
0 3 3
1 3 3
2 3 3
3 3 2 ← Vlasov wins here
4 2 3 ← Balanced ternary wins here
5 3 3
6 3 3
7 3 3
The tree shape matters: different qubit assignments put different modes at different depths, so the “best” tree depends on which modes appear most frequently in your Hamiltonian.
Comparing tree shapes on H₂
Both ternary tree shapes produce valid H₂ Hamiltonians with identical eigenspectra but different Pauli structure:
// A minimal H₂/STO-3G coefficient factory (4 spin-orbitals).
// See Chapter 10 for the full integral set and Chapter 13 for loading FCIDUMP files.
let lookup =
let fcidump = """
&FCI NORB= 2,NELEC= 2,MS2=0,
ORBSYM=1,1,
ISYM=1,
&END
0.6747559268144484 1 1 1 1
0.6637114013508132 1 1 2 2
0.1812104620151968 2 1 2 1
0.6637114013508132 2 2 1 1
0.697651504490461 2 2 2 2
-1.253309786645977 1 1 0 0
-0.4750688487721783 2 2 0 0
0.7151043390810812 0 0 0 0
"""
let (factory, _core, _nso) = Fcidump.parseToSpinOrbitalFactory fcidump
factory
let hamEncoders = [
("Jordan-Wigner", jordanWignerTerms)
("Bravyi-Kitaev", bravyiKitaevTerms)
("Balanced Ternary", ternaryTreeTerms)
("Vlasov Tree", vlasovTreeTerms)
]
for (name, encoder) in hamEncoders do
let ham = (computeHamiltonianWith encoder lookup 4u).DistributeCoefficient
let terms = ham.SummandTerms |> Array.filter (fun t -> Complex.Abs t.Coefficient > 1e-10)
let weights = terms |> Array.map (fun t ->
t.Signature |> Seq.filter (fun c -> c <> 'I') |> Seq.length)
printfn "%-20s Terms: %d MaxWt: %d AvgWt: %.2f"
name terms.Length (Array.max weights) (Array.averageBy float weights)
Jordan-Wigner Terms: 15 MaxWt: 4 AvgWt: 2.13
Bravyi-Kitaev Terms: 15 MaxWt: 4 AvgWt: 2.40
Balanced Ternary Terms: 15 MaxWt: 4 AvgWt: 2.40
Vlasov Tree Terms: 15 MaxWt: 4 AvgWt: 2.40
For H₂ (4 qubits), both tree encodings match each other (and Bravyi–Kitaev) in term count and average weight. The differences grow with system size — at $n = 64$, different tree shapes can differ by 30–40% in total CNOT cost, making tree selection a first-order optimization concern.
Try it yourself: See Lab 10: Vlasov Tree for the full comparison script with tree structure printouts, per-mode weight tables, and H₂ Hamiltonian analysis.
Next: Building a Real Hamiltonian — the complete end-to-end pipeline