<a id="mappers-explanation"></a>

# Map operator representations

**Mappers** are routines that transform operators from one representation to another
while preserving mathematical equivalence. Use them to work flexibly across
different operator algebras and to prepare operators for quantum circuit execution.

## Implement custom mappers

The mappers module makes it straightforward to implement custom
mappings. Mappers work by transforming the fundamental
actions (creation, annihilation, Majorana, and so on) that compose an operator, then
combining these transformed actions according to the target representation’s rules.

As a concrete example, consider a Jordan-Wigner transformation that maps
a [`MajoranaOperator`](../stubs/qiskit_fermions.operators.MajoranaOperator.html.md#qiskit_fermions.operators.MajoranaOperator) to a qubit operator using [`map_majorana_action_generators()`](../stubs/qiskit_fermions.mappers.map_majorana_action_generators.html.md#qiskit_fermions.mappers.map_majorana_action_generators):

### PYTHON

```python
>>> from qiskit.quantum_info import SparseObservable
>>> from qiskit_fermions.mappers import map_majorana_action_generators
>>> from qiskit_fermions.operators import MajoranaOperator
>>>
>>> # Create a Majorana operator
>>> maj_op = MajoranaOperator.from_dict({(0, 1): 1.0, (2,): 0.5})
>>>
>>> # Define how each Majorana action maps to Pauli strings
>>> num_qubits = 2
>>> def map_action(mode: int) -> SparseObservable:
...     idx = mode // 2
...     qubits = list(range(idx + 1))
...     pauli = "Y" if mode % 2 else "X"
...     return SparseObservable.from_sparse_list(
...         [("Z" * idx + pauli, qubits, 1.0)],
...         num_qubits=num_qubits,
...     )
>>>
>>> # Apply the mapping to transform the operator
>>> custom_observable = map_majorana_action_generators(
...     maj_op,
...     map_action,
...     identity=lambda: SparseObservable.identity(num_qubits),
...     compose=SparseObservable.compose,
... )
>>> print(custom_observable.simplify())
<SparseObservable with 2 terms on 2 qubits: (0+1j)(Z_0) + (0.5+0j)(X_1 Z_0)>
```

### C

```c
// The C API provides direct mapping functions for common transformations.
// Custom mapper prototyping as shown in Python is primarily a Python feature.
// For custom mappings in C, you will need to perform the iteration and
// conversion logic entirely by yourself.
```

The key steps to implement any custom mapper are:

1. **Define action transformation**: Specify how each action (indexed by mode) maps
   to the target representation. In the example above, Majorana actions map to Pauli
   strings according to Jordan-Wigner rules.
2. **Use one of the provided mapping functions**: The [`mappers`](../pydoc/qiskit_fermions.mappers.html.md#module-qiskit_fermions.mappers)
   module provides utility functions to handle iterating over the operator terms and
   subsequent applications of the custom action map for the operator
   representations provided by the [`operators`](../pydoc/qiskit_fermions.operators.html.md#module-qiskit_fermions.operators) module. For example,
   [`map_majorana_action_generators()`](../stubs/qiskit_fermions.mappers.map_majorana_action_generators.html.md#qiskit_fermions.mappers.map_majorana_action_generators) works with Majorana operators.
3. **Get the result**: The mapping function returns an operator in the target
   representation with the transformation applied consistently across all terms.

This design makes it easy to prototype custom mappings without deep knowledge of
internal operator structures. The transformation is applied automatically to every
term in the operator while preserving mathematical equivalence.

#### HINT
You can also iterate the terms of an operator manually rather than
using one of the provided iterator functions. See the section on [term iteration](operators.html.md#term-iteration-and-reconstruction) in the operators guide for details.

## Library implementations

The [`qiskit_fermions.mappers.library`](../pydoc/qiskit_fermions.mappers.library.html.md#module-qiskit_fermions.mappers.library) module provides efficient, thoroughly
tested implementations of common mappings. These follow the same underlying pattern
as custom mappers but are optimized for production use and are available in both the
Python and C APIs.

The example below applies a library mapper to the same [`MajoranaOperator`](../stubs/qiskit_fermions.operators.MajoranaOperator.html.md#qiskit_fermions.operators.MajoranaOperator)
from the custom mapper section, demonstrating equivalent results:

### PYTHON

```python
>>> from qiskit_fermions.mappers.library import majorana_to_fermion, jordan_wigner
>>>
>>> # Convert MajoranaOperator to FermionOperator using library mapper
>>> ferm_op = majorana_to_fermion(maj_op)
>>>
>>> # Apply Jordan-Wigner mapper to get qubit operator
>>> sparse_observable = jordan_wigner(ferm_op, num_qubits=2)
>>> difference = sparse_observable - custom_observable
>>> print(difference.simplify() == SparseObservable.zero(num_qubits))
True
```

### C

```c
#include <qiskit_fermions.h>

// Create a MajoranaOperator with the same terms as maj_op above
uint64_t num_terms = 2;
uint64_t num_modes = 3;
uint32_t modes[3] = {0, 1, 2};
QkComplex64 coeffs[2] = {{1.0, 0.0}, {0.5, 0.0}};
uint32_t boundaries[3] = {0, 2, 3};
QfMajoranaOperator *maj_op = qf_maj_op_new(num_terms, num_modes, coeffs, modes, boundaries);

// Convert MajoranaOperator to FermionOperator using library mapper
QfFermionOperator *ferm_op = qf_majorana_to_fermion(maj_op);

// Apply Jordan-Wigner mapper to get qubit operator
QkObs *qubit_op;
QfExitCode exit = qf_ferm_op_jordan_wigner(ferm_op, 2, &qubit_op);
assert(exit == QfExitCode_Success);

// Clean up
qf_ferm_op_free(ferm_op);
qf_maj_op_free(maj_op);
qk_obs_free(qubit_op);
```

#### NOTE
The two-step above is written out to show two library mappers composing. In practice you would map
a [`MajoranaOperator`](../stubs/qiskit_fermions.operators.MajoranaOperator.html.md#qiskit_fermions.operators.MajoranaOperator) in one step instead, with [`majorana_jordan_wigner()`](../stubs/qiskit_fermions.mappers.library.majorana_jordan_wigner.html.md#qiskit_fermions.mappers.library.majorana_jordan_wigner)
([`qf_maj_op_jordan_wigner()`](../cdoc/qf-mappers-library.html.md#c.qf_maj_op_jordan_wigner) in the C API), and likewise for the other operator types –
[`edge_vertex_jordan_wigner()`](../stubs/qiskit_fermions.mappers.library.edge_vertex_jordan_wigner.html.md#qiskit_fermions.mappers.library.edge_vertex_jordan_wigner) and [`transfer_vertex_jordan_wigner()`](../stubs/qiskit_fermions.mappers.library.transfer_vertex_jordan_wigner.html.md#qiskit_fermions.mappers.library.transfer_vertex_jordan_wigner)
([`qf_edge_op_jordan_wigner()`](../cdoc/qf-mappers-library.html.md#c.qf_edge_op_jordan_wigner), [`qf_transfer_op_jordan_wigner()`](../cdoc/qf-mappers-library.html.md#c.qf_transfer_op_jordan_wigner)). Going via a
[`FermionOperator`](../stubs/qiskit_fermions.operators.FermionOperator.html.md#qiskit_fermions.operators.FermionOperator) also inflates the intermediate result: every generator of these algebras
maps onto a single Pauli string, whereas each fermionic action maps onto a two-term sum, so a term
built from several generators expands only to be merged back down again. That saving grows with the
length of the terms; for the single-generator terms of a hopping Hamiltonian the two routes cost
about the same.

In the Python API, some of these library mappers are also reachable through type-agnostic
convenience mappers. [`fermion_operator()`](../stubs/qiskit_fermions.mappers.library.fermion_operator.html.md#qiskit_fermions.mappers.library.fermion_operator) and [`majorana_operator()`](../stubs/qiskit_fermions.mappers.library.majorana_operator.html.md#qiskit_fermions.mappers.library.majorana_operator) sit in front of
[`majorana_to_fermion()`](../stubs/qiskit_fermions.mappers.library.majorana_to_fermion.html.md#qiskit_fermions.mappers.library.majorana_to_fermion) and [`fermion_to_majorana()`](../stubs/qiskit_fermions.mappers.library.fermion_to_majorana.html.md#qiskit_fermions.mappers.library.fermion_to_majorana) (among others), delegating to the
appropriate library implementation internally based on the type of object they are given,
rather than requiring you to pick the right function yourself. The same applies to
[`jordan_wigner()`](../stubs/qiskit_fermions.mappers.library.jordan_wigner.html.md#qiskit_fermions.mappers.library.jordan_wigner) and to [`ternary_tree()`](../stubs/qiskit_fermions.mappers.library.ternary_tree.html.md#qiskit_fermions.mappers.library.ternary_tree) (introduced below), each of which dispatches to
whichever direct implementation matches the operator it is given. See
[`qiskit_fermions.protocols`](../pydoc/qiskit_fermions.protocols.html.md#module-qiskit_fermions.protocols) for how this dispatch is implemented internally.

## Choose an encoding with ternary trees

Jordan-Wigner is not the only fermion-to-qubit encoding, and it is not the cheapest. Its
$Z$ strings grow with the mode index, so a Majorana operator on the last of $n$ modes maps
onto a Pauli string of weight $n$. Encodings that spread the parity information across a tree
instead of a chain do asymptotically better.

A **ternary tree** on $n$ nodes captures this whole family. Each node is a qubit carrying three
downward links labeled `X`, `Y` and `Z`; a link either descends to a child node (an *edge*) or
terminates (a *leg*). Each leg’s path back to the root spells a Pauli string: crossing the link
labeled $P$ that descends *from* node $u$ contributes $P$ on qubit $u$. Every
such tree yields a valid encoding, and the familiar ones are simply particular shapes – a chain along
`Z` is Jordan-Wigner, while a tree in which every node has three children, as far as the node count
allows, is balanced.

### PYTHON

```python
>>> from qiskit_fermions.mappers import TernaryTree, TernaryTreeEncoding
>>>
>>> # Jordan-Wigner is a chain descending along Z, with X and Y as legs at every node.
>>> TernaryTree.chain(13, "Z").max_weight
13
>>> # The balanced tree spreads the same information over depth log_3(2n+1).
>>> TernaryTree.breadth_first(13, 3).max_weight
3
```

### C

```c
#include <qiskit_fermions.h>

QfTernaryTree *chain;
qf_ternary_tree_chain(13, QfPauliLabel_Z, &chain);

QfPauliLabel order[3] = {QfPauliLabel_X, QfPauliLabel_Y, QfPauliLabel_Z};
QfTernaryTree *balanced;
qf_ternary_tree_breadth_first(13, 3, order, &balanced);

assert(qf_ternary_tree_max_weight(chain) == 13);
assert(qf_ternary_tree_max_weight(balanced) == 3);

qf_ternary_tree_free(chain);
qf_ternary_tree_free(balanced);
```

That maximum weight is optimal. [Jiang et al.](https://arxiv.org/abs/1910.10746) prove that the
Majorana operators of *any* fermion-to-qubit mapping of $n$ modes have an average Pauli weight of
at least $\log_3(2n)$, so no mapping can bring the largest of them below
$\lceil \log_3(2n+1) \rceil$. Both trees use exactly $n$ qubits, so the saving is in
Pauli weight, not qubit count.

To map an operator, compile a tree into an encoding and hand that to a mapper along with the qubit
count for the result. Compiling pairs the tree’s legs into Majorana operators and builds their Pauli
strings once, so the same encoding should be reused across many operators. The qubit count stays with
the mapping call rather than the encoding, so one encoding can serve registers of different widths; a
count above the tree’s node count pads with the identity.

### PYTHON

```python
>>> from qiskit_fermions.mappers.library import fermion_ternary_tree
>>> from qiskit_fermions.operators import FermionOperator
>>>
>>> hamiltonian = FermionOperator.from_dict(
...     {((True, 0), (False, 3)): 1.0, ((True, 3), (False, 0)): 1.0}
... )
>>> encoding = TernaryTreeEncoding(TernaryTree.breadth_first(4, 3))
>>> fermion_ternary_tree(hamiltonian, encoding, 4).simplify()
<SparseObservable with 2 terms on 4 qubits: (0.5+0j)(Y_3 Z_1 Y_0) + (0.5+0j)(X_3 Z_2 X_0)>
```

### C

```c
// Create the hopping Hamiltonian a^dagger_0 a_3 + a^dagger_3 a_0
QfFermionOperator *hamiltonian = qf_ferm_op_zero();
QkComplex64 coeffs[2] = {{1.0, 0.0}, {1.0, 0.0}};
bool actions[4] = {true, false, true, false};
uint32_t indices[4] = {0, 3, 3, 0};
for (int i = 0; i < 2; i++) {
    qf_ferm_op_add_term(hamiltonian, 2, actions + 2 * i, indices + 2 * i, &coeffs[i]);
}

QfPauliLabel order[3] = {QfPauliLabel_X, QfPauliLabel_Y, QfPauliLabel_Z};
QfTernaryTree *tree;
qf_ternary_tree_breadth_first(4, 3, order, &tree);

// Compile the tree once, then reuse the encoding for every operator mapped through it.
QfTernaryTreeEncoding *encoding;
qf_ternary_tree_encoding_new(tree, NULL, &encoding);

QkObs *qubit_op;
QfExitCode exit = qf_ferm_op_ternary_tree(hamiltonian, encoding, 4, &qubit_op);
assert(exit == QfExitCode_Success);

// Clean up
qf_ferm_op_free(hamiltonian);
qf_ternary_tree_encoding_free(encoding);
qf_ternary_tree_free(tree);
qk_obs_free(qubit_op);
```

Mapping through a chain along `Z` reproduces [`fermion_jordan_wigner()`](../stubs/qiskit_fermions.mappers.library.fermion_jordan_wigner.html.md#qiskit_fermions.mappers.library.fermion_jordan_wigner)
([`qf_ferm_op_jordan_wigner()`](../cdoc/qf-mappers-library.html.md#c.qf_ferm_op_jordan_wigner) in the C API) exactly, which is the sense in which this generalizes
it:

```pycon
>>> from qiskit_fermions.mappers.library import fermion_jordan_wigner
>>> chain = TernaryTreeEncoding(TernaryTree.chain(4, "Z"))
>>> direct = fermion_jordan_wigner(hamiltonian, 4)
>>> (fermion_ternary_tree(hamiltonian, chain, 4) - direct).simplify().num_terms
0
```

Every operator representation has a ternary-tree mapper, mirroring the Jordan-Wigner set; see
[`qiskit_fermions.mappers.library`](../pydoc/qiskit_fermions.mappers.library.html.md#module-qiskit_fermions.mappers.library) for the full list, and [Mapper Library](../cdoc/qf-mappers-library.html.md#qf-mapper-library) in the C API.
In the Python API they are also reachable through [`ternary_tree()`](../stubs/qiskit_fermions.mappers.library.ternary_tree.html.md#qiskit_fermions.mappers.library.ternary_tree), the type-agnostic wrapper
that picks the right one for the operator it is given, just as [`jordan_wigner()`](../stubs/qiskit_fermions.mappers.library.jordan_wigner.html.md#qiskit_fermions.mappers.library.jordan_wigner) does:

```pycon
>>> from qiskit_fermions.mappers.library import ternary_tree
>>> ternary_tree(hamiltonian, encoding, 4).simplify()
<SparseObservable with 2 terms on 4 qubits: (0.5+0j)(Y_3 Z_1 Y_0) + (0.5+0j)(X_3 Z_2 X_0)>
```

#### NOTE
Some identities that hold under Jordan-Wigner are properties of the *chain* rather than of ternary
trees. The number operator is $n_j = (1 - Z_j)/2$ only on the chain; on another tree the
$Z_j$ becomes a $Z$ string over a set of qubits. Likewise a vertex operator $V_j$
is the weight-1 Pauli $Z_j$ only on the chain, where the two Majorana strings’ $Z$
chains cancel.

Because every number operator still maps to a single $Z$ string, a Fock state is still a
computational basis state under any ternary tree, but bit $q$ no longer reads occupation
$q$. Occupation $j$ is instead the parity of the measured bits on the support of
$n_j$’s image. Read those supports off the mapper rather than deriving them from the tree, so
the decoding stays correct for any shape or mode permutation:

```pycon
>>> for mode in range(4):
...     number = FermionOperator.from_dict({((True, mode), (False, mode)): 1.0})
...     print(mode, ternary_tree(number, encoding, 4).simplify())
0 <SparseObservable with 2 terms on 4 qubits: (0.5+0j)() + (-0.5+0j)(Z_2 Z_1 Z_0)>
1 <SparseObservable with 2 terms on 4 qubits: (0.5+0j)() + (-0.5+0j)(Z_1)>
2 <SparseObservable with 2 terms on 4 qubits: (0.5+0j)() + (-0.5+0j)(Z_2)>
3 <SparseObservable with 2 terms on 4 qubits: (0.5+0j)() + (-0.5+0j)(Z_3)>
```

On this balanced tree, mode 0 is occupied when qubits 0, 1, and 2 hold an odd number of ones, so
preparing it means flipping qubit 0 alone, while decoding a measured bitstring takes the parity over
all three. A single bit-flip error on qubit 1 then corrupts the occupations of both mode 0 and mode
1, which matters for any workflow that reads occupations from noisy samples.

When an [`Evolution`](../stubs/qiskit_fermions.circuit.library.Evolution.html.md#qiskit_fermions.circuit.library.Evolution) is lowered through such a mapper, wrap the mapper in [`group_wise()`](../stubs/qiskit_fermions.transpiler.passes.synthesis.group_wise.html.md#qiskit_fermions.transpiler.passes.synthesis.group_wise)
(see [Synthesize an Evolution gate in fermionic space](fermionic_synthesis.html.md#fermionic-synthesis-explanation)). A hopping term maps to two commuting Pauli strings,
neither of which conserves particle number on its own; only their product in the evolution does.
Under Jordan-Wigner both strings act on the same qubits, so a simplified term order keeps them
adjacent. Under other trees their supports differ (as in `Y_3 Z_1 Y_0` and `X_3 Z_2 X_0`
earlier), the raw and the simplified orders both interleave them with the images of other terms,
and a qubit-side product formula then leaks particle number.

### Build an arbitrary tree

The chain and breadth-first constructors cover the uniform families, but the general interface takes
one `(parent, label)` pair per node, with `None` for the root (the C API takes the root’s index
instead). This matters because the trees worth searching for are usually neither a chain nor
uniform – the Bonsai algorithm, for instance, grows a spanning tree of a device’s coupling graph, so
its branching follows that connectivity.

### PYTHON

```python
>>> # Node 0 branches three ways, node 1 twice, and node 4 carries a single child.
>>> tree = TernaryTree([None, (0, "X"), (0, "Y"), (0, "Z"), (1, "X"), (1, "Z"), (4, "Y")])
>>> tree.num_nodes, tree.num_legs, tree.max_weight
(7, 15, 4)
>>> tree.children(0)
{'X': 1, 'Y': 2, 'Z': 3}
```

### C

```c
// Node 0, the root, branches three ways, node 1 twice, and node 4 carries a single child.
// The root has no parent and no incoming link, so its entries are never read: any value works.
#define IGNORED 0

uint32_t parents[7] = {IGNORED, 0, 0, 0, 1, 1, 4};
QfPauliLabel labels[7] = {
    IGNORED,        QfPauliLabel_X, QfPauliLabel_Y, QfPauliLabel_Z,
    QfPauliLabel_X, QfPauliLabel_Z, QfPauliLabel_Y,
};

QfTernaryTree *tree;
QfExitCode exit = qf_ternary_tree_new(7, 0, parents, labels, &tree);
assert(exit == QfExitCode_Success);

assert(qf_ternary_tree_num_nodes(tree) == 7);
assert(qf_ternary_tree_num_legs(tree) == 15);
assert(qf_ternary_tree_max_weight(tree) == 4);

qf_ternary_tree_free(tree);
```

Note the leg count: it is always $2n+1$, whatever the shape. A node with fewer children does not
generate fewer Pauli strings – its unused links are legs, and a leg spells a string just as an edge’s
descendants do. Of those $2n+1$ strings, $2n$ pair into the Majorana operators of the
$n$ modes, and exactly one is left over.

### Total parity

That leftover leg is the total fermionic parity $\prod_j (1 - 2 n_j)$. It is a property of the
encoding rather than a mapping of any operator, so it is a method on the encoding itself:

### PYTHON

```python
>>> TernaryTreeEncoding(TernaryTree.chain(4, "Z")).total_parity(4)
<SparseObservable with 1 term on 4 qubits: (1+0j)(Z_3 Z_2 Z_1 Z_0)>
>>> TernaryTreeEncoding(TernaryTree.breadth_first(4, 3)).total_parity(4)
<SparseObservable with 1 term on 4 qubits: (1+0j)(Z_3 Z_0)>
```

### C

```c
QfTernaryTree *tree;
qf_ternary_tree_chain(4, QfPauliLabel_Z, &tree);

QfTernaryTreeEncoding *encoding;
qf_ternary_tree_encoding_new(tree, NULL, &encoding);

// On the chain this is the full Z string; on the balanced tree it is not.
QkObs *parity;
qf_ternary_tree_encoding_total_parity(encoding, 4, &parity);

qf_ternary_tree_encoding_free(encoding);
qf_ternary_tree_free(tree);
qk_obs_free(parity);
```

As the balanced case shows, this is *not* in general the all-$Z$ string: it is the leg reached by
the all-$Z$ *path*, which acts as the identity on every node not on that path.

#### IMPORTANT
Total parity here is an **observable to measure, not a constraint to impose**. Both of its
eigenvalues are physical, so do not project a state onto the $+1$ sector: a ternary-tree
encoding spends one qubit per mode and is onto the full $2^n$-dimensional space, leaving no
unphysical subspace to project away.

<a id="bravyi-kitaev-by-hand"></a>

### Worked example: a Bravyi-Kitaev tree

Specifications are more often computed than typed out, since the tree for a real encoding is rarely small
enough to write by hand. Bravyi-Kitaev is a good example: it belongs to this family, but its tree is not
uniform, so no convenience constructor covers it.

Its tree is built by a recursive split over the nodes, one per mode. Within a range of nodes
$[l, r]$, let $h$ be the largest power of two that fits; the node placed for that range is
$l + h - 1$, the last one in that block. Its `X` link descends to the tree built from the block
before it and its `Z` link to the tree built from the remainder after it, leaving its `Y` link as a
leg:

```pycon
>>> def bravyi_kitaev_tree(num_modes: int) -> TernaryTree:
...     """Builds the ternary tree of the Bravyi-Kitaev encoding on ``num_modes`` modes."""
...     spec: list[tuple[int, str] | None] = [None] * num_modes
...     def build(left: int, right: int) -> int | None:
...         if left > right:
...             return None  # an empty range: the parent's link stays a leg
...         block = 1
...         while 2 * block <= right - left + 1:
...             block *= 2
...         node = left + block - 1
...         for child, label in (
...             (build(left, node - 1), "X"),   # the block before this node
...             (build(node + 1, right), "Z"),  # the remainder after it
...         ):
...             if child is not None:
...                 spec[child] = (node, label)
...         return node
...     build(0, num_modes - 1)
...     return TernaryTree(spec)
```

At four modes the whole range is a single block, so the node placed first is 3, with $[0, 2]$
before it and nothing after; that sub-range gives node 1 over the single nodes 0 and 2. Compiling the
tree confirms the encoding is Bravyi-Kitaev and not merely a tree of the same shape class – its first
Majorana image is $X_0 X_1 X_3$:

```pycon
>>> tree = bravyi_kitaev_tree(4)
>>> tree.children(1)
{'X': 0, 'Z': 2}
>>> encoding = TernaryTreeEncoding(tree)
>>> encoding.majorana_image(0)
[(0, 'X'), (1, 'X'), (3, 'X')]
```

Its total parity makes the point of the previous section concrete: the leftover leg is the one reached
by the all-$Z$ path, and here that path stops at the root, so the observable is the single-qubit
$Z_3$ rather than a string over all four qubits:

```pycon
>>> encoding.total_parity(4)
<SparseObservable with 1 term on 4 qubits: (1+0j)(Z_3)>
```

Scaling up needs nothing but a larger argument. Because no node ever spends its `Y` link on an edge,
each branches at most two ways, so the Pauli weight grows logarithmically rather than linearly:

```pycon
>>> [bravyi_kitaev_tree(n).max_weight for n in (4, 8, 16, 32, 64)]
[3, 4, 5, 6, 7]
```
