Source code for qiskit.providers.aer.pulse.duffing_model_generators

# -*- coding: utf-8 -*-

# This code is part of Qiskit.
#
# (C) Copyright IBM 2018, 2019.
#
# This code is licensed under the Apache License, Version 2.0. You may
# obtain a copy of this license in the LICENSE.txt file in the root directory
# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0.
#
# Any modifications or derivative works of this code must retain this
# copyright notice, and modified files need to carry a notice indicating
# that they have been altered from the originals.
# pylint: disable=invalid-name

"Helper functions for creating HamiltonianModel and PulseSystemModel objects"

from warnings import warn
from collections.abc import Iterable
from .hamiltonian_model import HamiltonianModel
from .pulse_system_model import PulseSystemModel


[docs]def duffing_system_model(dim_oscillators, oscillator_freqs, anharm_freqs, drive_strengths, coupling_dict, dt): r"""Returns a :class:`PulseSystemModel` representing a physical model for a collection of Duffing oscillators. In the model, each individual oscillator is specified by the parameters: * Frequency: :math:`\nu`, specified in the list ``oscillator_freqs`` * Anharmonicity: :math:`\alpha`, specified in the list ``anharm_freqs``, and * Drive strength: :math:`r`, specified in the list ``drive_strengths``. For each oscillator, the above parameters enter into the Hamiltonian via the terms: .. math:: \pi(2 \nu - \alpha)a^\dagger a + \pi \alpha (a^\dagger a)^2 + 2 \pi r D(t) (a + a^\dagger), where :math:`a^\dagger` and :math:`a` are, respectively, the creation and annihilation operators for the oscillator, and :math:`D(t)` is the drive signal for the oscillator. Each coupling term between a pair of oscillators is specified by: * Oscillator pair: :math:`(i,k)`, and * Coupling strength: :math:`j`, which are passed in the argument ``coupling_dict``, which is a ``dict`` with keys being the ``tuple`` ``(i,k)``, and values the strength ``j``. Specifying a coupling results in the Hamiltonian term: .. math:: 2 \pi j (a_i^\dagger a_k + a_i a_k^\dagger). Finally, the returned :class:`PulseSystemModel` is setup for performing cross-resonance drives between coupled qubits. The index for the :class:`ControlChannel` corresponding to a particular cross-resonance drive channel is retreived by calling :meth:`PulseSystemModel.control_channel_index` with the tuple ``(drive_idx, target_idx)``, where ``drive_idx`` is the index of the oscillator being driven, and ``target_idx`` is the target oscillator (see example below). Note: In this model, all frequencies are in frequency units (as opposed to radial). **Example** Constructing a three Duffing Oscillator :class:``PulseSystemModel``. .. code-block:: python # cutoff dimensions dim_oscillators = 3 # single oscillator drift parameters oscillator_freqs = [5.0e9, 5.1e9, 5.2e9] anharm_freqs = [-0.33e9, -0.33e9, -0.33e9] # drive strengths drive_strengths = [0.02e9, 0.02e9, 0.02e9] # specify coupling as a dictionary; here the qubit pair (0,1) is coupled with # strength 0.002e9, and the qubit pair (1,2) is coupled with strength 0.001e9 coupling_dict = {(0,1): 0.002e9, (1,2): 0.001e9} # time dt = 1e-9 # create the model three_qubit_model = duffing_system_model(dim_oscillators=dim_oscillators, oscillator_freqs=oscillator_freqs, anharm_freqs=anharm_freqs, drive_strengths=drive_strengths, coupling_dict=coupling_dict, dt=dt) In the above model, qubit pairs (0,1) and (1,2) are coupled. To perform a cross-resonance drive on qubit 1 with target 0, use the :class:`ControlChannel` with index: .. code-block:: python three_qubit_model.control_channel_index((1,0)) Args: dim_oscillators (int): Dimension of truncation for each oscillator. oscillator_freqs (list): Oscillator frequencies in frequency units. anharm_freqs (list): Anharmonicity values in frequency units. drive_strengths (list): Drive strength values in frequency units. coupling_dict (dict): Coupling graph with keys being edges, and values the coupling strengths in frequency units. dt (float): Sample width for pulse instructions. Returns: PulseSystemModel: The generated Duffing system model """ # set symbols for string generation freq_symbol = 'v' anharm_symbol = 'alpha' drive_symbol = 'r' coupling_symbol = 'j' coupling_edges = coupling_dict.keys() # construct coupling graph, and raise warning if coupling_edges contains duplicate edges coupling_graph = CouplingGraph(coupling_edges) if len(coupling_graph.graph) < len(coupling_edges): warn('Warning: The coupling_dict contains diplicate edges, and the second appearance of \ the same edge will be ignored.') # construct the HamiltonianModel num_oscillators = len(oscillator_freqs) oscillators = list(range(num_oscillators)) oscillator_dims = [dim_oscillators] * num_oscillators freq_symbols = _str_list_generator(freq_symbol + '{0}', oscillators) anharm_symbols = _str_list_generator(anharm_symbol + '{0}', oscillators) drive_symbols = _str_list_generator(drive_symbol + '{0}', oscillators) sorted_coupling_edges = coupling_graph.sorted_graph # populate coupling strengths in sorted order (vertex indices are now also sorted within edges, # so this needs to be accounted for when retrieving weights from coupling_dict) coupling_strengths = [coupling_dict.get(edge) or coupling_dict.get((edge[1], edge[0])) for edge in sorted_coupling_edges] coupling_symbols = _str_list_generator(coupling_symbol + '{0}{1}', *zip(*sorted_coupling_edges)) cr_idx_dict = coupling_graph.two_way_graph_dict hamiltonian_dict = _duffing_hamiltonian_dict(oscillators=oscillators, oscillator_dims=oscillator_dims, oscillator_freqs=oscillator_freqs, freq_symbols=freq_symbols, anharm_freqs=anharm_freqs, anharm_symbols=anharm_symbols, drive_strengths=drive_strengths, drive_symbols=drive_symbols, ordered_coupling_edges=sorted_coupling_edges, coupling_strengths=coupling_strengths, coupling_symbols=coupling_symbols, cr_idx_dict=cr_idx_dict) hamiltonian_model = HamiltonianModel.from_dict(hamiltonian_dict) # construct the u_channel_lo list u_channel_lo = _cr_lo_list(cr_idx_dict) # construct and return the PulseSystemModel return PulseSystemModel(hamiltonian=hamiltonian_model, u_channel_lo=u_channel_lo, control_channel_labels=coupling_graph.sorted_two_way_graph, subsystem_list=oscillators, dt=dt)
# Helper functions for creating pieces necessary to construct oscillator system models def _duffing_hamiltonian_dict(oscillators, oscillator_dims, oscillator_freqs, freq_symbols, anharm_freqs, anharm_symbols, drive_strengths, drive_symbols, ordered_coupling_edges, coupling_strengths, coupling_symbols, cr_idx_dict): """Creates a hamiltonian string dict for a duffing oscillator model Note, this function makes the following assumptions: - oscillators, oscillator_dims, oscillator_freqs, freq_symbols, anharm_freqs, anharm_symbols, drive_strengths, and drive_symbols are all lists of the same length (i.e. the total oscillator number) - ordered_coupling_edges, coupling_strengths, and coupling_symbols are lists of the same length Args: oscillators (list): ints for oscillator labels oscillator_dims (list): ints for oscillator dimensions oscillator_freqs (list): oscillator frequencies freq_symbols (list): symbols to be used for oscillator frequencies anharm_freqs (list): anharmonicity values anharm_symbols (list): symbols to be used for anharmonicity terms drive_strengths (list): drive strength coefficients drive_symbols (list): symbols for drive coefficients ordered_coupling_edges (list): tuples of two ints specifying oscillator couplings. Order corresponds to order of coupling_strengths and coupling_symbols coupling_strengths (list): strength of each coupling term (corresponds to ordering of ordered_coupling_edges) coupling_symbols (list): symbols for coupling coefficients cr_idx_dict (dict): A dict with keys given by tuples containing two ints, and value an int, representing cross resonance drive channels. E.g. an entry {(0,1) : 1} specifies a CR drive on oscillator 0 with oscillator 1 as target, with u_channel index 1. Returns: dict: hamiltonian string format """ # single oscillator terms hamiltonian_str = _single_duffing_drift_terms(freq_symbols, anharm_symbols, oscillators) hamiltonian_str += _drive_terms(drive_symbols, oscillators) # exchange terms if len(ordered_coupling_edges) > 0: hamiltonian_str += _exchange_coupling_terms(coupling_symbols, ordered_coupling_edges) # cr terms if len(cr_idx_dict) > 0: driven_system_indices = [key[0] for key in cr_idx_dict.keys()] cr_drive_symbols = [drive_symbols[idx] for idx in driven_system_indices] cr_channel_idx = cr_idx_dict.values() hamiltonian_str += _cr_terms(cr_drive_symbols, driven_system_indices, cr_channel_idx) # construct vars dictionary var_dict = {} for idx in oscillators: var_dict[freq_symbols[idx]] = oscillator_freqs[idx] var_dict[anharm_symbols[idx]] = anharm_freqs[idx] var_dict[drive_symbols[idx]] = drive_strengths[idx] if len(coupling_symbols) > 0: for symbol, strength in zip(coupling_symbols, coupling_strengths): var_dict[symbol] = strength dim_dict = {str(oscillator): dim for oscillator, dim in zip(oscillators, oscillator_dims)} return {'h_str': hamiltonian_str, 'vars': var_dict, 'qub': dim_dict} def _cr_lo_list(cr_idx_dict): """Generates u_channel_lo list for a PulseSystemModel from a cr_idx_dict. Args: cr_idx_dict (dict): A dictionary with keys given by tuples of ints with int values. A key, e.g. (0,1), signifies CR drive on system 0 with target 1, and the value is the u channel index corresponding to that drive. Note: this function assumes that cr_idx_dict.values() == range(len(cr_idx_dict)). Returns: list: u_channel_lo format required by the simulator """ # populate list of u channel lo for cr gates lo_list = [0] * len(cr_idx_dict) for system_pair, u_idx in cr_idx_dict.items(): lo_list[u_idx] = [{'scale': [1.0, 0.0], 'q': system_pair[1]}] return lo_list # Functions for creating Hamiltonian strings for various types of terms def _single_duffing_drift_terms(freq_symbols, anharm_symbols, system_list): """Harmonic and anharmonic drift terms Args: freq_symbols (list): coefficients for harmonic part anharm_symbols (list): coefficients for anharmonic part system_list (list): list of system indices Returns: list: drift term strings """ harm_terms = _str_list_generator('np.pi*(2*{0}-{1})*O{2}', freq_symbols, anharm_symbols, system_list) anharm_terms = _str_list_generator('np.pi*{0}*O{1}*O{1}', anharm_symbols, system_list) return harm_terms + anharm_terms def _drive_terms(drive_symbols, system_list): """Drive terms for single oscillator Args: drive_symbols (list): coefficients of drive terms system_list (list): list of system indices Returns: list: drive term strings """ return _str_list_generator('2*np.pi*{0}*X{1}||D{1}', drive_symbols, system_list) def _exchange_coupling_terms(coupling_symbols, ordered_edges): """Exchange coupling terms between systems Args: coupling_symbols (list): coefficients of exchange couplings ordered_edges (list): list tuples of system indices for the couplings Returns: list: exchange coupling strings """ idx1_list, idx2_list = zip(*list(ordered_edges)) return _str_list_generator('2*np.pi*{0}*(Sp{1}*Sm{2}+Sm{1}*Sp{2})', coupling_symbols, idx1_list, idx2_list) def _cr_terms(drive_symbols, driven_system_indices, u_channel_indices): """Cross resonance drive terms Args: drive_symbols (list): coefficients for drive terms driven_system_indices (list): list of indices for systems that drive is applied to u_channel_indices (list): indicies for the u_channels corresponding to each term Returns: list: cr term strings """ return _str_list_generator('2*np.pi*{0}*X{1}||U{2}', drive_symbols, driven_system_indices, u_channel_indices) def _str_list_generator(str_template, *args): """Given a string template, returns a list where each entry is the template formatted by the zip of args. It is assumed that either args is a tuple of lists each of the same length, or is a tuple with each entry beign either an str or int. E.g. 1. _str_list_generator('First: {0}, Second: {1}', 'a0', 'b0') returns ['First: a0, Second: b0'] 2. _str_list_generator('First: {0}, Second: {1}', ['a0', 'a1'], ['b0', 'b1']) returns ['First: a0, Second: b0', 'First: a1, Second: b1'] Args: str_template (str): string template args (tuple): assumed to be either tuple of iterables of the same length, or a tuple with entries that are either type str or int Returns: list: list of str_template formated by args lists """ args = [_arg_to_iterable(arg) for arg in args] return [str_template.format(*zipped_arg) for zipped_arg in zip(*args)] def _arg_to_iterable(arg): """Check if arg is an iterable, if not put it into a list. The purpose is to allow arguments of functions to be either lists or singletons, e.g. instead of having to pass ['a'], 'a' can be passed directly. Args: arg (Iterable): argument to be checked and turned into an interable if necessary Returns: Iterable: either arg, or arg transformed into a list """ # catch expected types (issue is str is iterable) if isinstance(arg, (int, str)): return [arg] if isinstance(arg, Iterable): return arg return [arg] # Helper classes class CouplingGraph: """ Helper class containing functionality for representing coupling graphs, with the main goal to construct different representations for different purposes: - self.graph: graph as a set of edges stored as frozen sets, e.g. {frozenset({0,1}), frozenset({1,2}), frozenset({2,3})} - self.sorted_graph: graph as a list of tuples in lexicographic order, e.g. [(0,1), (1,2), (2,3)] Note: these are actively ordered by the object, as the point is to have a canonical ordering of edges. The integers in the tuples are also ordered. - self.sorted_two_way_graph: list of tuples where each edge is repeated with the vertices reversed. The ordering is the same as in sorted_graph, with the duplicate appearing immediately after the original, e.g. [(0,1), (1,0), (1,2), (2,1), (2,3), (3,2)] - self.two_way_graph_dict: same as above, but in dict form, e.g. {(0,1) : 0, (1,0) : 1, (1,2) : 2, (2,1) : 3, (2,3) : 4, (3,2) : 5} """ def __init__(self, edges): """returns CouplingGraph object Args: edges (Iterable): An iterable of iterables, where the inner interables are assumed to contain two elements, e.g. [(0,1), (2,3)], or ((0,1), (2,3)) Returns: CouplingGraph: coupling graph specified by edges """ # create the set representation of the graph self.graph = {frozenset({idx1, idx2}) for idx1, idx2 in edges} # created the sorted list representation graph_list = [] for edge in self.graph: edge_list = list(edge) edge_list.sort() graph_list.append(tuple(edge_list)) graph_list.sort() self.sorted_graph = graph_list # create the sorted_two_way_graph two_way_graph_list = [] for edge in self.sorted_graph: two_way_graph_list.append(edge) two_way_graph_list.append((edge[1], edge[0])) self.sorted_two_way_graph = two_way_graph_list # create the dictionary version self.two_way_graph_dict = {self.sorted_two_way_graph[k]: k for k in range(len(self.sorted_two_way_graph))} def sorted_edge_index(self, edge): """Given an edge, returns the index in self.sorted_graph. Order in edge does not matter. Args: edge (Iterable): an iterable containing two integers Returns: int: index of edge """ edge_list = list(edge) edge_list.sort() return self.sorted_graph.index(tuple(edge_list)) def two_way_edge_index(self, directed_edge): """Given a directed edge, returns the index in self.sorted_two_way_graph Args: directed_edge (Iterable): an iterable containing two integers Returns: int: index of directed_edge """ return self.two_way_graph_dict[tuple(directed_edge)]