Hierarchical diagonalization#

For a large circuit with many degrees of freedoms, a possibly efficient way of obtaining low-lying eigenenergies and eigenstates is to partition the system into several subsystems, and to use the low-lying energy eigenstates of each subsystems as basis states to diagonalize the full system Hamiltonian. The schematic diagram below illustrates how hierarchical diagonalization is performed:

HD

For the example of the zero-pi qubit, the expression from sym_hamiltonian shows that \(\theta_2\) corresponds to the harmonic zeta mode of the zero-pi qubit. The remaining variables \(\theta_1\) and \(\theta_3\) form the primary qubit degrees of freedom and may be considered a “separate”, weakly coupled subsystem.

System hierarchy#

This idea of a subsystem hierarchy is made explicit by grouping circuit variable indices in a nested list:

[14]:
system_hierarchy = [[1,3], [2]]

This nested list groups variables \(1\) and \(3\) into one subsystem, and makes variable \(2\) a separate subsystem.

List nesting extends to multiple layers, so that more complex hierarchies can be captured.
For example, a zero-pi qubit coupled to an oscillator (variable \(4\)) could be associated with the hierarchy [[[1,3], [2]], [4]].

For convenience, a default list of truncated Hilbert space dimensions is generated by truncation_template:

[15]:
scq.truncation_template(system_hierarchy)
[15]:
$\displaystyle \left[ 6, \ 6\right]$

This template obtained this way is meant to provide a list of the right shape. The entries specifying the truncation levels should, of course, be adjusted.

Enabling hierarchical diagonalization#

To enable hierarchical diagonalization, the system hierarchy and truncation scheme info are handed over to configure:

[16]:
zero_pi.cutoff_n_1 = 15
zero_pi.cutoff_ext_2 = 50
zero_pi.cutoff_ext_3 = 100
zero_pi.configure(system_hierarchy=system_hierarchy, subsystem_trunc_dims=[150, 30])

Note that the truncated dimension (specified using subsystem_trunc_dims in configure) for each of the subsystems should be less than \(N-1\), where \(N\) is the Hilbert space dimension of the subsystem.

Once the hierarchy is set, subsystem Hamiltonians can be viewed via

[17]:
zero_pi.sym_hamiltonian(subsystem_index=0, float_round=4)  # show Hamiltonian for subsystem 0
[17]:
$\displaystyle \left(40.0 Q_{3}^{2} + 0.04 n_{1}^{2} + 0.0799 n_{1} n_{g1}\right) - \left(- 0.008 θ_{3}^{2} + EJ \cos{\left(θ_{1} - 1.0 θ_{3} \right)} + EJ \cos{\left(θ_{1} + θ_{3} - 1.0 (2πΦ_{1}) \right)}\right)$

Hamiltonian terms describing the coupling between two subsystems are displayed via

[18]:
zero_pi.sym_interaction((0,1))  # show coupling terms between subsystems 0 and 1
[18]:
$\displaystyle 0$

(For the symmetric zero-pi qubit, the zeta mode and the primary qubit degrees of freedom decouple.)

Each subsystem has access to circuit methods, like eigenvals. Here are the eigenenergies for the zeta mode:

[19]:
zero_pi.subsystems[1].eigenvals()
[19]:
array([0.01777276, 0.05203508, 0.08763912, 0.10359738, 0.16700863,
       0.16783645])

Hierarchical diagonalization allows us to increase variable-specific cutoffs without exploding the dimension of the joint Hilbert space.

Runtime information

  • Computation of eigenvalues and eigenvectors is deferred until unavoidable.

  • Subsystem eigenenergies and eigenstates are stored internally for re-use (until they must be replaced by new ones).

For instance, in hierarchical-diagonalization mode, calling for the <circuit>.hamiltonian() necessitates the computation of eigenvalues and eigenstates of individual subsystems. The first run will involve subsystem diagonalization and will be slower; calls subsequent to that will re-use data and be accordingly faster.