Source code for qiskit_dynamics.backend.backend_string_parser.hamiltonian_string_parser

# This code is part of Qiskit.
#
# (C) Copyright IBM 2022.
#
# 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

"""
Functionality for importing qiskit.pulse model string representation.

This file is meant for internal use and may be changed at any point.
"""

from typing import Tuple, List, Optional
from collections import OrderedDict

# required for calls to exec
# pylint: disable=unused-import
import numpy as np

from qiskit import QiskitError

from .regex_parser import _regex_parser


# valid channel characters
CHANNEL_CHARS = ["U", "D", "M", "A", "u", "d", "m", "a"]


[docs] def parse_backend_hamiltonian_dict( hamiltonian_dict: dict, subsystem_list: Optional[List[int]] = None ) -> Tuple[np.ndarray, np.ndarray, List[str], dict]: r"""Convert Pulse backend Hamiltonian dictionary into concrete array format with an ordered list of corresponding channels. The Pulse backend Hamiltonian dictionary, ``hamiltonian_dict``, must have the following keys: * ``'h_str'``: List of Hamiltonian terms in string format (see below). * ``'qub'``: Dictionary giving subsystem dimensions. Keys are subsystem labels, values are their dimensions. * ``'vars'``: Dictionary whose keys are the variables appearing in the terms in the ``h_str`` list, and values being the numerical values of the variables. The optional argument ``subsystem_list`` specifies a subset of subsystems to keep when parsing. If ``None``, all subsystems are kept. If ``subsystem_list`` is specified, then terms including subsystems not in the list will be ignored. Entries in the list ``hamiltonian_dict['h_str']`` must be formatted as a product of constants (either numerical constants or variables in ``hamiltonian_dict['vars'].keys()``) with operators. Operators are indicated with a capital letters followed by an integer indicating the subsystem the operator acts on. Accepted operator strings are: * ``'X'``: If the target subsystem is two dimensional, the Pauli :math:`X` operator, and if greater than two dimensional, returns :math:`a + a^\dagger`, where :math:`a` and :math:`a^\dagger` are the annihiliation and creation operators, respectively. * ``'Y'``: If the target subsystem is two dimensional, the Pauli :math:`Y` operator, and if greater than two dimensional, returns :math:`-i(a - a^\dagger)`, where :math:`a` and :math:`a^\dagger` are the annihiliation and creation operators, respectively. * ``'Z'``: If the target subsystem is two dimensional, the Pauli :math:`Z` operator, and if greater than two dimensional, returns :math:`I - 2 * N`, where :math:`N` is the number operator. * ``'a'``, ``'A'``, or ``'Sm'``: If two dimensional, the sigma minus operator, and if greater, generalizes to the annihilation operator. * ``'C'``, or ``'Sp'``: If two dimensional, sigma plus operator, and if greater, generalizes to the creation operator. * ``'N'``, or ``'O'``: The number operator. * ``'I'``: The identity operator. In addition to the above, a term in ``hamiltonian_dict['h_str']`` can be associated with a channel by ending it with a string of the form ``'||Sxx'``, where ``S`` is a valid channel label, and ``'xx'`` is an integer. Accepted channel labels are: * ``'D'`` or ``'d'`` for drive channels. * ``'U'`` or ``'u'`` for control channels. * ``'M'`` or ``'m'`` for measurement channels. * ``'A'`` or ``'a'`` for acquire channels. Finally, summations of terms of the above form can be indicated in ``hamiltonian_dict['h_str']`` via strings with syntax ``'_SUM[i, lb, ub, aa||S{i}]'``, where: * ``i`` is the summation variable. * ``lb`` and ``ub`` are the summation endpoints (inclusive). * ``aa`` is a valid operator string, possibly including the string ``{i}`` to indicate operators acting on subsystem ``i``. * ``S{i}`` is the specification of a channel indexed by ``i``. For example, the following ``hamiltonian_dict`` specifies a single transmon with 4 levels: .. code-block:: python hamiltonian_dict = { "h_str": ["v*np.pi*O0", "alpha*np.pi*O0*O0", "r*np.pi*X0||D0"], "qub": {"0": 4}, "vars": {"v": 2.1, "alpha": -0.33, "r": 0.02}, } The following example specifies a two transmon system, with single system terms specified using the summation format: .. code-block:: python hamiltonian_dict = { "h_str": [ "_SUM[i,0,1,wq{i}/2*(I{i}-Z{i})]", "_SUM[i,0,1,delta{i}/2*O{i}*O{i}]", "_SUM[i,0,1,-delta{i}/2*O{i}]", "_SUM[i,0,1,omegad{i}*X{i}||D{i}]", "jq0q1*Sp0*Sm1", "jq0q1*Sm0*Sp1", "omegad1*X0||U0", "omegad0*X1||U1" ], "qub": {"0": 4, "1": 4}, "vars": { "delta0": -2.111793476400394, "delta1": -2.0894421352015744, "jq0q1": 0.010495754104003914, "omegad0": 0.9715458990879812, "omegad1": 0.9803812537440838, "wq0": 32.517894442809514, "wq1": 33.0948996120196, }, } Args: hamiltonian_dict: Pulse backend Hamiltonian dictionary. subsystem_list: List of subsystems to include in the model. If ``None`` all are kept. Returns: Tuple: Model converted into concrete arrays - the static Hamiltonian, a list of Hamiltonians corresponding to different channels, a list of channel labels corresponding to the list of time-dependent Hamiltonians, and a dictionary with subsystem dimensions whose keys are the subsystem labels. """ # raise errors for invalid hamiltonian_dict _hamiltonian_pre_parse_exceptions(hamiltonian_dict) # get variables variables = OrderedDict() if "vars" in hamiltonian_dict: variables = OrderedDict(hamiltonian_dict["vars"]) # Get qubit subspace dimensions if subsystem_list is None: subsystem_list = [int(qubit) for qubit in hamiltonian_dict["qub"]] else: # if user supplied, make a copy and sort it subsystem_list = sorted(subsystem_list) # force keys in hamiltonian['qub'] to be ints qub_dict = {int(key): val for key, val in hamiltonian_dict["qub"].items()} subsystem_dims_dict = {int(qubit): qub_dict[int(qubit)] for qubit in subsystem_list} # Parse the Hamiltonian system = _regex_parser( operator_str=hamiltonian_dict["h_str"], subsystem_dims_dict=subsystem_dims_dict, subsystem_list=subsystem_list, ) # Extract which channels are associated with which Hamiltonian terms. # Assumes one channel appearing in each term appearing at the end. channels = [] for _, ham_str in system: chan_idx = None for c in CHANNEL_CHARS: # if c in ham_str, and all characters after are digits, treat # as channel if c in ham_str: if all(a.isdigit() for a in ham_str[ham_str.index(c) + 1 :]): chan_idx = ham_str.index(c) break if chan_idx is None: channels.append(None) else: channels.append(ham_str[chan_idx:]) # evaluate coefficients local_vars = {chan: 1.0 for chan in set(channels) if chan is not None} local_vars.update(variables) evaluated_ops = [] for op, coeff in system: # pylint: disable=exec-used exec(f"evaluated_coeff = {coeff}", globals(), local_vars) evaluated_ops.append(local_vars["evaluated_coeff"] * op) # merge terms based on channel static_hamiltonian = None hamiltonian_operators = [] reduced_channels = [] for channel, op in zip(channels, evaluated_ops): # if None, add it to the static hamiltonian if channel is None: if static_hamiltonian is None: static_hamiltonian = op else: static_hamiltonian += op else: channel = channel.lower() if channel in reduced_channels: hamiltonian_operators[reduced_channels.index(channel)] += op else: hamiltonian_operators.append(op) reduced_channels.append(channel) # sort channels/operators according to channel ordering if len(reduced_channels) > 0: reduced_channels, hamiltonian_operators = zip( *sorted(zip(reduced_channels, hamiltonian_operators)) ) return ( static_hamiltonian, list(hamiltonian_operators), list(reduced_channels), subsystem_dims_dict, )
def _hamiltonian_pre_parse_exceptions(hamiltonian_dict: dict): """Raises exceptions for improperly formatted or unsupported elements of hamiltonian dict specification. Parameters: hamiltonian_dict: Dictionary specification of hamiltonian. Returns: Raises: QiskitError: If some part of the Hamiltonian dictionary is unsupported or invalid. """ ham_str = hamiltonian_dict.get("h_str", []) if ham_str in ([], [""]): raise QiskitError("Hamiltonian dict requires a non-empty 'h_str' entry.") if hamiltonian_dict.get("qub", {}) == {}: raise QiskitError( "Hamiltonian dict requires non-empty 'qub' entry with subsystem dimensions." ) if hamiltonian_dict.get("osc", {}) != {}: raise QiskitError("Oscillator-type systems are not supported.") # verify that if terms in h_str have the divider ||, then the channels are in the valid format for term in hamiltonian_dict["h_str"]: malformed_text = f"""Term '{term}' does not conform to required string format. Channels may only be specified in the format 'aa||Cxx', where 'aa' specifies an operator, C is a valid channel character, and 'xx' is a string of digits.""" # if two vertical bars used together, check if channels in correct format if term.count("|") == 2 and term.count("||") == 1: # get the string reserved for channel channel_str = term[term.index("||") + 2 :] # if channel string is empty if len(channel_str) == 0: raise QiskitError(malformed_text) # if first entry in channel string isn't a valid channel character if channel_str[0] not in CHANNEL_CHARS: raise QiskitError(malformed_text) # Verify either that: all remaining characters are digits, or, # if term starts with _SUM[ and ends with ], all remaining characters # are either digits, or starts and ends with {} if term[-1] == "]" and len(term) > 5 and term[:5] == "_SUM[": # drop the closing ] channel_str = channel_str[:-1] # if channel string doesn't contain anything other than channel character if len(channel_str) == 1: raise QiskitError(malformed_text) # if starts with opening bracket, verify that it ends with closing bracket if channel_str[1] == "{": if not channel_str[-1] == "}": raise QiskitError(malformed_text) # otherwise verify that the remainder of terms only contains digits elif any(not c.isdigit() for c in channel_str[1:]): raise QiskitError(malformed_text) else: # if channel string doesn't contain anything other than channel character if len(channel_str) == 1: raise QiskitError(malformed_text) if any(not c.isdigit() for c in channel_str[1:]): raise QiskitError(malformed_text) # if bars present but not in correct format, raise error elif term.count("|") != 0: raise QiskitError(malformed_text)