From e8160a736da0f759c998d4f9b0caba6b8e79676e Mon Sep 17 00:00:00 2001 From: DanPuzzuoli Date: Tue, 22 Nov 2022 09:19:21 -0800 Subject: [PATCH 01/21] restarting PR branch --- .../backend/backend_parser/__init__.py | 19 + .../backend_parser/operator_from_string.py | 142 ++++++ .../backend/backend_parser/regex_parser.py | 369 +++++++++++++++ .../backend_parser/string_model_parser.py | 306 ++++++++++++ .../backend/test_operator_from_string.py | 103 +++++ .../backend/test_string_model_parser.py | 437 ++++++++++++++++++ 6 files changed, 1376 insertions(+) create mode 100644 qiskit_dynamics/backend/backend_parser/__init__.py create mode 100644 qiskit_dynamics/backend/backend_parser/operator_from_string.py create mode 100644 qiskit_dynamics/backend/backend_parser/regex_parser.py create mode 100644 qiskit_dynamics/backend/backend_parser/string_model_parser.py create mode 100644 test/dynamics/backend/test_operator_from_string.py create mode 100644 test/dynamics/backend/test_string_model_parser.py diff --git a/qiskit_dynamics/backend/backend_parser/__init__.py b/qiskit_dynamics/backend/backend_parser/__init__.py new file mode 100644 index 000000000..d6dde3096 --- /dev/null +++ b/qiskit_dynamics/backend/backend_parser/__init__.py @@ -0,0 +1,19 @@ +# -*- coding: utf-8 -*- + +# 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. + +""" +String model parser functionality. + +This module is meant for internal use and may be changed at any point. +""" diff --git a/qiskit_dynamics/backend/backend_parser/operator_from_string.py b/qiskit_dynamics/backend/backend_parser/operator_from_string.py new file mode 100644 index 000000000..fff05af08 --- /dev/null +++ b/qiskit_dynamics/backend/backend_parser/operator_from_string.py @@ -0,0 +1,142 @@ +# -*- coding: utf-8 -*- + +# 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 + +"""Generate operators from string. + +This file is meant for internal use and may be changed at any point. +""" + +from typing import Dict +import numpy as np + +from qiskit import QiskitError +import qiskit.quantum_info as qi + + +def operator_from_string( + op_label: str, subsystem_label: int, subsystem_dims: Dict[int, int] +) -> np.ndarray: + r"""Generates a dense operator acting on a single subsystem, tensoring + identities for remaining subsystems. + + The single system operator is specified via a string in ``op_label``, + the list of subsystems and their corresponding dimensions are specified in the + dictionary ``subsystem_dims``, with system label being the keys specified as ``int``s, + and system dimensions the values also specified as ``int``s, and ``subsystem_label`` + indicates which subsystem the operator specified by ``op_label`` acts on. + + Accepted ``op_labels`` 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 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. + + Note that the ordering of tensor factors is reversed. + + Args: + op_label: The string labelling the single system operator. + subsystem_label: Index of the subsystem to apply the operator. + subsystem_dims: Dictionary of subsystem labels and dimensions. + + Returns: + np.ndarray corresponding to the specified operator. + + Raises: + QiskitError: If op_label is invalid. + """ + + # construct single system operator + op_func = __operdict.get(op_label, None) + if op_func is None: + raise QiskitError(f"String {op_label} does not correspond to a known operator.") + + dim = subsystem_dims[subsystem_label] + out = qi.Operator(op_func(dim), input_dims=[dim], output_dims=[dim]) + + # sort subsystem labels and dimensions according to subsystem label + sorted_subsystem_keys, sorted_subsystem_dims = zip( + *sorted(zip(subsystem_dims.keys(), subsystem_dims.values())) + ) + + # get subsystem location in ordered list + subsystem_location = sorted_subsystem_keys.index(subsystem_label) + + # construct full operator + return qi.ScalarOp(sorted_subsystem_dims).compose(out, [subsystem_location]).data + + +# functions for generating individual operators +def a(dim: int) -> np.ndarray: + """Annihilation operator.""" + return np.diag(np.sqrt(np.arange(1, dim, dtype=complex)), 1) + + +def adag(dim: int) -> np.ndarray: + """Creation operator.""" + return a(dim).conj().transpose() + + +def N(dim: int) -> np.ndarray: + """Number operator.""" + return np.diag(np.arange(dim, dtype=complex)) + + +def X(dim: int) -> np.ndarray: + """Generalized X operator, written in terms of raising and lowering operators.""" + return a(dim) + adag(dim) + + +def Y(dim: int) -> np.ndarray: + """Generalized Y operator, written in terms of raising and lowering operators.""" + return -1j * (a(dim) - adag(dim)) + + +def Z(dim: int) -> np.ndarray: + """Generalized Z operator, written as id - 2 * N.""" + return ident(dim) - 2 * N(dim) + + +def ident(dim: int) -> np.ndarray: + """Identity operator.""" + return np.eye(dim, dtype=complex) + + +# operator names +__operdict = { + "X": X, + "Y": Y, + "Z": Z, + "a": a, + "A": a, + "Sm": a, + "Sp": adag, + "C": adag, + "N": N, + "O": N, + "I": ident, +} diff --git a/qiskit_dynamics/backend/backend_parser/regex_parser.py b/qiskit_dynamics/backend/backend_parser/regex_parser.py new file mode 100644 index 000000000..5b0eef254 --- /dev/null +++ b/qiskit_dynamics/backend/backend_parser/regex_parser.py @@ -0,0 +1,369 @@ +# -*- coding: utf-8 -*- + +# This code is part of Qiskit. +# +# (C) Copyright IBM 2018, 2019, 2020, 2021, 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 + +"""Legacy code for parsing operator strings. + +This file is meant for internal use and may be changed at any point. +""" + +import re +import copy +from typing import List, Dict, Tuple +from collections import namedtuple, OrderedDict + +import numpy as np + +from .operator_from_string import operator_from_string + + +def _regex_parser( + operator_str: List[str], subsystem_dims: Dict[int, int], subsystem_list: List[int] +) -> List[Tuple[np.array, str]]: + """Function wrapper for regex parsing object. + + Args: + operator_str: List of strings in accepted format as described in + string_model_parser.parse_hamiltonian_dict. + subsystem_dims: Dictionary mapping subsystem labels to dimensions. + subsystem_list: List of subsystems on which the operators are to be constructed. + Returns: + List of tuples containing pairs operators and their string coefficients. + """ + + return _HamiltonianParser(h_str=operator_str, subsystem_dims=subsystem_dims).parse( + subsystem_list + ) + + +class _HamiltonianParser: + """Legacy object for parsing string specifications of Hamiltonians.""" + + Token = namedtuple("Token", ("type", "name")) + + str_elements = OrderedDict( + QubOpr=re.compile(r"(?PO|Sp|Sm|X|Y|Z|I)(?P[0-9]+)"), + PrjOpr=re.compile(r"P(?P[0-9]+),(?P[0-9]+),(?P[0-9]+)"), + CavOpr=re.compile(r"(?PA|C|N)(?P[0-9]+)"), + Func=re.compile(r"(?P[a-z]+)\("), + Ext=re.compile(r"\.(?Pdag)"), + Var=re.compile(r"[a-z]+[0-9]*"), + Num=re.compile(r"[0-9.]+"), + MathOrd0=re.compile(r"[*/]"), + MathOrd1=re.compile(r"[+-]"), + BrkL=re.compile(r"\("), + BrkR=re.compile(r"\)"), + ) + + def __init__(self, h_str, subsystem_dims): + """Create new quantum operator generator + + Parameters: + h_str (list): list of Hamiltonian string + subsystem_dims (dict): dimension of subsystems + """ + self.h_str = h_str + self.subsystem_dims = {int(label): int(dim) for label, dim in subsystem_dims.items()} + self.str2qopr = {} + + def parse(self, qubit_list=None): + """Parse and generate Hamiltonian terms.""" + td_hams = [] + tc_hams = [] + + # expand sum + self._expand_sum() + + # convert to reverse Polish notation + for ham in self.h_str: + if len(re.findall(r"\|\|", ham)) > 1: + raise Exception(f"Multiple time-dependent terms in {ham}") + p_td = re.search(r"(?P[\S]+)\|\|(?P[\S]+)", ham) + + # find time-dependent term + if p_td: + coef, token = self._tokenizer(p_td.group("opr"), qubit_list) + if token is None: + continue + # combine coefficient to time-dependent term + if coef: + td = "*".join([coef, p_td.group("ch")]) + else: + td = p_td.group("ch") + token = self._shunting_yard(token) + _td = self._token2qobj(token), td + + td_hams.append(_td) + else: + coef, token = self._tokenizer(ham, qubit_list) + if token is None: + continue + token = self._shunting_yard(token) + + if (coef == "") or (coef is None): + coef = "1." + + _tc = self._token2qobj(token), coef + + tc_hams.append(_tc) + + return tc_hams + td_hams + + def _expand_sum(self): + """Takes a string-based Hamiltonian list and expands the _SUM action items out.""" + sum_str = re.compile(r"_SUM\[(?P[a-z]),(?P[a-z\d{}+-]+),(?P[a-z\d{}+-]+),") + brk_str = re.compile(r"]") + + ham_list = copy.copy(self.h_str) + ham_out = [] + + while any(ham_list): + ham = ham_list.pop(0) + p_sums = list(sum_str.finditer(ham)) + p_brks = list(brk_str.finditer(ham)) + if len(p_sums) != len(p_brks): + raise Exception(f"Missing correct number of brackets in {ham}") + + # find correct sum-bracket correspondence + if any(p_sums) == 0: + ham_out.append(ham) + else: + itr = p_sums[0].group("itr") + _l = int(p_sums[0].group("l")) + _u = int(p_sums[0].group("u")) + for ii in range(len(p_sums) - 1): + if p_sums[ii + 1].end() > p_brks[ii].start(): + break + else: + ii = len(p_sums) - 1 + + # substitute iterator value + _temp = [] + for kk in range(_l, _u + 1): + trg_s = ham[p_sums[0].end() : p_brks[ii].start()] + # generate replacement pattern + pattern = {} + for p in re.finditer(r"\{(?P[a-z0-9*/+-]+)\}", trg_s): + if p.group() not in pattern: + sub = parse_binop(p.group("op_str"), operands={itr: str(kk)}) + if sub.isdecimal(): + pattern[p.group()] = sub + else: + pattern[p.group()] = f"{{{sub}}}" + for key, val in pattern.items(): + trg_s = trg_s.replace(key, val) + _temp.append( + "".join([ham[: p_sums[0].start()], trg_s, ham[p_brks[ii].end() :]]) + ) + ham_list.extend(_temp) + + self.h_str = ham_out + + return ham_out + + def _tokenizer(self, op_str, qubit_list=None): + """Convert string to token and coefficient + Check if the index is in qubit_list + """ + + # generate token + _op_str = copy.copy(op_str) + token_list = [] + prev = "none" + while any(_op_str): + for key, parser in _HamiltonianParser.str_elements.items(): + p = parser.match(_op_str) + if p: + # find quantum operators + if key in ["QubOpr", "CavOpr"]: + _key = key + _name = p.group() + if p.group() not in self.str2qopr: + idx = int(p.group("idx")) + if qubit_list is not None and idx not in qubit_list: + return 0, None + name = p.group("opr") + opr = operator_from_string(name, idx, self.subsystem_dims) + self.str2qopr[p.group()] = opr + elif key == "PrjOpr": + _key = key + _name = p.group() + if p.group() not in self.str2qopr: + idx = int(p.group("idx")) + name = "P" + opr = operator_from_string(name, idx, self.subsystem_dims) + self.str2qopr[p.group()] = opr + elif key in ["Func", "Ext"]: + _name = p.group("name") + _key = key + elif key == "MathOrd1": + _name = p.group() + if prev not in ["QubOpr", "PrjOpr", "CavOpr", "Var", "Num"]: + _key = "MathUnitary" + else: + _key = key + else: + _name = p.group() + _key = key + token_list.append(_HamiltonianParser.Token(_key, _name)) + _op_str = _op_str[p.end() :] + prev = _key + break + else: + raise Exception(f"Invalid input string {op_str} is found") + + # split coefficient + coef = "" + if any(k.type == "Var" for k in token_list): + for ii, _ in enumerate(token_list): + if token_list[ii].name == "*": + if all(k.type != "Var" for k in token_list[ii + 1 :]): + coef = "".join([k.name for k in token_list[:ii]]) + token_list = token_list[ii + 1 :] + break + else: + raise Exception(f"Invalid order of operators and coefficients in {op_str}") + + return coef, token_list + + def _shunting_yard(self, token_list): + """Reformat token to reverse Polish notation""" + stack = [] + queue = [] + while any(token_list): + token = token_list.pop(0) + if token.type in ["QubOpr", "PrjOpr", "CavOpr", "Num"]: + queue.append(token) + elif token.type in ["Func", "Ext"]: + stack.append(token) + elif token.type in ["MathUnitary", "MathOrd0", "MathOrd1"]: + while stack and math_priority(token, stack[-1]): + queue.append(stack.pop(-1)) + stack.append(token) + elif token.type in ["BrkL"]: + stack.append(token) + elif token.type in ["BrkR"]: + while stack[-1].type not in ["BrkL", "Func"]: + queue.append(stack.pop(-1)) + if not any(stack): + raise Exception("Missing correct number of brackets") + pop = stack.pop(-1) + if pop.type == "Func": + queue.append(pop) + else: + raise Exception(f"Invalid token {token.name} is found") + + while any(stack): + queue.append(stack.pop(-1)) + + return queue + + def _token2qobj(self, tokens): + """Generate Hamiltonian term from tokens.""" + stack = [] + for token in tokens: + if token.type in ["QubOpr", "PrjOpr", "CavOpr"]: + stack.append(self.str2qopr[token.name]) + elif token.type == "Num": + stack.append(float(token.name)) + elif token.type in ["MathUnitary"]: + if token.name == "-": + stack.append(-stack.pop(-1)) + elif token.type in ["MathOrd0", "MathOrd1"]: + op2 = stack.pop(-1) + op1 = stack.pop(-1) + if token.name == "+": + stack.append(op1 + op2) + elif token.name == "-": + stack.append(op1 - op2) + elif token.name == "*": + stack.append(op1 @ op2) + elif token.name == "/": + stack.append(op1 / op2) + elif token.type in ["Func", "Ext"]: + if token.name == "dag": + stack.append(np.conjugate(np.transpose(stack.pop(-1)))) + else: + raise Exception(f"Invalid token {token.name} of type Func, Ext.") + else: + raise Exception(f"Invalid token {token.name} is found.") + + if len(stack) > 1: + raise Exception("Invalid mathematical operation in string.") + + return stack[0] + + +def math_priority(o1, o2): + """Check priority of given math operation""" + rank = {"MathUnitary": 2, "MathOrd0": 1, "MathOrd1": 0} + diff_ops = rank.get(o1.type, -1) - rank.get(o2.type, -1) + + if diff_ops > 0: + return False + else: + return True + + +# pylint: disable=dangerous-default-value +def parse_binop(op_str, operands={}, cast_str=True): + """Calculate binary operation in string format""" + oprs = OrderedDict( + sum=r"(?P[a-zA-Z0-9]+)\+(?P[a-zA-Z0-9]+)", + sub=r"(?P[a-zA-Z0-9]+)\-(?P[a-zA-Z0-9]+)", + mul=r"(?P[a-zA-Z0-9]+)\*(?P[a-zA-Z0-9]+)", + div=r"(?P[a-zA-Z0-9]+)\/(?P[a-zA-Z0-9]+)", + non=r"(?P[a-zA-Z0-9]+)", + ) + + for key, regr in oprs.items(): + p = re.match(regr, op_str) + if p: + val0 = operands.get(p.group("v0"), p.group("v0")) + if key == "non": + # substitution + retv = val0 + else: + val1 = operands.get(p.group("v1"), p.group("v1")) + # binary operation + if key == "sum": + if val0.isdecimal() and val1.isdecimal(): + retv = int(val0) + int(val1) + else: + retv = "+".join([str(val0), str(val1)]) + elif key == "sub": + if val0.isdecimal() and val1.isdecimal(): + retv = int(val0) - int(val1) + else: + retv = "-".join([str(val0), str(val1)]) + elif key == "mul": + if val0.isdecimal() and val1.isdecimal(): + retv = int(val0) * int(val1) + else: + retv = "*".join([str(val0), str(val1)]) + elif key == "div": + if val0.isdecimal() and val1.isdecimal(): + retv = int(val0) / int(val1) + else: + retv = "/".join([str(val0), str(val1)]) + else: + retv = 0 + break + else: + raise Exception(f"Invalid string {op_str}") + + if cast_str: + return str(retv) + else: + return retv diff --git a/qiskit_dynamics/backend/backend_parser/string_model_parser.py b/qiskit_dynamics/backend/backend_parser/string_model_parser.py new file mode 100644 index 000000000..ccfeaa247 --- /dev/null +++ b/qiskit_dynamics/backend/backend_parser/string_model_parser.py @@ -0,0 +1,306 @@ +# 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"] + + +def parse_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 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 = subsystem_list.copy() + subsystem_list.sort() + + # force keys in hamiltonian['qub'] to be ints + qub_dict = {int(key): val for key, val in hamiltonian_dict["qub"].items()} + + subsystem_dims = {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=subsystem_dims, + 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 + + +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) diff --git a/test/dynamics/backend/test_operator_from_string.py b/test/dynamics/backend/test_operator_from_string.py new file mode 100644 index 000000000..68f76402d --- /dev/null +++ b/test/dynamics/backend/test_operator_from_string.py @@ -0,0 +1,103 @@ +# 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 + +""" +Tests for pulse.operator_from_string. +""" + +import numpy as np + +from qiskit_dynamics.pulse.backend_parser.operator_from_string import operator_from_string +from ..common import QiskitDynamicsTestCase + + +class TestOperatorFromString(QiskitDynamicsTestCase): + """Test cases for operator_from_string.""" + + def test_correct_single_ops_dim2(self): + """Test that single operators give correct outputs.""" + + ident = np.eye(2) + a = np.array([[0.0, 1.0], [0.0, 0.0]]) + adag = a.conj().transpose() + N = np.diag(np.arange(2)) + X = a + adag + Y = -1j * (a - adag) + Z = ident - 2 * N + + self.assertAllClose(operator_from_string("X", 0, {0: 2}), X) + self.assertAllClose(operator_from_string("Y", 0, {0: 2}), Y) + self.assertAllClose(operator_from_string("Z", 0, {0: 2}), Z) + self.assertAllClose(operator_from_string("a", 0, {0: 2}), a) + self.assertAllClose(operator_from_string("A", 0, {0: 2}), a) + self.assertAllClose(operator_from_string("Sm", 0, {0: 2}), a) + self.assertAllClose(operator_from_string("C", 0, {0: 2}), adag) + self.assertAllClose(operator_from_string("Sp", 0, {0: 2}), adag) + self.assertAllClose(operator_from_string("N", 0, {0: 2}), N) + self.assertAllClose(operator_from_string("O", 0, {0: 2}), N) + self.assertAllClose(operator_from_string("I", 0, {0: 2}), np.eye(2)) + + def test_correct_single_ops_dim4(self): + """Test that single operators give correct outputs.""" + + sq2 = np.sqrt(2) + sq3 = np.sqrt(3) + ident = np.eye(4) + a = np.array( + [[0.0, 1.0, 0.0, 0.0], [0.0, 0.0, sq2, 0.0], [0.0, 0.0, 0.0, sq3], [0.0, 0.0, 0.0, 0.0]] + ) + adag = a.conj().transpose() + N = np.diag(np.arange(4)) + X = a + adag + Y = -1j * (a - adag) + Z = ident - 2 * N + + self.assertAllClose(operator_from_string("X", 0, {0: 4}), X) + self.assertAllClose(operator_from_string("Y", 0, {0: 4}), Y) + self.assertAllClose(operator_from_string("Z", 0, {0: 4}), Z) + self.assertAllClose(operator_from_string("a", 0, {0: 4}), a) + self.assertAllClose(operator_from_string("A", 0, {0: 4}), a) + self.assertAllClose(operator_from_string("Sm", 0, {0: 4}), a) + self.assertAllClose(operator_from_string("C", 0, {0: 4}), adag) + self.assertAllClose(operator_from_string("Sp", 0, {0: 4}), adag) + self.assertAllClose(operator_from_string("N", 0, {0: 4}), N) + self.assertAllClose(operator_from_string("O", 0, {0: 4}), N) + self.assertAllClose(operator_from_string("I", 0, {0: 4}), ident) + + def test_ident_before(self): + """Test adding identity before the subsystem in question.""" + + out = operator_from_string("X", 2, {0: 4, 1: 2, 2: 2}) + expected = np.kron(np.array([[0.0, 1.0], [1.0, 0.0]]), np.eye(8)) + self.assertAllClose(out, expected) + + def test_ident_after(self): + """Test adding identity after the subsystem in question.""" + + out = operator_from_string("Z", 0, {0: 2, 1: 2, 2: 4}) + expected = np.kron(np.eye(8), np.array([[1.0, 0.0], [0.0, -1.0]])) + self.assertAllClose(out, expected) + + def test_ident_after_unordered(self): + """Test adding identity after the subsystem in question.""" + + out = operator_from_string("Z", 0, {2: 4, 0: 2, 1: 2}) + expected = np.kron(np.eye(8), np.array([[1.0, 0.0], [0.0, -1.0]])) + self.assertAllClose(out, expected) + + def test_ident_before_and_after(self): + """Test adding identity before and after the subsystem in question.""" + + out = operator_from_string("a", 1, {0: 2, 1: 2, 2: 4}) + expected = np.kron(np.kron(np.eye(4), np.array([[0.0, 1.0], [0.0, 0.0]])), np.eye(2)) + self.assertAllClose(out, expected) diff --git a/test/dynamics/backend/test_string_model_parser.py b/test/dynamics/backend/test_string_model_parser.py new file mode 100644 index 000000000..378d1938f --- /dev/null +++ b/test/dynamics/backend/test_string_model_parser.py @@ -0,0 +1,437 @@ +# 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 + +""" +Tests for pulse.string_model_parser for importing qiskit.pulse model string representation. +""" + +import numpy as np + +from qiskit import QiskitError +from qiskit.quantum_info.operators import Operator + +from qiskit_dynamics.pulse.backend_parser.string_model_parser import ( + parse_hamiltonian_dict, + hamiltonian_pre_parse_exceptions, +) + +from qiskit_dynamics.type_utils import to_array + +from ..common import QiskitDynamicsTestCase + + +class TestHamiltonianPreParseExceptions(QiskitDynamicsTestCase): + """Tests for preparse formatting exceptions.""" + + def test_no_h_str(self): + """Test no h_str empty raises error.""" + + with self.assertRaises(QiskitError) as qe: + hamiltonian_pre_parse_exceptions({}) + self.assertTrue("requires a non-empty 'h_str'" in str(qe.exception)) + + with self.assertRaises(QiskitError) as qe: + hamiltonian_pre_parse_exceptions({"h_str": []}) + self.assertTrue("requires a non-empty 'h_str'" in str(qe.exception)) + + with self.assertRaises(QiskitError) as qe: + hamiltonian_pre_parse_exceptions({"h_str": [""]}) + self.assertTrue("requires a non-empty 'h_str'" in str(qe.exception)) + + def test_empty_qub(self): + """Test qub dict empty raises error.""" + + with self.assertRaises(QiskitError) as qe: + hamiltonian_pre_parse_exceptions({"h_str": ["a * X0|||D0"]}) + self.assertTrue("requires non-empty 'qub'" in str(qe.exception)) + + def test_too_many_vertical_bars(self): + """Test that too many vertical bars raises error.""" + + with self.assertRaises(QiskitError) as qe: + hamiltonian_pre_parse_exceptions({"h_str": ["a * X0|||D0"], "qub": {0: 2}}) + self.assertTrue("does not conform" in str(qe.exception)) + + def test_single_vertical_bar(self): + """Test that only a single vertical bar raises error.""" + + with self.assertRaises(QiskitError) as qe: + hamiltonian_pre_parse_exceptions({"h_str": ["a * X0|D0"], "qub": {0: 2}}) + self.assertTrue("does not conform" in str(qe.exception)) + + def test_multiple_channel_error(self): + """Test multiple channels raises error.""" + + with self.assertRaises(QiskitError) as qe: + hamiltonian_pre_parse_exceptions({"h_str": ["a * X0||D0*D1"], "qub": {0: 2}}) + self.assertTrue("does not conform" in str(qe.exception)) + + def test_divider_no_channel(self): + """Test that divider with no channel raises error.""" + + with self.assertRaises(QiskitError) as qe: + hamiltonian_pre_parse_exceptions({"h_str": ["a * X0||"], "qub": {0: 2}}) + self.assertTrue("does not conform" in str(qe.exception)) + + with self.assertRaises(QiskitError) as qe: + hamiltonian_pre_parse_exceptions({"h_str": ["a * X0|"], "qub": {0: 2}}) + self.assertTrue("does not conform" in str(qe.exception)) + + def test_non_digit_after_channel(self): + """Test that everything after the D or U is an int.""" + + with self.assertRaises(QiskitError) as qe: + hamiltonian_pre_parse_exceptions({"h_str": ["a * X0||Da"], "qub": {0: 2}}) + self.assertTrue("does not conform" in str(qe.exception)) + + with self.assertRaises(QiskitError) as qe: + hamiltonian_pre_parse_exceptions({"h_str": ["a * X0||D1a"], "qub": {0: 2}}) + self.assertTrue("does not conform" in str(qe.exception)) + + def test_nothing_after_channel(self): + """Test that everything after the D or U is an int.""" + + with self.assertRaises(QiskitError) as qe: + hamiltonian_pre_parse_exceptions({"h_str": ["a * X0||U"], "qub": {0: 2}}) + self.assertTrue("does not conform" in str(qe.exception)) + + +class TestParseHamiltonianDict(QiskitDynamicsTestCase): + """Tests for parse_hamiltonian_dict.""" + + def setUp(self): + """Build operators.""" + self.X = Operator.from_label("X").data + self.Y = Operator.from_label("Y").data + self.Z = Operator.from_label("Z").data + + dim = 4 + self.a = np.array(np.diag(np.sqrt(range(1, dim)), k=1), dtype=complex) + self.adag = self.a.conj().transpose() + self.N = np.array(np.diag(range(dim)), dtype=complex) + + def test_only_static_terms(self): + """Test a basic system.""" + ham_dict = {"h_str": ["v*np.pi*Z0"], "qub": {"0": 2}, "vars": {"v": 2.1}} + + static_ham, ham_ops, channels, subsystem_dims = parse_hamiltonian_dict(ham_dict) + self.assertAllClose(static_ham, 2.1 * np.pi * self.Z) + self.assertTrue(not ham_ops) + self.assertTrue(not channels) + self.assertTrue(subsystem_dims == {0: 2}) + + def test_simple_single_q_system(self): + """Test a basic system.""" + ham_dict = { + "h_str": ["v*np.pi*Z0", "0.02*np.pi*X0||D0"], + "qub": {"0": 2}, + "vars": {"v": 2.1}, + } + + static_ham, ham_ops, channels, subsystem_dims = parse_hamiltonian_dict(ham_dict) + + self.assertAllClose(static_ham, 2.1 * np.pi * self.Z) + self.assertAllClose(to_array(ham_ops), [0.02 * np.pi * self.X]) + self.assertTrue(channels == ["d0"]) + self.assertTrue(subsystem_dims == {0: 2}) + + def test_simple_single_q_system_repeat_entries(self): + """Test merging of terms with same channel or no channel.""" + ham_dict = { + "h_str": ["v*np.pi*Z0", "0.02*np.pi*X0||D0", "v*np.pi*Z0", "0.02*np.pi*X0||D0"], + "qub": {"0": 2}, + "vars": {"v": 2.1}, + } + + static_ham, ham_ops, channels, subsystem_dims = parse_hamiltonian_dict(ham_dict) + + self.assertAllClose(static_ham, 2 * 2.1 * np.pi * self.Z) + self.assertAllClose(to_array(ham_ops), [2 * 0.02 * np.pi * self.X]) + self.assertTrue(channels == ["d0"]) + self.assertTrue(subsystem_dims == {0: 2}) + + def test_simple_single_q_system_repeat_entries_different_case(self): + """Test merging of terms with same channel or no channel, + with upper and lower case. + """ + ham_dict = { + "h_str": ["v*np.pi*Z0", "0.02*np.pi*X0||D0", "v*np.pi*Z0", "0.02*np.pi*X0||d0"], + "qub": {"0": 2}, + "vars": {"v": 2.1}, + } + + static_ham, ham_ops, channels, subsystem_dims = parse_hamiltonian_dict(ham_dict) + + self.assertAllClose(static_ham, 2 * 2.1 * np.pi * self.Z) + self.assertAllClose(to_array(ham_ops), [2 * 0.02 * np.pi * self.X]) + self.assertTrue(channels == ["d0"]) + self.assertTrue(subsystem_dims == {0: 2}) + + def test_simple_two_q_system(self): + """Test a two qubit system.""" + + ham_dict = { + "h_str": [ + "v0*np.pi*Z0", + "v1*np.pi*Z1", + "j*np.pi*X0*Y1", + "0.03*np.pi*X1||D1", + "0.02*np.pi*X0||D0", + ], + "qub": {"0": 2, "1": 2}, + "vars": {"v0": 2.1, "v1": 2.0, "j": 0.02}, + } + + static_ham, ham_ops, channels, subsystem_dims = parse_hamiltonian_dict(ham_dict) + + ident = np.eye(2) + self.assertAllClose( + static_ham, + 2.1 * np.pi * np.kron(ident, self.Z) + + 2.0 * np.pi * np.kron(self.Z, ident) + + 0.02 * np.pi * np.kron(self.Y, self.X), + ) + self.assertAllClose( + to_array(ham_ops), + [0.02 * np.pi * np.kron(ident, self.X), 0.03 * np.pi * np.kron(self.X, ident)], + ) + self.assertTrue(channels == ["d0", "d1"]) + self.assertTrue(subsystem_dims == {0: 2, 1: 2}) + + def test_simple_two_q_system_measurement_channel(self): + """Test a two qubit system with a measurement-labelled channel.""" + + ham_dict = { + "h_str": [ + "v0*np.pi*Z0", + "v1*np.pi*Z1", + "j*np.pi*X0*Y1", + "0.03*np.pi*X1||M1", + "0.02*np.pi*X0||D0", + ], + "qub": {"0": 2, "1": 2}, + "vars": {"v0": 2.1, "v1": 2.0, "j": 0.02}, + } + + static_ham, ham_ops, channels, subsystem_dims = parse_hamiltonian_dict(ham_dict) + + ident = np.eye(2) + self.assertAllClose( + static_ham, + 2.1 * np.pi * np.kron(ident, self.Z) + + 2.0 * np.pi * np.kron(self.Z, ident) + + 0.02 * np.pi * np.kron(self.Y, self.X), + ) + self.assertAllClose( + to_array(ham_ops), + [0.02 * np.pi * np.kron(ident, self.X), 0.03 * np.pi * np.kron(self.X, ident)], + ) + self.assertTrue(channels == ["d0", "m1"]) + self.assertTrue(subsystem_dims == {0: 2, 1: 2}) + + def test_single_oscillator_system(self): + """Test single oscillator system.""" + + ham_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}, + } + + static_ham, ham_ops, channels, subsystem_dims = parse_hamiltonian_dict(ham_dict) + + self.assertAllClose(static_ham, 2.1 * np.pi * self.N - 0.33 * np.pi * self.N * self.N) + self.assertAllClose(to_array(ham_ops), [0.02 * np.pi * (self.a + self.adag)]) + self.assertTrue(channels == ["d0"]) + self.assertTrue(subsystem_dims == {0: 4}) + + def test_two_oscillator_system(self): + """Test a two qubit system.""" + + ham_dict = { + "h_str": [ + "v0*np.pi*O0", + "alpha0*np.pi*O0*O0", + "v1*np.pi*O1", + "alpha1*np.pi*O1*O1", + "j*np.pi*X0*Y1", + "0.03*np.pi*X1||D1", + "0.02*np.pi*X0||D0", + ], + "qub": {"0": 4, "1": 4}, + "vars": {"v0": 2.1, "v1": 2.0, "alpha0": -0.33, "alpha1": -0.33, "j": 0.02}, + } + + static_ham, ham_ops, channels, subsystem_dims = parse_hamiltonian_dict(ham_dict) + + ident = np.eye(4) + + self.assertAllClose( + static_ham, + 2.1 * np.pi * np.kron(ident, self.N) + - 0.33 * np.pi * np.kron(ident, self.N * self.N) + + 2.0 * np.pi * np.kron(self.N, ident) + - 0.33 * np.pi * np.kron(self.N * self.N, ident) + + 0.02 * np.pi * np.kron(-1j * (self.a - self.adag), self.a + self.adag), + ) + self.assertAllClose( + to_array(ham_ops), + [ + 0.02 * np.pi * np.kron(ident, self.a + self.adag), + 0.03 * np.pi * np.kron(self.a + self.adag, ident), + ], + ) + self.assertTrue(channels == ["d0", "d1"]) + self.assertTrue(subsystem_dims == {0: 4, 1: 4}) + + def test_single_q_high_dim(self): + """Test single q system but higher dim.""" + ham_dict = { + "h_str": ["v*np.pi*Z0", "0.02*np.pi*X0||D0"], + "qub": {"0": 4}, + "vars": {"v": 2.1}, + } + + static_ham, ham_ops, channels, subsystem_dims = parse_hamiltonian_dict(ham_dict) + + self.assertAllClose(static_ham, 2.1 * np.pi * (np.eye(4) - 2 * self.N)) + self.assertAllClose(to_array(ham_ops), [0.02 * np.pi * (self.a + self.adag)]) + self.assertTrue(channels == ["d0"]) + self.assertTrue(subsystem_dims == {0: 4}) + + def test_dagger(self): + """Test correct parsing of dagger.""" + ham_dict = { + "h_str": ["v*np.pi*dag(A0)"], + "qub": {"0": 4}, + "vars": {"v": 2.1}, + } + + static_ham, _, _, _ = parse_hamiltonian_dict(ham_dict) + self.assertAllClose(static_ham, 2.1 * np.pi * self.adag) + + def test_5q_hamiltonian_reduced(self): + """Test case for complicated Hamiltonian with reductions.""" + ham_dict = { + "h_str": [ + "_SUM[i,0,4,wq{i}/2*(I{i}-Z{i})]", + "_SUM[i,0,4,delta{i}/2*O{i}*O{i}]", + "_SUM[i,0,4,-delta{i}/2*O{i}]", + "_SUM[i,0,4,omegad{i}*X{i}||D{i}]", + "jq1q2*Sp1*Sm2", + "jq1q2*Sm1*Sp2", + "jq3q4*Sp3*Sm4", + "jq3q4*Sm3*Sp4", + "jq0q1*Sp0*Sm1", + "jq0q1*Sm0*Sp1", + "jq2q3*Sp2*Sm3", + "jq2q3*Sm2*Sp3", + "omegad1*X0||U0", + "omegad0*X1||U1", + "omegad2*X1||U2", + "omegad1*X2||U3", + "omegad3*X2||U4", + "omegad4*X3||U6", + "omegad2*X3||U5", + "omegad3*X4||U7", + ], + "qub": {"0": 4, "1": 4, "2": 4, "3": 4, "4": 4}, + "vars": { + "delta0": -2.111793476400394, + "delta1": -2.0894421352015744, + "delta2": -2.1179183671068604, + "delta3": -2.0410045431261215, + "delta4": -2.1119885565086776, + "jq0q1": 0.010495754104003914, + "jq1q2": 0.010781715511200012, + "jq2q3": 0.008920779377814226, + "jq3q4": 0.008985191651087791, + "omegad0": 0.9715458990879812, + "omegad1": 0.9803812537440838, + "omegad2": 0.9494756077681784, + "omegad3": 0.9763998543087951, + "omegad4": 0.9829308019780478, + "wq0": 32.517894442809514, + "wq1": 33.0948996120196, + "wq2": 31.74518096417169, + "wq3": 30.51062025552735, + "wq4": 32.16082685025662, + }, + } + + ident = np.eye(4, dtype=complex) + X = self.a + self.adag + X0 = np.kron(ident, X) + X1 = np.kron(X, ident) + N0 = np.kron(ident, self.N) + N1 = np.kron(self.N, ident) + + # test case for subsystems [0, 1] + + w0 = ham_dict["vars"]["wq0"] + w1 = ham_dict["vars"]["wq1"] + delta0 = ham_dict["vars"]["delta0"] + delta1 = ham_dict["vars"]["delta1"] + j01 = ham_dict["vars"]["jq0q1"] + omegad0 = ham_dict["vars"]["omegad0"] + omegad1 = ham_dict["vars"]["omegad1"] + omegad2 = ham_dict["vars"]["omegad2"] + static_ham_expected = ( + w0 * N0 + + 0.5 * delta0 * (N0 @ N0 - N0) + + w1 * N1 + + 0.5 * delta1 * (N1 @ N1 - N1) + + j01 * (np.kron(self.a, self.adag) + np.kron(self.adag, self.a)) + ) + ham_ops_expected = np.array( + [omegad0 * X0, omegad1 * X1, omegad1 * X0, omegad0 * X1, omegad2 * X1] + ) + channels_expected = ["d0", "d1", "u0", "u1", "u2"] + + static_ham, ham_ops, channels, subsystem_dims = parse_hamiltonian_dict( + ham_dict, subsystem_list=[0, 1] + ) + self.assertAllClose(static_ham, static_ham_expected) + self.assertAllClose(ham_ops, ham_ops_expected) + self.assertTrue(channels == channels_expected) + self.assertTrue(subsystem_dims == {0: 4, 1: 4}) + + # test case for subsystems [3, 4] + + w3 = ham_dict["vars"]["wq3"] + w4 = ham_dict["vars"]["wq4"] + delta3 = ham_dict["vars"]["delta3"] + delta4 = ham_dict["vars"]["delta4"] + j34 = ham_dict["vars"]["jq3q4"] + omegad3 = ham_dict["vars"]["omegad3"] + omegad4 = ham_dict["vars"]["omegad4"] + omegad2 = ham_dict["vars"]["omegad2"] + static_ham_expected = ( + w3 * N0 + + 0.5 * delta3 * (N0 @ N0 - N0) + + w4 * N1 + + 0.5 * delta4 * (N1 @ N1 - N1) + + j34 * (np.kron(self.a, self.adag) + np.kron(self.adag, self.a)) + ) + ham_ops_expected = np.array( + [omegad3 * X0, omegad4 * X1, omegad2 * X0, omegad4 * X0, omegad3 * X1] + ) + channels_expected = ["d3", "d4", "u5", "u6", "u7"] + + static_ham, ham_ops, channels, subsystem_dims = parse_hamiltonian_dict( + ham_dict, subsystem_list=[3, 4] + ) + self.assertAllClose(static_ham, static_ham_expected) + self.assertAllClose(ham_ops, ham_ops_expected) + self.assertTrue(channels == channels_expected) + self.assertTrue(subsystem_dims == {3: 4, 4: 4}) From afb85b9f0a59024c5a9a936ffffdd04b318cb541 Mon Sep 17 00:00:00 2001 From: DanPuzzuoli Date: Tue, 22 Nov 2022 09:50:12 -0800 Subject: [PATCH 02/21] renaming and moving around files --- qiskit_dynamics/backend/__init__.py | 2 ++ .../__init__.py | 6 +++--- .../hamiltonian_string_parser.py} | 6 +++--- .../operator_from_string.py | 2 +- .../regex_parser.py | 6 +++--- .../backend/backend_string_parser/__init__.py | 15 +++++++++++++++ .../test_hamiltonian_string_parser.py} | 0 .../test_operator_from_string.py | 0 8 files changed, 27 insertions(+), 10 deletions(-) rename qiskit_dynamics/backend/{backend_parser => backend_string_parser}/__init__.py (82%) rename qiskit_dynamics/backend/{backend_parser/string_model_parser.py => backend_string_parser/hamiltonian_string_parser.py} (98%) rename qiskit_dynamics/backend/{backend_parser => backend_string_parser}/operator_from_string.py (99%) rename qiskit_dynamics/backend/{backend_parser => backend_string_parser}/regex_parser.py (98%) create mode 100644 test/dynamics/backend/backend_string_parser/__init__.py rename test/dynamics/backend/{test_string_model_parser.py => backend_string_parser/test_hamiltonian_string_parser.py} (100%) rename test/dynamics/backend/{ => backend_string_parser}/test_operator_from_string.py (100%) diff --git a/qiskit_dynamics/backend/__init__.py b/qiskit_dynamics/backend/__init__.py index 89e8ad671..7f7233342 100644 --- a/qiskit_dynamics/backend/__init__.py +++ b/qiskit_dynamics/backend/__init__.py @@ -27,6 +27,8 @@ DynamicsBackend default_experiment_result_function + parse_backend_hamiltonian_dict """ from .dynamics_backend import DynamicsBackend, default_experiment_result_function +from .backend_string_parser import parse_backend_hamiltonian_dict diff --git a/qiskit_dynamics/backend/backend_parser/__init__.py b/qiskit_dynamics/backend/backend_string_parser/__init__.py similarity index 82% rename from qiskit_dynamics/backend/backend_parser/__init__.py rename to qiskit_dynamics/backend/backend_string_parser/__init__.py index d6dde3096..137a1af95 100644 --- a/qiskit_dynamics/backend/backend_parser/__init__.py +++ b/qiskit_dynamics/backend/backend_string_parser/__init__.py @@ -13,7 +13,7 @@ # that they have been altered from the originals. """ -String model parser functionality. - -This module is meant for internal use and may be changed at any point. +Backend string parsing functionality. """ + +from hamiltonian_string_parser import parse_backend_hamiltonian_dict \ No newline at end of file diff --git a/qiskit_dynamics/backend/backend_parser/string_model_parser.py b/qiskit_dynamics/backend/backend_string_parser/hamiltonian_string_parser.py similarity index 98% rename from qiskit_dynamics/backend/backend_parser/string_model_parser.py rename to qiskit_dynamics/backend/backend_string_parser/hamiltonian_string_parser.py index ccfeaa247..872727fc3 100644 --- a/qiskit_dynamics/backend/backend_parser/string_model_parser.py +++ b/qiskit_dynamics/backend/backend_string_parser/hamiltonian_string_parser.py @@ -33,7 +33,7 @@ CHANNEL_CHARS = ["U", "D", "M", "A", "u", "d", "m", "a"] -def parse_hamiltonian_dict( +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 @@ -146,7 +146,7 @@ def parse_hamiltonian_dict( """ # raise errors for invalid hamiltonian_dict - hamiltonian_pre_parse_exceptions(hamiltonian_dict) + _hamiltonian_pre_parse_exceptions(hamiltonian_dict) # get variables variables = OrderedDict() @@ -231,7 +231,7 @@ def parse_hamiltonian_dict( return static_hamiltonian, list(hamiltonian_operators), list(reduced_channels), subsystem_dims -def hamiltonian_pre_parse_exceptions(hamiltonian_dict: dict): +def _hamiltonian_pre_parse_exceptions(hamiltonian_dict: dict): """Raises exceptions for improperly formatted or unsupported elements of hamiltonian dict specification. diff --git a/qiskit_dynamics/backend/backend_parser/operator_from_string.py b/qiskit_dynamics/backend/backend_string_parser/operator_from_string.py similarity index 99% rename from qiskit_dynamics/backend/backend_parser/operator_from_string.py rename to qiskit_dynamics/backend/backend_string_parser/operator_from_string.py index fff05af08..ff96c2008 100644 --- a/qiskit_dynamics/backend/backend_parser/operator_from_string.py +++ b/qiskit_dynamics/backend/backend_string_parser/operator_from_string.py @@ -25,7 +25,7 @@ import qiskit.quantum_info as qi -def operator_from_string( +def _operator_from_string( op_label: str, subsystem_label: int, subsystem_dims: Dict[int, int] ) -> np.ndarray: r"""Generates a dense operator acting on a single subsystem, tensoring diff --git a/qiskit_dynamics/backend/backend_parser/regex_parser.py b/qiskit_dynamics/backend/backend_string_parser/regex_parser.py similarity index 98% rename from qiskit_dynamics/backend/backend_parser/regex_parser.py rename to qiskit_dynamics/backend/backend_string_parser/regex_parser.py index 5b0eef254..b6b643ff8 100644 --- a/qiskit_dynamics/backend/backend_parser/regex_parser.py +++ b/qiskit_dynamics/backend/backend_string_parser/regex_parser.py @@ -25,7 +25,7 @@ import numpy as np -from .operator_from_string import operator_from_string +from .operator_from_string import _operator_from_string def _regex_parser( @@ -194,7 +194,7 @@ def _tokenizer(self, op_str, qubit_list=None): if qubit_list is not None and idx not in qubit_list: return 0, None name = p.group("opr") - opr = operator_from_string(name, idx, self.subsystem_dims) + opr = _operator_from_string(name, idx, self.subsystem_dims) self.str2qopr[p.group()] = opr elif key == "PrjOpr": _key = key @@ -202,7 +202,7 @@ def _tokenizer(self, op_str, qubit_list=None): if p.group() not in self.str2qopr: idx = int(p.group("idx")) name = "P" - opr = operator_from_string(name, idx, self.subsystem_dims) + opr = _operator_from_string(name, idx, self.subsystem_dims) self.str2qopr[p.group()] = opr elif key in ["Func", "Ext"]: _name = p.group("name") diff --git a/test/dynamics/backend/backend_string_parser/__init__.py b/test/dynamics/backend/backend_string_parser/__init__.py new file mode 100644 index 000000000..90d8c684e --- /dev/null +++ b/test/dynamics/backend/backend_string_parser/__init__.py @@ -0,0 +1,15 @@ +# 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. + +""" +Backend string parser tests. +""" diff --git a/test/dynamics/backend/test_string_model_parser.py b/test/dynamics/backend/backend_string_parser/test_hamiltonian_string_parser.py similarity index 100% rename from test/dynamics/backend/test_string_model_parser.py rename to test/dynamics/backend/backend_string_parser/test_hamiltonian_string_parser.py diff --git a/test/dynamics/backend/test_operator_from_string.py b/test/dynamics/backend/backend_string_parser/test_operator_from_string.py similarity index 100% rename from test/dynamics/backend/test_operator_from_string.py rename to test/dynamics/backend/backend_string_parser/test_operator_from_string.py From 90cac0060dc970bc4f5ed5951e106b2a5b4457e0 Mon Sep 17 00:00:00 2001 From: DanPuzzuoli Date: Tue, 22 Nov 2022 09:56:53 -0800 Subject: [PATCH 03/21] getting tests to work again --- .../backend/backend_string_parser/__init__.py | 2 +- .../test_hamiltonian_string_parser.py | 58 +++++++++---------- .../test_operator_from_string.py | 58 +++++++++---------- 3 files changed, 59 insertions(+), 59 deletions(-) diff --git a/qiskit_dynamics/backend/backend_string_parser/__init__.py b/qiskit_dynamics/backend/backend_string_parser/__init__.py index 137a1af95..5c0fc54e4 100644 --- a/qiskit_dynamics/backend/backend_string_parser/__init__.py +++ b/qiskit_dynamics/backend/backend_string_parser/__init__.py @@ -16,4 +16,4 @@ Backend string parsing functionality. """ -from hamiltonian_string_parser import parse_backend_hamiltonian_dict \ No newline at end of file +from .hamiltonian_string_parser import parse_backend_hamiltonian_dict \ No newline at end of file diff --git a/test/dynamics/backend/backend_string_parser/test_hamiltonian_string_parser.py b/test/dynamics/backend/backend_string_parser/test_hamiltonian_string_parser.py index 378d1938f..4cab67ae9 100644 --- a/test/dynamics/backend/backend_string_parser/test_hamiltonian_string_parser.py +++ b/test/dynamics/backend/backend_string_parser/test_hamiltonian_string_parser.py @@ -20,14 +20,14 @@ from qiskit import QiskitError from qiskit.quantum_info.operators import Operator -from qiskit_dynamics.pulse.backend_parser.string_model_parser import ( - parse_hamiltonian_dict, - hamiltonian_pre_parse_exceptions, +from qiskit_dynamics.backend.backend_string_parser.hamiltonian_string_parser import ( + parse_backend_hamiltonian_dict, + _hamiltonian_pre_parse_exceptions, ) from qiskit_dynamics.type_utils import to_array -from ..common import QiskitDynamicsTestCase +from ...common import QiskitDynamicsTestCase class TestHamiltonianPreParseExceptions(QiskitDynamicsTestCase): @@ -37,77 +37,77 @@ def test_no_h_str(self): """Test no h_str empty raises error.""" with self.assertRaises(QiskitError) as qe: - hamiltonian_pre_parse_exceptions({}) + _hamiltonian_pre_parse_exceptions({}) self.assertTrue("requires a non-empty 'h_str'" in str(qe.exception)) with self.assertRaises(QiskitError) as qe: - hamiltonian_pre_parse_exceptions({"h_str": []}) + _hamiltonian_pre_parse_exceptions({"h_str": []}) self.assertTrue("requires a non-empty 'h_str'" in str(qe.exception)) with self.assertRaises(QiskitError) as qe: - hamiltonian_pre_parse_exceptions({"h_str": [""]}) + _hamiltonian_pre_parse_exceptions({"h_str": [""]}) self.assertTrue("requires a non-empty 'h_str'" in str(qe.exception)) def test_empty_qub(self): """Test qub dict empty raises error.""" with self.assertRaises(QiskitError) as qe: - hamiltonian_pre_parse_exceptions({"h_str": ["a * X0|||D0"]}) + _hamiltonian_pre_parse_exceptions({"h_str": ["a * X0|||D0"]}) self.assertTrue("requires non-empty 'qub'" in str(qe.exception)) def test_too_many_vertical_bars(self): """Test that too many vertical bars raises error.""" with self.assertRaises(QiskitError) as qe: - hamiltonian_pre_parse_exceptions({"h_str": ["a * X0|||D0"], "qub": {0: 2}}) + _hamiltonian_pre_parse_exceptions({"h_str": ["a * X0|||D0"], "qub": {0: 2}}) self.assertTrue("does not conform" in str(qe.exception)) def test_single_vertical_bar(self): """Test that only a single vertical bar raises error.""" with self.assertRaises(QiskitError) as qe: - hamiltonian_pre_parse_exceptions({"h_str": ["a * X0|D0"], "qub": {0: 2}}) + _hamiltonian_pre_parse_exceptions({"h_str": ["a * X0|D0"], "qub": {0: 2}}) self.assertTrue("does not conform" in str(qe.exception)) def test_multiple_channel_error(self): """Test multiple channels raises error.""" with self.assertRaises(QiskitError) as qe: - hamiltonian_pre_parse_exceptions({"h_str": ["a * X0||D0*D1"], "qub": {0: 2}}) + _hamiltonian_pre_parse_exceptions({"h_str": ["a * X0||D0*D1"], "qub": {0: 2}}) self.assertTrue("does not conform" in str(qe.exception)) def test_divider_no_channel(self): """Test that divider with no channel raises error.""" with self.assertRaises(QiskitError) as qe: - hamiltonian_pre_parse_exceptions({"h_str": ["a * X0||"], "qub": {0: 2}}) + _hamiltonian_pre_parse_exceptions({"h_str": ["a * X0||"], "qub": {0: 2}}) self.assertTrue("does not conform" in str(qe.exception)) with self.assertRaises(QiskitError) as qe: - hamiltonian_pre_parse_exceptions({"h_str": ["a * X0|"], "qub": {0: 2}}) + _hamiltonian_pre_parse_exceptions({"h_str": ["a * X0|"], "qub": {0: 2}}) self.assertTrue("does not conform" in str(qe.exception)) def test_non_digit_after_channel(self): """Test that everything after the D or U is an int.""" with self.assertRaises(QiskitError) as qe: - hamiltonian_pre_parse_exceptions({"h_str": ["a * X0||Da"], "qub": {0: 2}}) + _hamiltonian_pre_parse_exceptions({"h_str": ["a * X0||Da"], "qub": {0: 2}}) self.assertTrue("does not conform" in str(qe.exception)) with self.assertRaises(QiskitError) as qe: - hamiltonian_pre_parse_exceptions({"h_str": ["a * X0||D1a"], "qub": {0: 2}}) + _hamiltonian_pre_parse_exceptions({"h_str": ["a * X0||D1a"], "qub": {0: 2}}) self.assertTrue("does not conform" in str(qe.exception)) def test_nothing_after_channel(self): """Test that everything after the D or U is an int.""" with self.assertRaises(QiskitError) as qe: - hamiltonian_pre_parse_exceptions({"h_str": ["a * X0||U"], "qub": {0: 2}}) + _hamiltonian_pre_parse_exceptions({"h_str": ["a * X0||U"], "qub": {0: 2}}) self.assertTrue("does not conform" in str(qe.exception)) class TestParseHamiltonianDict(QiskitDynamicsTestCase): - """Tests for parse_hamiltonian_dict.""" + """Tests for parse_backend_hamiltonian_dict.""" def setUp(self): """Build operators.""" @@ -124,7 +124,7 @@ def test_only_static_terms(self): """Test a basic system.""" ham_dict = {"h_str": ["v*np.pi*Z0"], "qub": {"0": 2}, "vars": {"v": 2.1}} - static_ham, ham_ops, channels, subsystem_dims = parse_hamiltonian_dict(ham_dict) + static_ham, ham_ops, channels, subsystem_dims = parse_backend_hamiltonian_dict(ham_dict) self.assertAllClose(static_ham, 2.1 * np.pi * self.Z) self.assertTrue(not ham_ops) self.assertTrue(not channels) @@ -138,7 +138,7 @@ def test_simple_single_q_system(self): "vars": {"v": 2.1}, } - static_ham, ham_ops, channels, subsystem_dims = parse_hamiltonian_dict(ham_dict) + static_ham, ham_ops, channels, subsystem_dims = parse_backend_hamiltonian_dict(ham_dict) self.assertAllClose(static_ham, 2.1 * np.pi * self.Z) self.assertAllClose(to_array(ham_ops), [0.02 * np.pi * self.X]) @@ -153,7 +153,7 @@ def test_simple_single_q_system_repeat_entries(self): "vars": {"v": 2.1}, } - static_ham, ham_ops, channels, subsystem_dims = parse_hamiltonian_dict(ham_dict) + static_ham, ham_ops, channels, subsystem_dims = parse_backend_hamiltonian_dict(ham_dict) self.assertAllClose(static_ham, 2 * 2.1 * np.pi * self.Z) self.assertAllClose(to_array(ham_ops), [2 * 0.02 * np.pi * self.X]) @@ -170,7 +170,7 @@ def test_simple_single_q_system_repeat_entries_different_case(self): "vars": {"v": 2.1}, } - static_ham, ham_ops, channels, subsystem_dims = parse_hamiltonian_dict(ham_dict) + static_ham, ham_ops, channels, subsystem_dims = parse_backend_hamiltonian_dict(ham_dict) self.assertAllClose(static_ham, 2 * 2.1 * np.pi * self.Z) self.assertAllClose(to_array(ham_ops), [2 * 0.02 * np.pi * self.X]) @@ -192,7 +192,7 @@ def test_simple_two_q_system(self): "vars": {"v0": 2.1, "v1": 2.0, "j": 0.02}, } - static_ham, ham_ops, channels, subsystem_dims = parse_hamiltonian_dict(ham_dict) + static_ham, ham_ops, channels, subsystem_dims = parse_backend_hamiltonian_dict(ham_dict) ident = np.eye(2) self.assertAllClose( @@ -223,7 +223,7 @@ def test_simple_two_q_system_measurement_channel(self): "vars": {"v0": 2.1, "v1": 2.0, "j": 0.02}, } - static_ham, ham_ops, channels, subsystem_dims = parse_hamiltonian_dict(ham_dict) + static_ham, ham_ops, channels, subsystem_dims = parse_backend_hamiltonian_dict(ham_dict) ident = np.eye(2) self.assertAllClose( @@ -248,7 +248,7 @@ def test_single_oscillator_system(self): "vars": {"v": 2.1, "alpha": -0.33, "r": 0.02}, } - static_ham, ham_ops, channels, subsystem_dims = parse_hamiltonian_dict(ham_dict) + static_ham, ham_ops, channels, subsystem_dims = parse_backend_hamiltonian_dict(ham_dict) self.assertAllClose(static_ham, 2.1 * np.pi * self.N - 0.33 * np.pi * self.N * self.N) self.assertAllClose(to_array(ham_ops), [0.02 * np.pi * (self.a + self.adag)]) @@ -272,7 +272,7 @@ def test_two_oscillator_system(self): "vars": {"v0": 2.1, "v1": 2.0, "alpha0": -0.33, "alpha1": -0.33, "j": 0.02}, } - static_ham, ham_ops, channels, subsystem_dims = parse_hamiltonian_dict(ham_dict) + static_ham, ham_ops, channels, subsystem_dims = parse_backend_hamiltonian_dict(ham_dict) ident = np.eye(4) @@ -302,7 +302,7 @@ def test_single_q_high_dim(self): "vars": {"v": 2.1}, } - static_ham, ham_ops, channels, subsystem_dims = parse_hamiltonian_dict(ham_dict) + static_ham, ham_ops, channels, subsystem_dims = parse_backend_hamiltonian_dict(ham_dict) self.assertAllClose(static_ham, 2.1 * np.pi * (np.eye(4) - 2 * self.N)) self.assertAllClose(to_array(ham_ops), [0.02 * np.pi * (self.a + self.adag)]) @@ -317,7 +317,7 @@ def test_dagger(self): "vars": {"v": 2.1}, } - static_ham, _, _, _ = parse_hamiltonian_dict(ham_dict) + static_ham, _, _, _ = parse_backend_hamiltonian_dict(ham_dict) self.assertAllClose(static_ham, 2.1 * np.pi * self.adag) def test_5q_hamiltonian_reduced(self): @@ -398,7 +398,7 @@ def test_5q_hamiltonian_reduced(self): ) channels_expected = ["d0", "d1", "u0", "u1", "u2"] - static_ham, ham_ops, channels, subsystem_dims = parse_hamiltonian_dict( + static_ham, ham_ops, channels, subsystem_dims = parse_backend_hamiltonian_dict( ham_dict, subsystem_list=[0, 1] ) self.assertAllClose(static_ham, static_ham_expected) @@ -428,7 +428,7 @@ def test_5q_hamiltonian_reduced(self): ) channels_expected = ["d3", "d4", "u5", "u6", "u7"] - static_ham, ham_ops, channels, subsystem_dims = parse_hamiltonian_dict( + static_ham, ham_ops, channels, subsystem_dims = parse_backend_hamiltonian_dict( ham_dict, subsystem_list=[3, 4] ) self.assertAllClose(static_ham, static_ham_expected) diff --git a/test/dynamics/backend/backend_string_parser/test_operator_from_string.py b/test/dynamics/backend/backend_string_parser/test_operator_from_string.py index 68f76402d..253134470 100644 --- a/test/dynamics/backend/backend_string_parser/test_operator_from_string.py +++ b/test/dynamics/backend/backend_string_parser/test_operator_from_string.py @@ -12,13 +12,13 @@ # pylint: disable=invalid-name """ -Tests for pulse.operator_from_string. +Tests for internal _operator_from_string function. """ import numpy as np -from qiskit_dynamics.pulse.backend_parser.operator_from_string import operator_from_string -from ..common import QiskitDynamicsTestCase +from qiskit_dynamics.backend.backend_string_parser.operator_from_string import _operator_from_string +from ...common import QiskitDynamicsTestCase class TestOperatorFromString(QiskitDynamicsTestCase): @@ -35,17 +35,17 @@ def test_correct_single_ops_dim2(self): Y = -1j * (a - adag) Z = ident - 2 * N - self.assertAllClose(operator_from_string("X", 0, {0: 2}), X) - self.assertAllClose(operator_from_string("Y", 0, {0: 2}), Y) - self.assertAllClose(operator_from_string("Z", 0, {0: 2}), Z) - self.assertAllClose(operator_from_string("a", 0, {0: 2}), a) - self.assertAllClose(operator_from_string("A", 0, {0: 2}), a) - self.assertAllClose(operator_from_string("Sm", 0, {0: 2}), a) - self.assertAllClose(operator_from_string("C", 0, {0: 2}), adag) - self.assertAllClose(operator_from_string("Sp", 0, {0: 2}), adag) - self.assertAllClose(operator_from_string("N", 0, {0: 2}), N) - self.assertAllClose(operator_from_string("O", 0, {0: 2}), N) - self.assertAllClose(operator_from_string("I", 0, {0: 2}), np.eye(2)) + self.assertAllClose(_operator_from_string("X", 0, {0: 2}), X) + self.assertAllClose(_operator_from_string("Y", 0, {0: 2}), Y) + self.assertAllClose(_operator_from_string("Z", 0, {0: 2}), Z) + self.assertAllClose(_operator_from_string("a", 0, {0: 2}), a) + self.assertAllClose(_operator_from_string("A", 0, {0: 2}), a) + self.assertAllClose(_operator_from_string("Sm", 0, {0: 2}), a) + self.assertAllClose(_operator_from_string("C", 0, {0: 2}), adag) + self.assertAllClose(_operator_from_string("Sp", 0, {0: 2}), adag) + self.assertAllClose(_operator_from_string("N", 0, {0: 2}), N) + self.assertAllClose(_operator_from_string("O", 0, {0: 2}), N) + self.assertAllClose(_operator_from_string("I", 0, {0: 2}), np.eye(2)) def test_correct_single_ops_dim4(self): """Test that single operators give correct outputs.""" @@ -62,42 +62,42 @@ def test_correct_single_ops_dim4(self): Y = -1j * (a - adag) Z = ident - 2 * N - self.assertAllClose(operator_from_string("X", 0, {0: 4}), X) - self.assertAllClose(operator_from_string("Y", 0, {0: 4}), Y) - self.assertAllClose(operator_from_string("Z", 0, {0: 4}), Z) - self.assertAllClose(operator_from_string("a", 0, {0: 4}), a) - self.assertAllClose(operator_from_string("A", 0, {0: 4}), a) - self.assertAllClose(operator_from_string("Sm", 0, {0: 4}), a) - self.assertAllClose(operator_from_string("C", 0, {0: 4}), adag) - self.assertAllClose(operator_from_string("Sp", 0, {0: 4}), adag) - self.assertAllClose(operator_from_string("N", 0, {0: 4}), N) - self.assertAllClose(operator_from_string("O", 0, {0: 4}), N) - self.assertAllClose(operator_from_string("I", 0, {0: 4}), ident) + self.assertAllClose(_operator_from_string("X", 0, {0: 4}), X) + self.assertAllClose(_operator_from_string("Y", 0, {0: 4}), Y) + self.assertAllClose(_operator_from_string("Z", 0, {0: 4}), Z) + self.assertAllClose(_operator_from_string("a", 0, {0: 4}), a) + self.assertAllClose(_operator_from_string("A", 0, {0: 4}), a) + self.assertAllClose(_operator_from_string("Sm", 0, {0: 4}), a) + self.assertAllClose(_operator_from_string("C", 0, {0: 4}), adag) + self.assertAllClose(_operator_from_string("Sp", 0, {0: 4}), adag) + self.assertAllClose(_operator_from_string("N", 0, {0: 4}), N) + self.assertAllClose(_operator_from_string("O", 0, {0: 4}), N) + self.assertAllClose(_operator_from_string("I", 0, {0: 4}), ident) def test_ident_before(self): """Test adding identity before the subsystem in question.""" - out = operator_from_string("X", 2, {0: 4, 1: 2, 2: 2}) + out = _operator_from_string("X", 2, {0: 4, 1: 2, 2: 2}) expected = np.kron(np.array([[0.0, 1.0], [1.0, 0.0]]), np.eye(8)) self.assertAllClose(out, expected) def test_ident_after(self): """Test adding identity after the subsystem in question.""" - out = operator_from_string("Z", 0, {0: 2, 1: 2, 2: 4}) + out = _operator_from_string("Z", 0, {0: 2, 1: 2, 2: 4}) expected = np.kron(np.eye(8), np.array([[1.0, 0.0], [0.0, -1.0]])) self.assertAllClose(out, expected) def test_ident_after_unordered(self): """Test adding identity after the subsystem in question.""" - out = operator_from_string("Z", 0, {2: 4, 0: 2, 1: 2}) + out = _operator_from_string("Z", 0, {2: 4, 0: 2, 1: 2}) expected = np.kron(np.eye(8), np.array([[1.0, 0.0], [0.0, -1.0]])) self.assertAllClose(out, expected) def test_ident_before_and_after(self): """Test adding identity before and after the subsystem in question.""" - out = operator_from_string("a", 1, {0: 2, 1: 2, 2: 4}) + out = _operator_from_string("a", 1, {0: 2, 1: 2, 2: 4}) expected = np.kron(np.kron(np.eye(4), np.array([[0.0, 1.0], [0.0, 0.0]])), np.eye(2)) self.assertAllClose(out, expected) From a2e45134a46f79ae3be375cf5d56e3ba415a65e2 Mon Sep 17 00:00:00 2001 From: DanPuzzuoli Date: Tue, 22 Nov 2022 09:58:22 -0800 Subject: [PATCH 04/21] modifying string parser tests --- .../test_hamiltonian_string_parser.py | 27 +++++++++---------- 1 file changed, 13 insertions(+), 14 deletions(-) diff --git a/test/dynamics/backend/backend_string_parser/test_hamiltonian_string_parser.py b/test/dynamics/backend/backend_string_parser/test_hamiltonian_string_parser.py index 4cab67ae9..5c8943987 100644 --- a/test/dynamics/backend/backend_string_parser/test_hamiltonian_string_parser.py +++ b/test/dynamics/backend/backend_string_parser/test_hamiltonian_string_parser.py @@ -22,7 +22,6 @@ from qiskit_dynamics.backend.backend_string_parser.hamiltonian_string_parser import ( parse_backend_hamiltonian_dict, - _hamiltonian_pre_parse_exceptions, ) from qiskit_dynamics.type_utils import to_array @@ -30,79 +29,79 @@ from ...common import QiskitDynamicsTestCase -class TestHamiltonianPreParseExceptions(QiskitDynamicsTestCase): +class TestHamiltonianParseExceptions(QiskitDynamicsTestCase): """Tests for preparse formatting exceptions.""" def test_no_h_str(self): """Test no h_str empty raises error.""" with self.assertRaises(QiskitError) as qe: - _hamiltonian_pre_parse_exceptions({}) + parse_backend_hamiltonian_dict({}) self.assertTrue("requires a non-empty 'h_str'" in str(qe.exception)) with self.assertRaises(QiskitError) as qe: - _hamiltonian_pre_parse_exceptions({"h_str": []}) + parse_backend_hamiltonian_dict({"h_str": []}) self.assertTrue("requires a non-empty 'h_str'" in str(qe.exception)) with self.assertRaises(QiskitError) as qe: - _hamiltonian_pre_parse_exceptions({"h_str": [""]}) + parse_backend_hamiltonian_dict({"h_str": [""]}) self.assertTrue("requires a non-empty 'h_str'" in str(qe.exception)) def test_empty_qub(self): """Test qub dict empty raises error.""" with self.assertRaises(QiskitError) as qe: - _hamiltonian_pre_parse_exceptions({"h_str": ["a * X0|||D0"]}) + parse_backend_hamiltonian_dict({"h_str": ["a * X0|||D0"]}) self.assertTrue("requires non-empty 'qub'" in str(qe.exception)) def test_too_many_vertical_bars(self): """Test that too many vertical bars raises error.""" with self.assertRaises(QiskitError) as qe: - _hamiltonian_pre_parse_exceptions({"h_str": ["a * X0|||D0"], "qub": {0: 2}}) + parse_backend_hamiltonian_dict({"h_str": ["a * X0|||D0"], "qub": {0: 2}}) self.assertTrue("does not conform" in str(qe.exception)) def test_single_vertical_bar(self): """Test that only a single vertical bar raises error.""" with self.assertRaises(QiskitError) as qe: - _hamiltonian_pre_parse_exceptions({"h_str": ["a * X0|D0"], "qub": {0: 2}}) + parse_backend_hamiltonian_dict({"h_str": ["a * X0|D0"], "qub": {0: 2}}) self.assertTrue("does not conform" in str(qe.exception)) def test_multiple_channel_error(self): """Test multiple channels raises error.""" with self.assertRaises(QiskitError) as qe: - _hamiltonian_pre_parse_exceptions({"h_str": ["a * X0||D0*D1"], "qub": {0: 2}}) + parse_backend_hamiltonian_dict({"h_str": ["a * X0||D0*D1"], "qub": {0: 2}}) self.assertTrue("does not conform" in str(qe.exception)) def test_divider_no_channel(self): """Test that divider with no channel raises error.""" with self.assertRaises(QiskitError) as qe: - _hamiltonian_pre_parse_exceptions({"h_str": ["a * X0||"], "qub": {0: 2}}) + parse_backend_hamiltonian_dict({"h_str": ["a * X0||"], "qub": {0: 2}}) self.assertTrue("does not conform" in str(qe.exception)) with self.assertRaises(QiskitError) as qe: - _hamiltonian_pre_parse_exceptions({"h_str": ["a * X0|"], "qub": {0: 2}}) + parse_backend_hamiltonian_dict({"h_str": ["a * X0|"], "qub": {0: 2}}) self.assertTrue("does not conform" in str(qe.exception)) def test_non_digit_after_channel(self): """Test that everything after the D or U is an int.""" with self.assertRaises(QiskitError) as qe: - _hamiltonian_pre_parse_exceptions({"h_str": ["a * X0||Da"], "qub": {0: 2}}) + parse_backend_hamiltonian_dict({"h_str": ["a * X0||Da"], "qub": {0: 2}}) self.assertTrue("does not conform" in str(qe.exception)) with self.assertRaises(QiskitError) as qe: - _hamiltonian_pre_parse_exceptions({"h_str": ["a * X0||D1a"], "qub": {0: 2}}) + parse_backend_hamiltonian_dict({"h_str": ["a * X0||D1a"], "qub": {0: 2}}) self.assertTrue("does not conform" in str(qe.exception)) def test_nothing_after_channel(self): """Test that everything after the D or U is an int.""" with self.assertRaises(QiskitError) as qe: - _hamiltonian_pre_parse_exceptions({"h_str": ["a * X0||U"], "qub": {0: 2}}) + parse_backend_hamiltonian_dict({"h_str": ["a * X0||U"], "qub": {0: 2}}) self.assertTrue("does not conform" in str(qe.exception)) From c912b7e0094165c009874ac6fff957999b7cca47 Mon Sep 17 00:00:00 2001 From: DanPuzzuoli Date: Tue, 22 Nov 2022 14:13:34 -0800 Subject: [PATCH 05/21] initial success actually building a single qubit backend --- .../backend/backend_string_parser/__init__.py | 2 +- .../hamiltonian_string_parser.py | 5 +- qiskit_dynamics/backend/dynamics_backend.py | 97 ++++++++++++++++++- 3 files changed, 99 insertions(+), 5 deletions(-) diff --git a/qiskit_dynamics/backend/backend_string_parser/__init__.py b/qiskit_dynamics/backend/backend_string_parser/__init__.py index 5c0fc54e4..9e7d46f72 100644 --- a/qiskit_dynamics/backend/backend_string_parser/__init__.py +++ b/qiskit_dynamics/backend/backend_string_parser/__init__.py @@ -16,4 +16,4 @@ Backend string parsing functionality. """ -from .hamiltonian_string_parser import parse_backend_hamiltonian_dict \ No newline at end of file +from .hamiltonian_string_parser import parse_backend_hamiltonian_dict diff --git a/qiskit_dynamics/backend/backend_string_parser/hamiltonian_string_parser.py b/qiskit_dynamics/backend/backend_string_parser/hamiltonian_string_parser.py index 872727fc3..9e94deaba 100644 --- a/qiskit_dynamics/backend/backend_string_parser/hamiltonian_string_parser.py +++ b/qiskit_dynamics/backend/backend_string_parser/hamiltonian_string_parser.py @@ -45,7 +45,7 @@ def parse_backend_hamiltonian_dict( * ``'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 + * ``'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. @@ -158,8 +158,7 @@ def parse_backend_hamiltonian_dict( subsystem_list = [int(qubit) for qubit in hamiltonian_dict["qub"]] else: # if user supplied, make a copy and sort it - subsystem_list = subsystem_list.copy() - subsystem_list.sort() + 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()} diff --git a/qiskit_dynamics/backend/dynamics_backend.py b/qiskit_dynamics/backend/dynamics_backend.py index cc9ba18bb..4b1874a84 100644 --- a/qiskit_dynamics/backend/dynamics_backend.py +++ b/qiskit_dynamics/backend/dynamics_backend.py @@ -32,7 +32,7 @@ from qiskit.pulse import Schedule, ScheduleBlock from qiskit.pulse.transforms.canonicalization import block_to_schedule from qiskit.providers.options import Options -from qiskit.providers.backend import BackendV2 +from qiskit.providers.backend import BackendV1, BackendV2 from qiskit.result import Result from qiskit.result.models import ExperimentResult, ExperimentResultData @@ -50,6 +50,7 @@ _sample_probability_dict, _get_counts_from_samples, ) +from .backend_string_parser import parse_backend_hamiltonian_dict class DynamicsBackend(BackendV2): @@ -351,6 +352,100 @@ def target(self) -> Target: def meas_map(self) -> List[List[int]]: return self.options.meas_map + @classmethod + def from_backend( + cls, + backend: Union[BackendV1, BackendV2], + subsystem_list: Optional[List[int]] = None, + solver_kwargs: Optional[dict] = None, + **options, + ) -> "DynamicsBackend": + """Construct a :class:`DynamicsBackend` instance from an existing backend instance. + + To do: + - Add validation of backend and subsystem_list. E.g. is it a pulse backend? Does + it have the properties we need? + - Maybe we need to also implement configuration(), properties(), and defaults() + for backwards compatibility + - How do we handle solver kwargs? Do we expose a couple of options, or just give a + generic solver_kwargs that allows a user to pass anything through? + - Bring in target? What else? + - Do we want to scale operators/time to be close to 1? This will be kind of a pain but + may be very useful numerically. Could maybe have an optional argument to this method + for whether to do this or not. + - One annoyance with this is we will need to have a different dt in the solver + than is returned by backend.configuration().dt. Maybe this is fine? + - Get configuration, properties, target, ... ? + + """ + + # validation + # to do: + # need to validate that it's a pulse default? Or can we just check that + # it has qubit_freq_est? + if not hasattr(backend, 'configuration'): + raise QiskitError('DynamicsBackend.from_backend requires that the backend argument have a configuration attribute.') + if not hasattr(backend, 'defaults'): + raise QiskitError('DynamicsBackend.from_backend requires that the backend argument have a defaults attribute.') + + config = backend.configuration() + defaults = backend.defaults() + + + # get and parse Hamiltonian string dictionary + if subsystem_list is not None: + subsystem_list = sorted(subsystem_list) + else: + subsystem_list = list(range(config.n_qubits)) + + + hamiltonian_dict = config.hamiltonian + ( + static_hamiltonian, + hamiltonian_operators, + hamiltonian_channels, + subsystem_dims, + ) = parse_backend_hamiltonian_dict(hamiltonian_dict, subsystem_list) + + # Question: could change the output of above function to be this list instead of the dict + subsystem_dims = [subsystem_dims[idx] for idx in subsystem_list] + + # get time step size + dt = config.dt + + # get the channel frequencies as qubit_freq_est + # to do: + # get control channel freqs - from where? + # there may also be other channels in hamiltonian_channels - how to check this? + channel_freqs = {f"d{idx}": defaults.qubit_freq_est[idx] for idx in subsystem_list} + + + # build the solver + ############################################################################################## + # scale args? + solver_kwargs = solver_kwargs or {} + solver = Solver( + static_hamiltonian=static_hamiltonian, + hamiltonian_operators=hamiltonian_operators, + hamiltonian_channels=hamiltonian_channels, + channel_carrier_freqs=channel_freqs, + dt=dt, + **solver_kwargs + ) + + # to do: modify target??? + target = None + if hasattr(backend, "target"): + target = backend.target + + return cls( + solver=solver, + target=target, + subsystem_labels=subsystem_list, + subsystem_dims=subsystem_dims, + **options + ) + def default_experiment_result_function( experiment_name: str, From fb3a35f855b1f2fcccc4c5db3a8a69763f1d4f24 Mon Sep 17 00:00:00 2001 From: DanPuzzuoli Date: Thu, 24 Nov 2022 05:57:46 -0800 Subject: [PATCH 06/21] saving --- qiskit_dynamics/backend/dynamics_backend.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/qiskit_dynamics/backend/dynamics_backend.py b/qiskit_dynamics/backend/dynamics_backend.py index 4b1874a84..9a8825a70 100644 --- a/qiskit_dynamics/backend/dynamics_backend.py +++ b/qiskit_dynamics/backend/dynamics_backend.py @@ -376,13 +376,18 @@ def from_backend( - One annoyance with this is we will need to have a different dt in the solver than is returned by backend.configuration().dt. Maybe this is fine? - Get configuration, properties, target, ... ? + - Some configuration/properties/defaults parameters relate to things in the + solver (dt, channel frequencies, anything else?). How do we handle a user + updating those things? These are things that a user may want to update + over time. What is the correct way to do this? """ # validation # to do: - # need to validate that it's a pulse default? Or can we just check that + # - need to validate that it's a pulse default? Or can we just check that # it has qubit_freq_est? + # - Validate that subsystem_list is non-empty/well-formed if not hasattr(backend, 'configuration'): raise QiskitError('DynamicsBackend.from_backend requires that the backend argument have a configuration attribute.') if not hasattr(backend, 'defaults'): From 4a9dfc336ce19294a3a30b8a191900b91133da4f Mon Sep 17 00:00:00 2001 From: DanPuzzuoli Date: Thu, 24 Nov 2022 14:12:31 -0800 Subject: [PATCH 07/21] adding function for extracting channel frequencies, along with tests --- qiskit_dynamics/backend/dynamics_backend.py | 141 +++++++++++++----- .../dynamics/backend/test_dynamics_backend.py | 77 ++++++++++ 2 files changed, 177 insertions(+), 41 deletions(-) diff --git a/qiskit_dynamics/backend/dynamics_backend.py b/qiskit_dynamics/backend/dynamics_backend.py index 9a8825a70..35d44170f 100644 --- a/qiskit_dynamics/backend/dynamics_backend.py +++ b/qiskit_dynamics/backend/dynamics_backend.py @@ -20,7 +20,7 @@ import datetime import uuid -from typing import List, Optional, Union +from typing import List, Optional, Union, Dict import copy import numpy as np from scipy.integrate._ivp.ivp import OdeResult # pylint: disable=unused-import @@ -33,6 +33,8 @@ from qiskit.pulse.transforms.canonicalization import block_to_schedule from qiskit.providers.options import Options from qiskit.providers.backend import BackendV1, BackendV2 +from qiskit.providers.models.pulsedefaults import PulseDefaults +from qiskit.providers.models.backendconfiguration import PulseBackendConfiguration from qiskit.result import Result from qiskit.result.models import ExperimentResult, ExperimentResultData @@ -354,48 +356,56 @@ def meas_map(self) -> List[List[int]]: @classmethod def from_backend( - cls, - backend: Union[BackendV1, BackendV2], + cls, + backend: Union[BackendV1, BackendV2], subsystem_list: Optional[List[int]] = None, solver_kwargs: Optional[dict] = None, **options, ) -> "DynamicsBackend": """Construct a :class:`DynamicsBackend` instance from an existing backend instance. - + To do: - - Add validation of backend and subsystem_list. E.g. is it a pulse backend? Does - it have the properties we need? - - Maybe we need to also implement configuration(), properties(), and defaults() - for backwards compatibility + - Add validation of backend and subsystem_list. E.g. is it a pulse backend? Does it have + the properties we need? + - Maybe we need to also implement configuration(), properties(), and defaults() for + backwards compatibility - How do we handle solver kwargs? Do we expose a couple of options, or just give a generic solver_kwargs that allows a user to pass anything through? - Bring in target? What else? - Do we want to scale operators/time to be close to 1? This will be kind of a pain but may be very useful numerically. Could maybe have an optional argument to this method - for whether to do this or not. - - One annoyance with this is we will need to have a different dt in the solver - than is returned by backend.configuration().dt. Maybe this is fine? - - Get configuration, properties, target, ... ? - - Some configuration/properties/defaults parameters relate to things in the - solver (dt, channel frequencies, anything else?). How do we handle a user - updating those things? These are things that a user may want to update - over time. What is the correct way to do this? - + for whether to do this or not. - One annoyance with this is we will need to have a + different dt in the solver than is returned by backend.configuration().dt. Maybe this + is fine? + - Get configuration, properties, target, ... ? + - Some configuration/properties/defaults parameters relate to things in the solver (dt, + channel frequencies, anything else?). How do we handle a user updating those things? + These are things that a user may want to update over time. What is the correct way to + do this? + - Important technical note: I would like to scale numbers to be close to 1 (operators, + frequencies, times) but it would be a bit complicated to track everything. A very + simple numerical example seems to indicate that the "unscaled" versions still behave + fine. I think maybe for now we can leave it as is. + + """ # validation # to do: - # - need to validate that it's a pulse default? Or can we just check that + # - need to validate that it's a pulse default? Or can we just check that # it has qubit_freq_est? # - Validate that subsystem_list is non-empty/well-formed - if not hasattr(backend, 'configuration'): - raise QiskitError('DynamicsBackend.from_backend requires that the backend argument have a configuration attribute.') - if not hasattr(backend, 'defaults'): - raise QiskitError('DynamicsBackend.from_backend requires that the backend argument have a defaults attribute.') + if not hasattr(backend, "configuration"): + raise QiskitError( + "DynamicsBackend.from_backend requires that the backend argument have a configuration attribute." + ) + if not hasattr(backend, "defaults"): + raise QiskitError( + "DynamicsBackend.from_backend requires that the backend argument have a defaults attribute." + ) config = backend.configuration() defaults = backend.defaults() - # get and parse Hamiltonian string dictionary if subsystem_list is not None: @@ -403,7 +413,6 @@ def from_backend( else: subsystem_list = list(range(config.n_qubits)) - hamiltonian_dict = config.hamiltonian ( static_hamiltonian, @@ -418,16 +427,11 @@ def from_backend( # get time step size dt = config.dt - # get the channel frequencies as qubit_freq_est - # to do: - # get control channel freqs - from where? - # there may also be other channels in hamiltonian_channels - how to check this? - channel_freqs = {f"d{idx}": defaults.qubit_freq_est[idx] for idx in subsystem_list} - + channel_freqs = _get_backend_channel_freqs( + backend_config=config, backend_defaults=defaults, channels=hamiltonian_channels + ) # build the solver - ############################################################################################## - # scale args? solver_kwargs = solver_kwargs or {} solver = Solver( static_hamiltonian=static_hamiltonian, @@ -435,20 +439,18 @@ def from_backend( hamiltonian_channels=hamiltonian_channels, channel_carrier_freqs=channel_freqs, dt=dt, - **solver_kwargs + **solver_kwargs, ) # to do: modify target??? - target = None - if hasattr(backend, "target"): - target = backend.target + target = getattr(backend, "target", None) return cls( - solver=solver, - target=target, - subsystem_labels=subsystem_list, - subsystem_dims=subsystem_dims, - **options + solver=solver, + target=target, + subsystem_labels=subsystem_list, + subsystem_dims=subsystem_dims, + **options, ) @@ -627,3 +629,60 @@ def _to_schedule_list( else: raise QiskitError(f"Type {type(sched)} cannot be converted to Schedule.") return schedules, num_memslots + + +def _get_backend_channel_freqs( + backend_config: PulseBackendConfiguration, backend_defaults: PulseDefaults, channels: List[str] +) -> Dict[str, float]: + """Extract frequencies of channels from a backend configuration and defaults. + + Args: + backend_config: A backend configuration object. + backend_defaults: A backend defaults object. + channels: Channel labels given as strings, assumed to be unique. + + Returns: + Dict: Mapping of channel labels to frequencies. + + Raises: + QiskitError: If the frequency for one of the channels cannot be found. + """ + + channel_freqs = {} + + # get drive and measure channel frequencies + for channel in channels: + if channel[0] == "d": + idx = int(channel[1:]) + if idx > len(backend_defaults.qubit_freq_est): + raise QiskitError(f"DriveChannel index {idx} is out of bounds.") + channel_freqs[channel] = backend_defaults.qubit_freq_est[idx] + elif channel[0] == "m": + idx = int(channel[1:]) + if idx > len(backend_defaults.meas_freq_est): + raise QiskitError(f"MeasureChannel index {idx} is out of bounds.") + channel_freqs[channel] = backend_defaults.meas_freq_est[idx] + + # get u_channel_lo freqs if model requires them + if any("u" in x for x in channels): + + # raise error if no u channel specification present + if not hasattr(backend_config, "u_channel_lo"): + raise QiskitError("U Channels in model but configuration does not have u_channel_lo.") + + # populate u channel frequencies + for idx, u_channel_lo_factors in enumerate(backend_config.u_channel_lo): + u_channel = f"u{idx}" + if u_channel in channels: + freq = 0.0 + for u_channel_lo in u_channel_lo_factors: + freq += backend_defaults.qubit_freq_est[u_channel_lo.q] * u_channel_lo.scale + + channel_freqs[u_channel] = freq + + # validate that all channels have frequencies + for channel in channels: + if channel not in channel_freqs: + raise QiskitError(f"No carrier frequency found for channel {channel}.") + + return channel_freqs diff --git a/test/dynamics/backend/test_dynamics_backend.py b/test/dynamics/backend/test_dynamics_backend.py index 98a4ad8d9..1345f47ad 100644 --- a/test/dynamics/backend/test_dynamics_backend.py +++ b/test/dynamics/backend/test_dynamics_backend.py @@ -15,6 +15,8 @@ Test DynamicsBackend. """ +from types import SimpleNamespace + import numpy as np from scipy.integrate._ivp.ivp import OdeResult @@ -22,9 +24,11 @@ from qiskit.transpiler import Target from qiskit.quantum_info import Statevector, DensityMatrix from qiskit.result.models import ExperimentResult, ExperimentResultData +from qiskit.providers.models.backendconfiguration import UchannelLO from qiskit_dynamics import Solver, DynamicsBackend from qiskit_dynamics.backend import default_experiment_result_function +from qiskit_dynamics.backend.dynamics_backend import _get_backend_channel_freqs from ..common import QiskitDynamicsTestCase @@ -435,3 +439,76 @@ def test_simple_example(self): ) self.assertDictEqual(output.data.counts, {"000": 513, "010": 511}) + + +class Test_get_channel_backend_freqs(QiskitDynamicsTestCase): + """Test cases for _get_channel_backend_freqs.""" + + def setUp(self): + """Setup a simple configuration and default.""" + + defaults = SimpleNamespace() + defaults.qubit_freq_est = [0.343, 1.131, 2.1232, 3.3534, 4.123, 5.3532] + defaults.meas_freq_est = [0.23432, 1.543, 2.543, 3.543, 4.1321, 5.5433] + self.defaults = defaults + + config = SimpleNamespace() + config.u_channel_lo = [ + [UchannelLO(q=0, scale=1.0), UchannelLO(q=1, scale=-1.0)], + [UchannelLO(q=3, scale=2.1)], + [UchannelLO(q=4, scale=1.1), UchannelLO(q=2, scale=-1.1)], + ] + self.config = config + + def _test_with_setUp_example(self, channels, expected_output): + """Test with defaults and config from setUp.""" + self.assertDictEqual( + _get_backend_channel_freqs( + backend_config=self.config, backend_defaults=self.defaults, channels=channels + ), + expected_output, + ) + + def test_drive_channels(self): + """Test case with just drive channels.""" + channels = ["d0", "d1", "d2"] + expected_output = {f"d{idx}": self.defaults.qubit_freq_est[idx] for idx in range(3)} + self._test_with_setUp_example(channels=channels, expected_output=expected_output) + + def test_drive_and_meas_channels(self): + """Test case drive and meas channels.""" + channels = ["d0", "d1", "d2", "m0", "m3"] + expected_output = {f"d{idx}": self.defaults.qubit_freq_est[idx] for idx in range(3)} | { + f"m{idx}": self.defaults.meas_freq_est[idx] for idx in [0, 3] + } + self._test_with_setUp_example(channels=channels, expected_output=expected_output) + + def test_drive_and_u_channels(self): + """Test case drive and u channels.""" + channels = ["d0", "d1", "d2", "u1", "u2"] + expected_output = {f"d{idx}": self.defaults.qubit_freq_est[idx] for idx in range(3)} | { + "u1": 2.1 * self.defaults.qubit_freq_est[3], + "u2": 1.1 * self.defaults.qubit_freq_est[4] - 1.1 * self.defaults.qubit_freq_est[2] + } + self._test_with_setUp_example(channels=channels, expected_output=expected_output) + + def test_no_u_channel_lo_attribute_error(self): + """Test error if u_channel_lo attribute for config.""" + + with self.assertRaisesRegex(QiskitError, "configuration does not have"): + _get_backend_channel_freqs(backend_config=SimpleNamespace(), backend_defaults=self.defaults, channels=["u1"]) + + def test_missing_u_channel_error(self): + """Raise error if missing u channel.""" + with self.assertRaisesRegex(QiskitError, "No carrier frequency found"): + _get_backend_channel_freqs(backend_config=self.config, backend_defaults=self.defaults, channels=["u4"]) + + def test_drive_out_of_bounds(self): + """Raise error if drive channel index too high.""" + with self.assertRaisesRegex(QiskitError, "DriveChannel index 10"): + _get_backend_channel_freqs(backend_config=self.config, backend_defaults=self.defaults, channels=["d10"]) + + def test_meas_out_of_bounds(self): + """Raise error if drive channel index too high.""" + with self.assertRaisesRegex(QiskitError, "MeasureChannel index 10"): + _get_backend_channel_freqs(backend_config=self.config, backend_defaults=self.defaults, channels=["m10"]) \ No newline at end of file From 024537c78e48af991d654ca4ec28d21a15e54d95 Mon Sep 17 00:00:00 2001 From: DanPuzzuoli Date: Sat, 26 Nov 2022 12:34:27 -0800 Subject: [PATCH 08/21] fixing channel index bounds --- qiskit_dynamics/backend/dynamics_backend.py | 4 ++-- test/dynamics/backend/test_dynamics_backend.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/qiskit_dynamics/backend/dynamics_backend.py b/qiskit_dynamics/backend/dynamics_backend.py index 35d44170f..fb298886f 100644 --- a/qiskit_dynamics/backend/dynamics_backend.py +++ b/qiskit_dynamics/backend/dynamics_backend.py @@ -654,12 +654,12 @@ def _get_backend_channel_freqs( for channel in channels: if channel[0] == "d": idx = int(channel[1:]) - if idx > len(backend_defaults.qubit_freq_est): + if idx >= len(backend_defaults.qubit_freq_est): raise QiskitError(f"DriveChannel index {idx} is out of bounds.") channel_freqs[channel] = backend_defaults.qubit_freq_est[idx] elif channel[0] == "m": idx = int(channel[1:]) - if idx > len(backend_defaults.meas_freq_est): + if idx >= len(backend_defaults.meas_freq_est): raise QiskitError(f"MeasureChannel index {idx} is out of bounds.") channel_freqs[channel] = backend_defaults.meas_freq_est[idx] diff --git a/test/dynamics/backend/test_dynamics_backend.py b/test/dynamics/backend/test_dynamics_backend.py index 1345f47ad..cec66b92c 100644 --- a/test/dynamics/backend/test_dynamics_backend.py +++ b/test/dynamics/backend/test_dynamics_backend.py @@ -510,5 +510,5 @@ def test_drive_out_of_bounds(self): def test_meas_out_of_bounds(self): """Raise error if drive channel index too high.""" - with self.assertRaisesRegex(QiskitError, "MeasureChannel index 10"): - _get_backend_channel_freqs(backend_config=self.config, backend_defaults=self.defaults, channels=["m10"]) \ No newline at end of file + with self.assertRaisesRegex(QiskitError, "MeasureChannel index 6"): + _get_backend_channel_freqs(backend_config=self.config, backend_defaults=self.defaults, channels=["m6"]) \ No newline at end of file From 502afe56a1674a19bbcae470c6a409b161b1e462 Mon Sep 17 00:00:00 2001 From: DanPuzzuoli Date: Sun, 27 Nov 2022 08:56:35 -0800 Subject: [PATCH 09/21] adding further validation --- qiskit_dynamics/backend/dynamics_backend.py | 72 +++++++++++++------ .../dynamics/backend/test_dynamics_backend.py | 14 +++- 2 files changed, 62 insertions(+), 24 deletions(-) diff --git a/qiskit_dynamics/backend/dynamics_backend.py b/qiskit_dynamics/backend/dynamics_backend.py index fb298886f..ca91d1cbc 100644 --- a/qiskit_dynamics/backend/dynamics_backend.py +++ b/qiskit_dynamics/backend/dynamics_backend.py @@ -236,7 +236,7 @@ def run( Args: run_input: A list of simulations, specified by ``QuantumCircuit``, ``Schedule``, or - ``ScheduleBlock`` instances. + ``ScheduleBlock`` instances. validate: Whether or not to run validation checks on the input. **options: Additional run options to temporarily override current backend options. @@ -362,30 +362,49 @@ def from_backend( solver_kwargs: Optional[dict] = None, **options, ) -> "DynamicsBackend": - """Construct a :class:`DynamicsBackend` instance from an existing backend instance. + """Construct a :class:`.DynamicsBackend` instance from an existing ``Backend`` instance. + + The ``backend`` must have the ``configuration`` and ``defaults`` attributes. + The ``configuration`` must containing a Hamiltonian description, + step size ``dt``, number of qubits ``n_qubits``, and ``u_channel_lo`` (when control channels are present). The ``defaults`` must contain ``qubit_freq_est`` and ``meas_freq_est``. + + The optional argument ``subsystem_list`` specifies which subset of qubits will be + modelled in the constructed :class:`DynamicsBackend`, with all other qubits will being + dropped from the model. + + + Args: + backend: The ``Backend`` instance to build the :class:`.DynamicsBackend` from. + subsystem_list: The list of qubits in the backend to include in the model. + solver_kwargs: Additional keyword arguments to pass to the :class:`.Solver` instance + constructed from the model in the backend. + **options: Additional options to be applied in construction of the + :class:`.DynamicsBackend`. + + Returns: + DynamicsBackend + + Raises: + QiskitError if any required parameters are missing from the passed backend. + To do: - Add validation of backend and subsystem_list. E.g. is it a pulse backend? Does it have the properties we need? - Maybe we need to also implement configuration(), properties(), and defaults() for backwards compatibility - - How do we handle solver kwargs? Do we expose a couple of options, or just give a - generic solver_kwargs that allows a user to pass anything through? - Bring in target? What else? - - Do we want to scale operators/time to be close to 1? This will be kind of a pain but - may be very useful numerically. Could maybe have an optional argument to this method - for whether to do this or not. - One annoyance with this is we will need to have a - different dt in the solver than is returned by backend.configuration().dt. Maybe this - is fine? - Get configuration, properties, target, ... ? - - Some configuration/properties/defaults parameters relate to things in the solver (dt, - channel frequencies, anything else?). How do we handle a user updating those things? - These are things that a user may want to update over time. What is the correct way to - do this? - - Important technical note: I would like to scale numbers to be close to 1 (operators, - frequencies, times) but it would be a bit complicated to track everything. A very - simple numerical example seems to indicate that the "unscaled" versions still behave - fine. I think maybe for now we can leave it as is. + - Issue with solver_kwargs is the user may will not have access to the hamiltonian + terms, and hence can't effectively specify the rotating frame they want to be in. + What to do about this? Could add string handling, like rotating_frame='static_hamiltonian'? + - Can we somehow provide the user with "standard" configurations? E.g. if sparse, + it will automatically simulate in the diagonal of the static hamiltonian? + - The user can set the rotating frame of the model AFTER construction as well + (by getting it from the solver). + - Could maybe change arg to "solver_configuration", and it can either be a + string like "sparse", "dense", and the appropriate frame is automatically entered, + or it can be a dict and explicitly be passed as """ @@ -397,11 +416,12 @@ def from_backend( # - Validate that subsystem_list is non-empty/well-formed if not hasattr(backend, "configuration"): raise QiskitError( - "DynamicsBackend.from_backend requires that the backend argument have a configuration attribute." + """DynamicsBackend.from_backend requires that the backend argument have a + configuration attribute.""" ) if not hasattr(backend, "defaults"): raise QiskitError( - "DynamicsBackend.from_backend requires that the backend argument have a defaults attribute." + """DynamicsBackend.from_backend requires that the backend argument have a defaults attribute.""" ) config = backend.configuration() @@ -648,6 +668,16 @@ def _get_backend_channel_freqs( QiskitError: If the frequency for one of the channels cannot be found. """ + # validate required attributes are present + if any("d" in x for x in channels) and not hasattr(backend_defaults, "qubit_freq_est"): + raise QiskitError("DriveChannels in model but defaults does not have qubit_freq_est.") + + if any("m" in x for x in channels) and not hasattr(backend_defaults, "meas_freq_est"): + raise QiskitError("MeasureChannels in model but defaults does not have meas_freq_est.") + + if any("u" in x for x in channels) and not hasattr(backend_config, "u_channel_lo"): + raise QiskitError("U Channels in model but configuration does not have u_channel_lo.") + channel_freqs = {} # get drive and measure channel frequencies @@ -666,10 +696,6 @@ def _get_backend_channel_freqs( # get u_channel_lo freqs if model requires them if any("u" in x for x in channels): - # raise error if no u channel specification present - if not hasattr(backend_config, "u_channel_lo"): - raise QiskitError("U Channels in model but configuration does not have u_channel_lo.") - # populate u channel frequencies for idx, u_channel_lo_factors in enumerate(backend_config.u_channel_lo): u_channel = f"u{idx}" diff --git a/test/dynamics/backend/test_dynamics_backend.py b/test/dynamics/backend/test_dynamics_backend.py index cec66b92c..5a452bfef 100644 --- a/test/dynamics/backend/test_dynamics_backend.py +++ b/test/dynamics/backend/test_dynamics_backend.py @@ -493,11 +493,23 @@ def test_drive_and_u_channels(self): self._test_with_setUp_example(channels=channels, expected_output=expected_output) def test_no_u_channel_lo_attribute_error(self): - """Test error if u_channel_lo attribute for config.""" + """Test error if no u_channel_lo attribute for config.""" with self.assertRaisesRegex(QiskitError, "configuration does not have"): _get_backend_channel_freqs(backend_config=SimpleNamespace(), backend_defaults=self.defaults, channels=["u1"]) + def test_no_qubit_freq_est_attribute_error(self): + """Test error if no qubit_freq_est in defaults.""" + + with self.assertRaisesRegex(QiskitError, "defaults does not have"): + _get_backend_channel_freqs(backend_config=SimpleNamespace(), backend_defaults=SimpleNamespace(), channels=["d0"]) + + def test_no_meas_freq_est_attribute_error(self): + """Test error if no meas_freq_est in defaults.""" + + with self.assertRaisesRegex(QiskitError, "defaults does not have"): + _get_backend_channel_freqs(backend_config=SimpleNamespace(), backend_defaults=SimpleNamespace(), channels=["m0"]) + def test_missing_u_channel_error(self): """Raise error if missing u channel.""" with self.assertRaisesRegex(QiskitError, "No carrier frequency found"): From 99957e3ef4dfbfa098c3bd45c1cd8bc81c443963 Mon Sep 17 00:00:00 2001 From: DanPuzzuoli Date: Sun, 27 Nov 2022 09:08:09 -0800 Subject: [PATCH 10/21] restructuring channel frequency extraction --- qiskit_dynamics/backend/dynamics_backend.py | 70 +++++++++++-------- .../dynamics/backend/test_dynamics_backend.py | 8 ++- 2 files changed, 48 insertions(+), 30 deletions(-) diff --git a/qiskit_dynamics/backend/dynamics_backend.py b/qiskit_dynamics/backend/dynamics_backend.py index ca91d1cbc..a557bd885 100644 --- a/qiskit_dynamics/backend/dynamics_backend.py +++ b/qiskit_dynamics/backend/dynamics_backend.py @@ -668,43 +668,55 @@ def _get_backend_channel_freqs( QiskitError: If the frequency for one of the channels cannot be found. """ + # partition types of channels + drive_channels = [] + meas_channels = [] + u_channels = [] + + for channel in channels: + if channel[0] == 'd': + drive_channels.append(channel) + elif channel[0] == 'm': + meas_channels.append(channel) + elif channel[0] == 'u': + u_channels.append(channel) + else: + raise QiskitError("Unrecognized channel type requested.") + # validate required attributes are present - if any("d" in x for x in channels) and not hasattr(backend_defaults, "qubit_freq_est"): + if drive_channels and not hasattr(backend_defaults, "qubit_freq_est"): raise QiskitError("DriveChannels in model but defaults does not have qubit_freq_est.") - if any("m" in x for x in channels) and not hasattr(backend_defaults, "meas_freq_est"): + if meas_channels and not hasattr(backend_defaults, "meas_freq_est"): raise QiskitError("MeasureChannels in model but defaults does not have meas_freq_est.") - if any("u" in x for x in channels) and not hasattr(backend_config, "u_channel_lo"): - raise QiskitError("U Channels in model but configuration does not have u_channel_lo.") + if u_channels and not hasattr(backend_config, "u_channel_lo"): + raise QiskitError("ControlChannels in model but configuration does not have u_channel_lo.") + # populate frequencies channel_freqs = {} - # get drive and measure channel frequencies - for channel in channels: - if channel[0] == "d": - idx = int(channel[1:]) - if idx >= len(backend_defaults.qubit_freq_est): - raise QiskitError(f"DriveChannel index {idx} is out of bounds.") - channel_freqs[channel] = backend_defaults.qubit_freq_est[idx] - elif channel[0] == "m": - idx = int(channel[1:]) - if idx >= len(backend_defaults.meas_freq_est): - raise QiskitError(f"MeasureChannel index {idx} is out of bounds.") - channel_freqs[channel] = backend_defaults.meas_freq_est[idx] - - # get u_channel_lo freqs if model requires them - if any("u" in x for x in channels): - - # populate u channel frequencies - for idx, u_channel_lo_factors in enumerate(backend_config.u_channel_lo): - u_channel = f"u{idx}" - if u_channel in channels: - freq = 0.0 - for u_channel_lo in u_channel_lo_factors: - freq += backend_defaults.qubit_freq_est[u_channel_lo.q] * u_channel_lo.scale - - channel_freqs[u_channel] = freq + for channel in drive_channels: + idx = int(channel[1:]) + if idx >= len(backend_defaults.qubit_freq_est): + raise QiskitError(f"DriveChannel index {idx} is out of bounds.") + channel_freqs[channel] = backend_defaults.qubit_freq_est[idx] + + for channel in meas_channels: + idx = int(channel[1:]) + if idx >= len(backend_defaults.meas_freq_est): + raise QiskitError(f"MeasureChannel index {idx} is out of bounds.") + channel_freqs[channel] = backend_defaults.meas_freq_est[idx] + + for channel in u_channels: + idx = int(channel[1:]) + if idx >= len(backend_config.u_channel_lo): + raise QiskitError(f"ControlChannel index {idx} is out of bounds.") + freq = 0.0 + for u_channel_lo in backend_config.u_channel_lo[idx]: + freq += backend_defaults.qubit_freq_est[u_channel_lo.q] * u_channel_lo.scale + + channel_freqs[channel] = freq # validate that all channels have frequencies for channel in channels: diff --git a/test/dynamics/backend/test_dynamics_backend.py b/test/dynamics/backend/test_dynamics_backend.py index 5a452bfef..fd0d32525 100644 --- a/test/dynamics/backend/test_dynamics_backend.py +++ b/test/dynamics/backend/test_dynamics_backend.py @@ -492,6 +492,12 @@ def test_drive_and_u_channels(self): } self._test_with_setUp_example(channels=channels, expected_output=expected_output) + def test_unrecognized_channel_type(self): + """Test error is raised if unrecognized channel type.""" + + with self.assertRaisesRegex(QiskitError, "Unrecognized"): + _get_backend_channel_freqs(backend_config=SimpleNamespace(), backend_defaults=SimpleNamespace(), channels=["r1"]) + def test_no_u_channel_lo_attribute_error(self): """Test error if no u_channel_lo attribute for config.""" @@ -512,7 +518,7 @@ def test_no_meas_freq_est_attribute_error(self): def test_missing_u_channel_error(self): """Raise error if missing u channel.""" - with self.assertRaisesRegex(QiskitError, "No carrier frequency found"): + with self.assertRaisesRegex(QiskitError, "ControlChannel index 4"): _get_backend_channel_freqs(backend_config=self.config, backend_defaults=self.defaults, channels=["u4"]) def test_drive_out_of_bounds(self): From 2e02fc12a8f09f0104fd5398676d24bd10df339c Mon Sep 17 00:00:00 2001 From: DanPuzzuoli Date: Sun, 27 Nov 2022 10:17:53 -0800 Subject: [PATCH 11/21] adding validation for configuration and defaults --- qiskit_dynamics/backend/dynamics_backend.py | 95 +++++++++++++------ .../dynamics/backend/test_dynamics_backend.py | 12 +++ 2 files changed, 79 insertions(+), 28 deletions(-) diff --git a/qiskit_dynamics/backend/dynamics_backend.py b/qiskit_dynamics/backend/dynamics_backend.py index a557bd885..5c1efd186 100644 --- a/qiskit_dynamics/backend/dynamics_backend.py +++ b/qiskit_dynamics/backend/dynamics_backend.py @@ -64,31 +64,39 @@ class DynamicsBackend(BackendV2): * ``solver``: The Qiskit Dynamics :class:`.Solver` instance used for simulation. * ``solver_options``: Dictionary containing optional kwargs for passing to :meth:`Solver.solve`, indicating solver methods and options. Defaults to the empty dictionary ``{}``. - * ``subsystem_dims``: Dimensions of subsystems making up the system in ``solver``. Defaults - to ``[solver.model.dim]``. - * ``subsystem_labels``: Integer labels for subsystems. Defaults to - ``[0, ..., len(subsystem_dims) - 1]``. + * ``subsystem_dims``: Dimensions of subsystems making up the system in ``solver``. Defaults to + ``[solver.model.dim]``. + * ``subsystem_labels``: Integer labels for subsystems. Defaults to ``[0, ..., + len(subsystem_dims) - 1]``. * ``meas_map``: Measurement map. Defaults to ``[[idx] for idx in subsystem_labels]``. * ``initial_state``: Initial state for simulation, either the string ``"ground_state"``, indicating that the ground state for the system Hamiltonian should be used, or an arbitrary ``Statevector`` or ``DensityMatrix``. Defaults to ``"ground_state"``. - * ``normalize_states``: Boolean indicating whether to normalize states before computing - outcome probabilities. Defaults to ``True``. Setting to ``False`` can result in errors if - the solution tolerance results in probabilities with significant numerical deviation from - proper probability distributions. + * ``normalize_states``: Boolean indicating whether to normalize states before computing outcome + probabilities. Defaults to ``True``. Setting to ``False`` can result in errors if the solution + tolerance results in probabilities with significant numerical deviation from proper + probability distributions. * ``meas_level``: Form of measurement return. Only supported value is ``2``, indicating that counts should be returned. Defaults to ``meas_level==2``. - * ``max_outcome_level``: For ``meas_level==2``, the maximum outcome for each subsystem. - Values will be rounded down to be no larger than ``max_outcome_level``. Must be a positive - integer or ``None``. If ``None``, no rounding occurs. Defaults to ``1``. + * ``max_outcome_level``: For ``meas_level==2``, the maximum outcome for each subsystem. Values + will be rounded down to be no larger than ``max_outcome_level``. Must be a positive integer or + ``None``. If ``None``, no rounding occurs. Defaults to ``1``. * ``memory``: Boolean indicating whether to return a list of explicit measurement outcomes for every experimental shot. Defaults to ``True``. * ``seed_simulator``: Seed to use in random sampling. Defaults to ``None``. - * ``experiment_result_function``: Function for computing the ``ExperimentResult`` - for each simulated experiment. This option defaults to - :func:`default_experiment_result_function`, and any other function set to this option - must have the same signature. Note that the default utilizes various other options that - control results computation, and hence changing it will impact the meaning of other options. + * ``experiment_result_function``: Function for computing the ``ExperimentResult`` for each + simulated experiment. This option defaults to :func:`default_experiment_result_function`, and + any other function set to this option must have the same signature. Note that the default + utilizes various other options that control results computation, and hence changing it will + impact the meaning of other options. + * ``configuration``: A :class:`PulseBackendConfiguration` instance or ``None``. This option + defaults to ``None``, and is not required for the functioning of this class, but is provided + for backwards compatibility. A set configuration will be returned by + :meth:`DynamicsBackend.configuration()`. + * ``defaults``: A :class:`PulseDefaults` instance or ``None``. This option + defaults to ``None``, and is not required for the functioning of this class, but is provided + for backwards compatibility. A set defaults will be returned by + :meth:`DynamicsBackend.defaults()`. """ def __init__( @@ -168,6 +176,8 @@ def _default_options(self): memory=True, seed_simulator=None, experiment_result_function=default_experiment_result_function, + configuration=None, + defaults=None ) def set_options(self, **fields): @@ -179,6 +189,7 @@ def set_options(self, **fields): if not hasattr(self._options, key): raise AttributeError(f"Invalid option {key}") + # validation checks if key == "initial_state": if value != "ground_state" and not isinstance(value, (Statevector, DensityMatrix)): raise QiskitError( @@ -192,7 +203,12 @@ def set_options(self, **fields): raise QiskitError("max_outcome_level must be a positive integer or None.") elif key == "experiment_result_function" and not callable(value): raise QiskitError("experiment_result_function must be callable.") + elif key == "configuration" and not isinstance(value, PulseBackendConfiguration): + raise QiskitError("configuration option must be an instance of PulseBackendConfiguration.") + elif key == "defaults" and not isinstance(value, PulseDefaults): + raise QiskitError("defaults option must be an instance of PulseDefaults.") + # special setting routines if key == "solver": self._set_solver(value) validate_subsystem_dims = True @@ -201,7 +217,7 @@ def set_options(self, **fields): validate_subsystem_dims = True self._options.update_options(**{key: value}) - # perform additional validation if certain options were modified + # perform additional consistency checks if certain options were modified if ( validate_subsystem_dims and np.prod(self._options.subsystem_dims) != self._options.solver.model.dim @@ -210,6 +226,14 @@ def set_options(self, **fields): "DynamicsBackend options subsystem_dims and solver.model.dim are inconsistent." ) + def configuration(self) -> PulseBackendConfiguration: + """Get the backend configuration.""" + return self.options.configuration + + def defaults(self) -> PulseDefaults: + """Get the backend defaults.""" + return self.options.defaults + def _set_solver(self, solver): """Configure simulator based on provided solver.""" if solver._dt is None: @@ -387,14 +411,13 @@ def from_backend( Raises: QiskitError if any required parameters are missing from the passed backend. + Notes: + - Added configuration/defaults methods for "backwards compatibility". They are not + strictly required by DynamicsBackend, but they are required of backends passed to + DynamicsBackend.from_backend, so I think it's probably natural for cases utilizing + from_backend for these to be present. To do: - - Add validation of backend and subsystem_list. E.g. is it a pulse backend? Does it have - the properties we need? - - Maybe we need to also implement configuration(), properties(), and defaults() for - backwards compatibility - - Bring in target? What else? - - Get configuration, properties, target, ... ? - Issue with solver_kwargs is the user may will not have access to the hamiltonian terms, and hence can't effectively specify the rotating frame they want to be in. What to do about this? Could add string handling, like rotating_frame='static_hamiltonian'? @@ -404,7 +427,11 @@ def from_backend( (by getting it from the solver). - Could maybe change arg to "solver_configuration", and it can either be a string like "sparse", "dense", and the appropriate frame is automatically entered, - or it can be a dict and explicitly be passed as + or it can be a dict and explicitly be passed as + - Modify configuration, defaults, target when copying into backend, or + leave as is? + - To test: + - all validation checks in from_backend, including option setting for configuration/defaults """ @@ -430,9 +457,14 @@ def from_backend( # get and parse Hamiltonian string dictionary if subsystem_list is not None: subsystem_list = sorted(subsystem_list) + if subsystem_list[-1] > config.n_qubits: + raise QiskitError(f"subsystem_list contained {subsystem_list[-1]} but backend only has {config.n_qubits} qubits.") else: subsystem_list = list(range(config.n_qubits)) + if not hasattr(config, "hamiltonian"): + raise QiskitError("DynamicsBackend.from_backend requires that backend.configuration() has a hamiltonian attribute.") + hamiltonian_dict = config.hamiltonian ( static_hamiltonian, @@ -440,13 +472,14 @@ def from_backend( hamiltonian_channels, subsystem_dims, ) = parse_backend_hamiltonian_dict(hamiltonian_dict, subsystem_list) - - # Question: could change the output of above function to be this list instead of the dict subsystem_dims = [subsystem_dims[idx] for idx in subsystem_list] # get time step size + if not hasattr(config, "dt"): + raise QiskitError("DynamicsBackend.from_backend requires that backend.configuration() has a dt attribute.") dt = config.dt + # construct model frequencies dictionary from backend channel_freqs = _get_backend_channel_freqs( backend_config=config, backend_defaults=defaults, channels=hamiltonian_channels ) @@ -462,8 +495,14 @@ def from_backend( **solver_kwargs, ) - # to do: modify target??? - target = getattr(backend, "target", None) + # copy major backend attributes + target = copy.copy(getattr(backend, "target", None)) + + if "configuration" not in options: + options["configuration"] = copy.copy(backend_config) + + if "defaults" not in options: + options["defaults"] = copy.copy(backend_defaults) return cls( solver=solver, diff --git a/test/dynamics/backend/test_dynamics_backend.py b/test/dynamics/backend/test_dynamics_backend.py index fd0d32525..d6a561080 100644 --- a/test/dynamics/backend/test_dynamics_backend.py +++ b/test/dynamics/backend/test_dynamics_backend.py @@ -140,6 +140,18 @@ def test_invalid_experiment_result_function(self): with self.assertRaisesRegex(QiskitError, "must be callable."): self.simple_backend.set_options(experiment_result_function=1) + + def test_invalid_configuration_type(self): + """Test setting non-PulseBackendConfiguration.""" + + with self.assertRaisesRegex(QiskitError, "configuration option must be"): + self.simple_backend.set_options(configuration=1) + + def test_invalid_defaults_type(self): + """Test setting non-PulseDefaults.""" + + with self.assertRaisesRegex(QiskitError, "defaults option must be"): + self.simple_backend.set_options(defaults=1) class TestDynamicsBackend(QiskitDynamicsTestCase): From 73ee507cf5d7338a9836d12352a3f7ffef094eae Mon Sep 17 00:00:00 2001 From: DanPuzzuoli Date: Sun, 27 Nov 2022 12:32:07 -0800 Subject: [PATCH 12/21] adding validation tests for from_backend --- qiskit_dynamics/backend/dynamics_backend.py | 91 +++++----- .../dynamics/backend/test_dynamics_backend.py | 161 ++++++++++++++++-- 2 files changed, 191 insertions(+), 61 deletions(-) diff --git a/qiskit_dynamics/backend/dynamics_backend.py b/qiskit_dynamics/backend/dynamics_backend.py index 5c1efd186..cdd97f7ba 100644 --- a/qiskit_dynamics/backend/dynamics_backend.py +++ b/qiskit_dynamics/backend/dynamics_backend.py @@ -177,7 +177,7 @@ def _default_options(self): seed_simulator=None, experiment_result_function=default_experiment_result_function, configuration=None, - defaults=None + defaults=None, ) def set_options(self, **fields): @@ -204,7 +204,9 @@ def set_options(self, **fields): elif key == "experiment_result_function" and not callable(value): raise QiskitError("experiment_result_function must be callable.") elif key == "configuration" and not isinstance(value, PulseBackendConfiguration): - raise QiskitError("configuration option must be an instance of PulseBackendConfiguration.") + raise QiskitError( + "configuration option must be an instance of PulseBackendConfiguration." + ) elif key == "defaults" and not isinstance(value, PulseDefaults): raise QiskitError("defaults option must be an instance of PulseDefaults.") @@ -388,59 +390,56 @@ def from_backend( ) -> "DynamicsBackend": """Construct a :class:`.DynamicsBackend` instance from an existing ``Backend`` instance. - The ``backend`` must have the ``configuration`` and ``defaults`` attributes. - The ``configuration`` must containing a Hamiltonian description, - step size ``dt``, number of qubits ``n_qubits``, and ``u_channel_lo`` (when control channels are present). The ``defaults`` must contain ``qubit_freq_est`` and ``meas_freq_est``. + The ``backend`` must have the ``configuration`` and ``defaults`` attributes. The + ``configuration`` must containing a Hamiltonian description, step size ``dt``, number of + qubits ``n_qubits``, and ``u_channel_lo`` (when control channels are present). The + ``defaults`` must contain ``qubit_freq_est`` and ``meas_freq_est``. + + The optional argument ``subsystem_list`` specifies which subset of qubits will be modelled + in the constructed :class:`DynamicsBackend`, with all other qubits will being dropped from + the model. - The optional argument ``subsystem_list`` specifies which subset of qubits will be - modelled in the constructed :class:`DynamicsBackend`, with all other qubits will being - dropped from the model. - Args: backend: The ``Backend`` instance to build the :class:`.DynamicsBackend` from. subsystem_list: The list of qubits in the backend to include in the model. solver_kwargs: Additional keyword arguments to pass to the :class:`.Solver` instance constructed from the model in the backend. - **options: Additional options to be applied in construction of the + **options: Additional options to be applied in construction of the :class:`.DynamicsBackend`. - + Returns: DynamicsBackend - + Raises: QiskitError if any required parameters are missing from the passed backend. Notes: - Added configuration/defaults methods for "backwards compatibility". They are not - strictly required by DynamicsBackend, but they are required of backends passed to + strictly required by DynamicsBackend, but they are required of backends passed to DynamicsBackend.from_backend, so I think it's probably natural for cases utilizing - from_backend for these to be present. + from_backend for these to be present. These are settable as options, defaulting to + None. To do: - Issue with solver_kwargs is the user may will not have access to the hamiltonian - terms, and hence can't effectively specify the rotating frame they want to be in. - What to do about this? Could add string handling, like rotating_frame='static_hamiltonian'? - - Can we somehow provide the user with "standard" configurations? E.g. if sparse, - it will automatically simulate in the diagonal of the static hamiltonian? - - The user can set the rotating frame of the model AFTER construction as well - (by getting it from the solver). - - Could maybe change arg to "solver_configuration", and it can either be a - string like "sparse", "dense", and the appropriate frame is automatically entered, - or it can be a dict and explicitly be passed as - - Modify configuration, defaults, target when copying into backend, or - leave as is? + terms, and hence can't effectively specify the rotating frame they want to be in. What + to do about this? Could add string handling, like rotating_frame='static_hamiltonian'? + - Can we somehow provide the user with "standard" configurations? E.g. if sparse, it + will automatically simulate in the diagonal of the static hamiltonian? + - The user can set the rotating frame of the model AFTER construction as well (by + getting it from the solver). + - Could maybe change arg to "solver_configuration", and it can either be a string + like "sparse", "dense", and the appropriate frame is automatically entered, or it + can be a dict and explicitly be passed as + - Modify configuration, defaults, target when copying into backend, or leave as is? - To test: - - all validation checks in from_backend, including option setting for configuration/defaults + - all validation checks in from_backend, including option setting for + configuration/defaults """ - # validation - # to do: - # - need to validate that it's a pulse default? Or can we just check that - # it has qubit_freq_est? - # - Validate that subsystem_list is non-empty/well-formed if not hasattr(backend, "configuration"): raise QiskitError( """DynamicsBackend.from_backend requires that the backend argument have a @@ -457,13 +456,17 @@ def from_backend( # get and parse Hamiltonian string dictionary if subsystem_list is not None: subsystem_list = sorted(subsystem_list) - if subsystem_list[-1] > config.n_qubits: - raise QiskitError(f"subsystem_list contained {subsystem_list[-1]} but backend only has {config.n_qubits} qubits.") + if subsystem_list[-1] >= config.n_qubits: + raise QiskitError( + f"""subsystem_list contained {subsystem_list[-1]}, which is out of bounds for config.n_qubits == {config.n_qubits}.""" + ) else: subsystem_list = list(range(config.n_qubits)) if not hasattr(config, "hamiltonian"): - raise QiskitError("DynamicsBackend.from_backend requires that backend.configuration() has a hamiltonian attribute.") + raise QiskitError( + "DynamicsBackend.from_backend requires that backend.configuration() has a hamiltonian attribute." + ) hamiltonian_dict = config.hamiltonian ( @@ -476,7 +479,9 @@ def from_backend( # get time step size if not hasattr(config, "dt"): - raise QiskitError("DynamicsBackend.from_backend requires that backend.configuration() has a dt attribute.") + raise QiskitError( + "DynamicsBackend.from_backend requires that backend.configuration() has a dt attribute." + ) dt = config.dt # construct model frequencies dictionary from backend @@ -500,7 +505,7 @@ def from_backend( if "configuration" not in options: options["configuration"] = copy.copy(backend_config) - + if "defaults" not in options: options["defaults"] = copy.copy(backend_defaults) @@ -713,11 +718,11 @@ def _get_backend_channel_freqs( u_channels = [] for channel in channels: - if channel[0] == 'd': + if channel[0] == "d": drive_channels.append(channel) - elif channel[0] == 'm': + elif channel[0] == "m": meas_channels.append(channel) - elif channel[0] == 'u': + elif channel[0] == "u": u_channels.append(channel) else: raise QiskitError("Unrecognized channel type requested.") @@ -725,10 +730,10 @@ def _get_backend_channel_freqs( # validate required attributes are present if drive_channels and not hasattr(backend_defaults, "qubit_freq_est"): raise QiskitError("DriveChannels in model but defaults does not have qubit_freq_est.") - + if meas_channels and not hasattr(backend_defaults, "meas_freq_est"): raise QiskitError("MeasureChannels in model but defaults does not have meas_freq_est.") - + if u_channels and not hasattr(backend_config, "u_channel_lo"): raise QiskitError("ControlChannels in model but configuration does not have u_channel_lo.") @@ -740,7 +745,7 @@ def _get_backend_channel_freqs( if idx >= len(backend_defaults.qubit_freq_est): raise QiskitError(f"DriveChannel index {idx} is out of bounds.") channel_freqs[channel] = backend_defaults.qubit_freq_est[idx] - + for channel in meas_channels: idx = int(channel[1:]) if idx >= len(backend_defaults.meas_freq_est): @@ -754,7 +759,7 @@ def _get_backend_channel_freqs( freq = 0.0 for u_channel_lo in backend_config.u_channel_lo[idx]: freq += backend_defaults.qubit_freq_est[u_channel_lo.q] * u_channel_lo.scale - + channel_freqs[channel] = freq # validate that all channels have frequencies diff --git a/test/dynamics/backend/test_dynamics_backend.py b/test/dynamics/backend/test_dynamics_backend.py index d6a561080..4336d6f5d 100644 --- a/test/dynamics/backend/test_dynamics_backend.py +++ b/test/dynamics/backend/test_dynamics_backend.py @@ -140,13 +140,13 @@ def test_invalid_experiment_result_function(self): with self.assertRaisesRegex(QiskitError, "must be callable."): self.simple_backend.set_options(experiment_result_function=1) - + def test_invalid_configuration_type(self): """Test setting non-PulseBackendConfiguration.""" with self.assertRaisesRegex(QiskitError, "configuration option must be"): self.simple_backend.set_options(configuration=1) - + def test_invalid_defaults_type(self): """Test setting non-PulseDefaults.""" @@ -408,6 +408,111 @@ def exp_result_function(*args, **kwargs): self.assertDictEqual(result.get_counts(), {"3": 1}) +class TestDynamicsBackend_from_backend(QiskitDynamicsTestCase): + """Test class for DynamicsBackend.from_backend and resulting DynamicsBackend instances.""" + + def setUp(self): + """Set up a simple backend valid for consumption by from_backend.""" + + configuration = SimpleNamespace() + configuration.n_qubits = 5 + configuration.hamiltonian = { + "h_str": [ + "_SUM[i,0,4,wq{i}/2*(I{i}-Z{i})]", + "_SUM[i,0,4,delta{i}/2*O{i}*O{i}]", + "_SUM[i,0,4,-delta{i}/2*O{i}]", + "_SUM[i,0,4,omegad{i}*X{i}||D{i}]", + "jq1q2*Sp1*Sm2", + "jq1q2*Sm1*Sp2", + "jq3q4*Sp3*Sm4", + "jq3q4*Sm3*Sp4", + "jq0q1*Sp0*Sm1", + "jq0q1*Sm0*Sp1", + "jq2q3*Sp2*Sm3", + "jq2q3*Sm2*Sp3", + "omegad1*X0||U0", + "omegad0*X1||U1", + "omegad2*X1||U2", + "omegad1*X2||U3", + "omegad3*X2||U4", + "omegad4*X3||U6", + "omegad2*X3||U5", + "omegad3*X4||U7", + ], + "osc": {}, + "qub": {"0": 3, "1": 3, "2": 3, "3": 3, "4": 3}, + "vars": { + "delta0": -2111793476.4003937, + "delta1": -2089442135.2015743, + "delta2": -2117918367.1068604, + "delta3": -2041004543.1261215, + "delta4": -2111988556.5086775, + "jq0q1": 10495754.104003914, + "jq1q2": 10781715.511200013, + "jq2q3": 8920779.377814226, + "jq3q4": 8985191.65108779, + "omegad0": 971545899.0879812, + "omegad1": 980381253.7440838, + "omegad2": 949475607.7681785, + "omegad3": 976399854.3087951, + "omegad4": 982930801.9780478, + "wq0": 32517894442.809513, + "wq1": 33094899612.019604, + "wq2": 31745180964.17169, + "wq3": 30510620255.52735, + "wq4": 32160826850.25662, + } + } + configuration.dt = 2e-9 / 9 + + defaults = SimpleNamespace() + + # configuration and defaults need to be methods + backend = SimpleNamespace() + backend.configuration = lambda: configuration + backend.defaults = lambda: defaults + + self.valid_backend = backend + + def test_no_configuration_error(self): + """Test that error is raised if no configuration present in backend.""" + + # delete configuration + delattr(self.valid_backend, "configuration") + + with self.assertRaisesRegex(QiskitError, "configuration attribute"): + DynamicsBackend.from_backend(backend=self.valid_backend) + + def test_no_defaults_error(self): + """Test that error is raised if configuration but no defaults present in backend.""" + + delattr(self.valid_backend, "defaults") + + with self.assertRaisesRegex(QiskitError, "defaults attribute"): + DynamicsBackend.from_backend(backend=self.valid_backend) + + def test_no_hamiltonian(self): + """Test error is raised if configuration does not have a hamiltonian.""" + + delattr(self.valid_backend.configuration(), "hamiltonian") + + with self.assertRaisesRegex(QiskitError, "hamiltonian attribute"): + DynamicsBackend.from_backend(backend=self.valid_backend) + + def test_no_dt(self): + """Test error is raised if no dt in configuration.""" + delattr(self.valid_backend.configuration(), "dt") + + with self.assertRaisesRegex(QiskitError, "dt attribute"): + DynamicsBackend.from_backend(backend=self.valid_backend) + + def test_subsystem_list_out_of_bounds(self): + """Test error is raised if subsystem_list contains values above config.n_qubits.""" + + with self.assertRaisesRegex(QiskitError, "out of bounds"): + DynamicsBackend.from_backend(backend=self.valid_backend, subsystem_list=[5]) + + class Test_default_experiment_result_function(QiskitDynamicsTestCase): """Test default_experiment_result_function.""" @@ -499,46 +604,66 @@ def test_drive_and_u_channels(self): """Test case drive and u channels.""" channels = ["d0", "d1", "d2", "u1", "u2"] expected_output = {f"d{idx}": self.defaults.qubit_freq_est[idx] for idx in range(3)} | { - "u1": 2.1 * self.defaults.qubit_freq_est[3], - "u2": 1.1 * self.defaults.qubit_freq_est[4] - 1.1 * self.defaults.qubit_freq_est[2] + "u1": 2.1 * self.defaults.qubit_freq_est[3], + "u2": 1.1 * self.defaults.qubit_freq_est[4] - 1.1 * self.defaults.qubit_freq_est[2], } self._test_with_setUp_example(channels=channels, expected_output=expected_output) - + def test_unrecognized_channel_type(self): """Test error is raised if unrecognized channel type.""" with self.assertRaisesRegex(QiskitError, "Unrecognized"): - _get_backend_channel_freqs(backend_config=SimpleNamespace(), backend_defaults=SimpleNamespace(), channels=["r1"]) + _get_backend_channel_freqs( + backend_config=SimpleNamespace(), + backend_defaults=SimpleNamespace(), + channels=["r1"], + ) def test_no_u_channel_lo_attribute_error(self): """Test error if no u_channel_lo attribute for config.""" - + with self.assertRaisesRegex(QiskitError, "configuration does not have"): - _get_backend_channel_freqs(backend_config=SimpleNamespace(), backend_defaults=self.defaults, channels=["u1"]) - + _get_backend_channel_freqs( + backend_config=SimpleNamespace(), backend_defaults=self.defaults, channels=["u1"] + ) + def test_no_qubit_freq_est_attribute_error(self): """Test error if no qubit_freq_est in defaults.""" - + with self.assertRaisesRegex(QiskitError, "defaults does not have"): - _get_backend_channel_freqs(backend_config=SimpleNamespace(), backend_defaults=SimpleNamespace(), channels=["d0"]) - + _get_backend_channel_freqs( + backend_config=SimpleNamespace(), + backend_defaults=SimpleNamespace(), + channels=["d0"], + ) + def test_no_meas_freq_est_attribute_error(self): """Test error if no meas_freq_est in defaults.""" - + with self.assertRaisesRegex(QiskitError, "defaults does not have"): - _get_backend_channel_freqs(backend_config=SimpleNamespace(), backend_defaults=SimpleNamespace(), channels=["m0"]) + _get_backend_channel_freqs( + backend_config=SimpleNamespace(), + backend_defaults=SimpleNamespace(), + channels=["m0"], + ) def test_missing_u_channel_error(self): """Raise error if missing u channel.""" with self.assertRaisesRegex(QiskitError, "ControlChannel index 4"): - _get_backend_channel_freqs(backend_config=self.config, backend_defaults=self.defaults, channels=["u4"]) - + _get_backend_channel_freqs( + backend_config=self.config, backend_defaults=self.defaults, channels=["u4"] + ) + def test_drive_out_of_bounds(self): """Raise error if drive channel index too high.""" with self.assertRaisesRegex(QiskitError, "DriveChannel index 10"): - _get_backend_channel_freqs(backend_config=self.config, backend_defaults=self.defaults, channels=["d10"]) + _get_backend_channel_freqs( + backend_config=self.config, backend_defaults=self.defaults, channels=["d10"] + ) def test_meas_out_of_bounds(self): """Raise error if drive channel index too high.""" with self.assertRaisesRegex(QiskitError, "MeasureChannel index 6"): - _get_backend_channel_freqs(backend_config=self.config, backend_defaults=self.defaults, channels=["m6"]) \ No newline at end of file + _get_backend_channel_freqs( + backend_config=self.config, backend_defaults=self.defaults, channels=["m6"] + ) From db34318c2480c4924b016dec36a3df2aec787d6b Mon Sep 17 00:00:00 2001 From: DanPuzzuoli Date: Tue, 6 Dec 2022 10:22:38 -0800 Subject: [PATCH 13/21] adding automatic rotating frame kwarg --- qiskit_dynamics/backend/dynamics_backend.py | 74 ++++++++++++++------- 1 file changed, 50 insertions(+), 24 deletions(-) diff --git a/qiskit_dynamics/backend/dynamics_backend.py b/qiskit_dynamics/backend/dynamics_backend.py index cdd97f7ba..31756c6b6 100644 --- a/qiskit_dynamics/backend/dynamics_backend.py +++ b/qiskit_dynamics/backend/dynamics_backend.py @@ -228,14 +228,6 @@ def set_options(self, **fields): "DynamicsBackend options subsystem_dims and solver.model.dim are inconsistent." ) - def configuration(self) -> PulseBackendConfiguration: - """Get the backend configuration.""" - return self.options.configuration - - def defaults(self) -> PulseDefaults: - """Get the backend defaults.""" - return self.options.defaults - def _set_solver(self, solver): """Configure simulator based on provided solver.""" if solver._dt is None: @@ -380,12 +372,21 @@ def target(self) -> Target: def meas_map(self) -> List[List[int]]: return self.options.meas_map + def configuration(self) -> PulseBackendConfiguration: + """Get the backend configuration.""" + return self.options.configuration + + def defaults(self) -> PulseDefaults: + """Get the backend defaults.""" + return self.options.defaults + @classmethod def from_backend( cls, backend: Union[BackendV1, BackendV2], subsystem_list: Optional[List[int]] = None, - solver_kwargs: Optional[dict] = None, + solver_init_kwargs: Optional[dict] = None, + auto_rotating_frame: bool = True, **options, ) -> "DynamicsBackend": """Construct a :class:`.DynamicsBackend` instance from an existing ``Backend`` instance. @@ -399,11 +400,34 @@ def from_backend( in the constructed :class:`DynamicsBackend`, with all other qubits will being dropped from the model. + Configuration of the underlying :class:`.Solver` is controlled via + ``solver_init_kwargs``, passed directly to :meth:`.Solver.__init__`. The additional + argument ``auto_rotating_frame`` allows this method to automatically choose the rotating + frame in which the :class:`.Solver` will be configured. If ``auto_rotating_frame==True`` + and no ``rotating_frame`` is specified in ``solver_init_kwargs``: + + * If a dense evaluation mode is chosen, the rotating frame will be set to the + ``static_hamiltonian`` indicated by the Hamiltonian in ``backend.configuration()``. + * If a sparse evaluation mode is chosen, the rotating frame will be set to the diagonal of + ``static_hamiltonian``. + + **Technical notes** + + * The whole ``configuration`` and ``defaults`` attributes of the original backend will not + be copied into the constructed :class:`DynamicsBackend` instance, only the required data + stored within these attributes will be extracted. If required, for backwards + compatibility, ``'configuration'`` and ``'defaults'`` options can be set, which will be + returned via the :meth:`.configuration` and :meth:`.defaults` methods. + * Gates and calibrations are not copied into the constructed :class:`DynamicsBackend`. + Due to inevitable model inaccuracies, gates calibrated on a real device will not + have the same performance on the constructed :class:`DynamicsBackend`. As such, the + :class:`DynamicsBackend` will be constructed with an empty ``InstructionScheduleMap``, and + must be recalibrated. Args: backend: The ``Backend`` instance to build the :class:`.DynamicsBackend` from. subsystem_list: The list of qubits in the backend to include in the model. - solver_kwargs: Additional keyword arguments to pass to the :class:`.Solver` instance + solver_configuration: Additional keyword arguments to pass to the :class:`.Solver` instance constructed from the model in the backend. **options: Additional options to be applied in construction of the :class:`.DynamicsBackend`. @@ -420,6 +444,11 @@ def from_backend( DynamicsBackend.from_backend, so I think it's probably natural for cases utilizing from_backend for these to be present. These are settable as options, defaulting to None. + - Configuration/defaults are not being copied over. They have way too many parameters + that aren't relevant, and some which would need to be deleted (like default gates). + - The new target is only being instantiated with dt. + - + To do: - Issue with solver_kwargs is the user may will not have access to the hamiltonian @@ -432,7 +461,6 @@ def from_backend( - Could maybe change arg to "solver_configuration", and it can either be a string like "sparse", "dense", and the appropriate frame is automatically entered, or it can be a dict and explicitly be passed as - - Modify configuration, defaults, target when copying into backend, or leave as is? - To test: - all validation checks in from_backend, including option setting for configuration/defaults @@ -490,28 +518,26 @@ def from_backend( ) # build the solver - solver_kwargs = solver_kwargs or {} + solver_init_kwargs = copy.copy(solver_init_kwargs) or {} + if auto_rotating_frame and "rotating_frame" not in solver_init_kwargs: + evaluation_mode = solver_init_kwargs.get("evaluation_mode", "dense") + if "dense" in evaluation_mode: + solver_init_kwargs["rotating_frame"] = static_hamiltonian + else: + solver_init_kwargs["rotating_frame"] = np.diag(static_hamiltonian) + solver = Solver( static_hamiltonian=static_hamiltonian, hamiltonian_operators=hamiltonian_operators, hamiltonian_channels=hamiltonian_channels, channel_carrier_freqs=channel_freqs, dt=dt, - **solver_kwargs, + **solver_init_kwargs, ) - - # copy major backend attributes - target = copy.copy(getattr(backend, "target", None)) - - if "configuration" not in options: - options["configuration"] = copy.copy(backend_config) - - if "defaults" not in options: - options["defaults"] = copy.copy(backend_defaults) - + return cls( solver=solver, - target=target, + target=Target(dt=dt), subsystem_labels=subsystem_list, subsystem_dims=subsystem_dims, **options, From 9bdd42a3896d132cec7088cec5f19c3b89a40985 Mon Sep 17 00:00:00 2001 From: DanPuzzuoli Date: Wed, 7 Dec 2022 14:12:13 -0800 Subject: [PATCH 14/21] adding tests for correct construction from backend --- qiskit_dynamics/backend/dynamics_backend.py | 44 ++--- .../dynamics/backend/test_dynamics_backend.py | 160 +++++++++++++++++- 2 files changed, 179 insertions(+), 25 deletions(-) diff --git a/qiskit_dynamics/backend/dynamics_backend.py b/qiskit_dynamics/backend/dynamics_backend.py index 31756c6b6..f6d789cbb 100644 --- a/qiskit_dynamics/backend/dynamics_backend.py +++ b/qiskit_dynamics/backend/dynamics_backend.py @@ -42,6 +42,8 @@ from qiskit import schedule as build_schedule from qiskit.quantum_info import Statevector, DensityMatrix +from qiskit_dynamics import RotatingFrame +from qiskit_dynamics.array import Array from qiskit_dynamics.solvers.solver_classes import Solver from .dynamics_job import DynamicsJob @@ -385,8 +387,9 @@ def from_backend( cls, backend: Union[BackendV1, BackendV2], subsystem_list: Optional[List[int]] = None, - solver_init_kwargs: Optional[dict] = None, - auto_rotating_frame: bool = True, + rotating_frame: Optional[Union[Array, RotatingFrame, str]] = "auto", + evaluation_mode: str = "dense", + rwa_cutoff_freq: Optional[float] = None, **options, ) -> "DynamicsBackend": """Construct a :class:`.DynamicsBackend` instance from an existing ``Backend`` instance. @@ -400,16 +403,19 @@ def from_backend( in the constructed :class:`DynamicsBackend`, with all other qubits will being dropped from the model. - Configuration of the underlying :class:`.Solver` is controlled via - ``solver_init_kwargs``, passed directly to :meth:`.Solver.__init__`. The additional - argument ``auto_rotating_frame`` allows this method to automatically choose the rotating - frame in which the :class:`.Solver` will be configured. If ``auto_rotating_frame==True`` - and no ``rotating_frame`` is specified in ``solver_init_kwargs``: + Configuration of the underlying :class:`.Solver` is controlled via the ``rotating_frame``, + ``evaluation_mode``, and ``rwa_cutoff_freq`` options. In contrast to :class:`.Solver` + initialization, ``rotating_frame`` defaults to the string ``"auto"``, which allows this + method to choose the rotating frame based on ``evaluation_mode``: * If a dense evaluation mode is chosen, the rotating frame will be set to the ``static_hamiltonian`` indicated by the Hamiltonian in ``backend.configuration()``. * If a sparse evaluation mode is chosen, the rotating frame will be set to the diagonal of ``static_hamiltonian``. + + Otherwise the ``rotating_frame``, ``evaluation_mode``, and ``rwa_cutoff_freq`` aer passed + directly to the :class:`.Solver` initialization. + **Technical notes** @@ -427,8 +433,10 @@ def from_backend( Args: backend: The ``Backend`` instance to build the :class:`.DynamicsBackend` from. subsystem_list: The list of qubits in the backend to include in the model. - solver_configuration: Additional keyword arguments to pass to the :class:`.Solver` instance - constructed from the model in the backend. + rotating_frame: Rotating frame argument for the internal :class:`.Solver`. Defaults to + ``"auto"``, allowing this method to pick a rotating frame. + evaluation_mode: Evaluation mode argument for the internal :class:`.Solver`. + rwa_cutoff_freq: Rotating wave approximation argument for the internal :class:`.Solver`. **options: Additional options to be applied in construction of the :class:`.DynamicsBackend`. @@ -464,18 +472,16 @@ def from_backend( - To test: - all validation checks in from_backend, including option setting for configuration/defaults - - """ if not hasattr(backend, "configuration"): raise QiskitError( """DynamicsBackend.from_backend requires that the backend argument have a - configuration attribute.""" + configuration method.""" ) if not hasattr(backend, "defaults"): raise QiskitError( - """DynamicsBackend.from_backend requires that the backend argument have a defaults attribute.""" + """DynamicsBackend.from_backend requires that the backend argument have a defaults method.""" ) config = backend.configuration() @@ -518,13 +524,11 @@ def from_backend( ) # build the solver - solver_init_kwargs = copy.copy(solver_init_kwargs) or {} - if auto_rotating_frame and "rotating_frame" not in solver_init_kwargs: - evaluation_mode = solver_init_kwargs.get("evaluation_mode", "dense") + if rotating_frame == "auto": if "dense" in evaluation_mode: - solver_init_kwargs["rotating_frame"] = static_hamiltonian + rotating_frame = static_hamiltonian else: - solver_init_kwargs["rotating_frame"] = np.diag(static_hamiltonian) + rotating_frame = np.diag(static_hamiltonian) solver = Solver( static_hamiltonian=static_hamiltonian, @@ -532,7 +536,9 @@ def from_backend( hamiltonian_channels=hamiltonian_channels, channel_carrier_freqs=channel_freqs, dt=dt, - **solver_init_kwargs, + rotating_frame=rotating_frame, + evaluation_mode=evaluation_mode, + rwa_cutoff_freq=rwa_cutoff_freq ) return cls( diff --git a/test/dynamics/backend/test_dynamics_backend.py b/test/dynamics/backend/test_dynamics_backend.py index 4336d6f5d..0269f64df 100644 --- a/test/dynamics/backend/test_dynamics_backend.py +++ b/test/dynamics/backend/test_dynamics_backend.py @@ -19,6 +19,7 @@ import numpy as np from scipy.integrate._ivp.ivp import OdeResult +from scipy.sparse import csr_matrix from qiskit import QiskitError, pulse, QuantumCircuit from qiskit.transpiler import Target @@ -27,6 +28,7 @@ from qiskit.providers.models.backendconfiguration import UchannelLO from qiskit_dynamics import Solver, DynamicsBackend +from qiskit_dynamics.array import Array from qiskit_dynamics.backend import default_experiment_result_function from qiskit_dynamics.backend.dynamics_backend import _get_backend_channel_freqs from ..common import QiskitDynamicsTestCase @@ -464,8 +466,25 @@ def setUp(self): } } configuration.dt = 2e-9 / 9 + configuration.u_channel_lo = [ + [UchannelLO(1, (1+0j))], + [UchannelLO(0, (1+0j))], + [UchannelLO(2, (1+0j))], + [UchannelLO(1, (1+0j))], + [UchannelLO(3, (1+0j))], + [UchannelLO(2, (1+0j))], + [UchannelLO(4, (1+0j))], + [UchannelLO(3, (1+0j))] + ] defaults = SimpleNamespace() + defaults.qubit_freq_est = [ + 5175383639.513607, + 5267216864.382969, + 5052402469.794663, + 4855916030.466884, + 5118554567.140891 + ] # configuration and defaults need to be methods backend = SimpleNamespace() @@ -480,7 +499,7 @@ def test_no_configuration_error(self): # delete configuration delattr(self.valid_backend, "configuration") - with self.assertRaisesRegex(QiskitError, "configuration attribute"): + with self.assertRaisesRegex(QiskitError, "configuration method"): DynamicsBackend.from_backend(backend=self.valid_backend) def test_no_defaults_error(self): @@ -488,9 +507,15 @@ def test_no_defaults_error(self): delattr(self.valid_backend, "defaults") - with self.assertRaisesRegex(QiskitError, "defaults attribute"): + with self.assertRaisesRegex(QiskitError, "defaults method"): DynamicsBackend.from_backend(backend=self.valid_backend) + def test_subsystem_list_out_of_bounds(self): + """Test error is raised if subsystem_list contains values above config.n_qubits.""" + + with self.assertRaisesRegex(QiskitError, "out of bounds"): + DynamicsBackend.from_backend(backend=self.valid_backend, subsystem_list=[5]) + def test_no_hamiltonian(self): """Test error is raised if configuration does not have a hamiltonian.""" @@ -505,13 +530,136 @@ def test_no_dt(self): with self.assertRaisesRegex(QiskitError, "dt attribute"): DynamicsBackend.from_backend(backend=self.valid_backend) + + def test_building_model(self): + """Test construction from_backend without additional options to solver.""" - def test_subsystem_list_out_of_bounds(self): - """Test error is raised if subsystem_list contains values above config.n_qubits.""" + backend = DynamicsBackend.from_backend( + self.valid_backend, + subsystem_list=[0, 1] + ) - with self.assertRaisesRegex(QiskitError, "out of bounds"): - DynamicsBackend.from_backend(backend=self.valid_backend, subsystem_list=[5]) + self.assertTrue(backend.target.dt == 2e-9 / 9) + + solver = backend.options.solver + self.assertDictEqual( + solver._schedule_converter._carriers, + { + "d0": 5175383639.513607, + "d1": 5267216864.382969, + "u0": 5267216864.382969, + "u1": 5175383639.513607, + "u2": 5052402469.794663 + } + ) + + self.assertTrue(isinstance(solver.model.static_operator, Array)) + + N0 = np.diag(np.kron([1., 1., 1.], [0., 1., 2.])) + N1 = np.diag(np.kron([0., 1., 2.], [1., 1., 1.])) + a0 = np.kron(np.eye(3), np.diag([1., np.sqrt(2)], 1)) + a0dag = a0.transpose() + a1 = np.kron(np.diag([1., np.sqrt(2)], 1), np.eye(3)) + a1dag = a1.transpose() + + frame_operator = (32517894442.809513 * N0 + (-2111793476.4003937 / 2) * (N0 * N0 - N0) + + 33094899612.019604 * N1 + (-2089442135.2015743 / 2) * (N1 * N1 - N1) + + 10495754.104003914 * (a0 @ a1dag + a0dag @ a1) + ) + + self.assertAllClose(frame_operator, solver.model.rotating_frame.frame_operator) + self.assertAllClose(solver.model.static_operator/1e9, np.zeros(9)) + + expected_operators = np.array([971545899.0879812 * (a0 + a0dag), 980381253.7440838 * (a1 + a1dag), + 980381253.7440838 * (a0 + a0dag), 971545899.0879812 * (a1 + a1dag), + 949475607.7681785 * (a1 + a1dag)]) + self.assertAllClose(expected_operators/1e9, solver.model.operators/1e9) + + def test_building_model_sparse(self): + """Test construction from_backend in sparse mode.""" + + backend = DynamicsBackend.from_backend( + self.valid_backend, + subsystem_list=[0, 1], + evaluation_mode="sparse" + ) + + self.assertTrue(backend.target.dt == 2e-9 / 9) + + solver = backend.options.solver + self.assertDictEqual( + solver._schedule_converter._carriers, + { + "d0": 5175383639.513607, + "d1": 5267216864.382969, + "u0": 5267216864.382969, + "u1": 5175383639.513607, + "u2": 5052402469.794663 + } + ) + + self.assertTrue(isinstance(solver.model.static_operator, csr_matrix)) + + N0 = np.diag(np.kron([1., 1., 1.], [0., 1., 2.])) + N1 = np.diag(np.kron([0., 1., 2.], [1., 1., 1.])) + a0 = np.kron(np.eye(3), np.diag([1., np.sqrt(2)], 1)) + a0dag = a0.transpose() + a1 = np.kron(np.diag([1., np.sqrt(2)], 1), np.eye(3)) + a1dag = a1.transpose() + + frame_operator = np.diag(32517894442.809513 * N0 + (-2111793476.4003937 / 2) * (N0 * N0 - N0) + + 33094899612.019604 * N1 + (-2089442135.2015743 / 2) * (N1 * N1 - N1)) + static_operator = 10495754.104003914 * (a0 @ a1dag + a0dag @ a1) + + self.assertAllClose(frame_operator, solver.model.rotating_frame.frame_operator) + self.assertAllCloseSparse(static_operator/1e9, solver.model.static_operator.todense()/1e9) + + expected_operators = np.array([971545899.0879812 * (a0 + a0dag), 980381253.7440838 * (a1 + a1dag), + 980381253.7440838 * (a0 + a0dag), 971545899.0879812 * (a1 + a1dag), + 949475607.7681785 * (a1 + a1dag)]) + self.assertAllClose(expected_operators/1e9, [x.todense()/1e9 for x in solver.model.operators]) + + def test_building_model_case2(self): + """Test construction from_backend without additional options to solver, case 2.""" + + backend = DynamicsBackend.from_backend( + self.valid_backend, + subsystem_list=[0, 4] + ) + + self.assertTrue(backend.target.dt == 2e-9 / 9) + + solver = backend.options.solver + self.assertDictEqual( + solver._schedule_converter._carriers, + { + "d0": 5175383639.513607, + "d4": 5118554567.140891, + "u0": 5267216864.382969, + "u7": 4855916030.466884 + } + ) + + self.assertTrue(isinstance(solver.model.static_operator, Array)) + + N0 = np.diag(np.kron([1., 1., 1.], [0., 1., 2.])) + N4 = np.diag(np.kron([0., 1., 2.], [1., 1., 1.])) + a0 = np.kron(np.eye(3), np.diag([1., np.sqrt(2)], 1)) + a0dag = a0.transpose() + a4 = np.kron(np.diag([1., np.sqrt(2)], 1), np.eye(3)) + a4dag = a4.transpose() + + frame_operator = (32517894442.809513 * N0 + (-2111793476.4003937 / 2) * (N0 * N0 - N0) + + 32160826850.25662 * N4 + (-2111988556.5086775 / 2) * (N4 * N4 - N4) + ) + + self.assertAllClose(frame_operator, solver.model.rotating_frame.frame_operator) + self.assertAllClose(solver.model.static_operator/1e9, np.zeros(9)) + expected_operators = np.array([971545899.0879812 * (a0 + a0dag), 982930801.9780478 * (a4 + a4dag), + 980381253.7440838 * (a0 + a0dag), + 976399854.3087951 * (a4 + a4dag)]) + self.assertAllClose(expected_operators/1e9, solver.model.operators/1e9) class Test_default_experiment_result_function(QiskitDynamicsTestCase): """Test default_experiment_result_function.""" From c6b1d4ed3bb3604a71ebfdf60eabe86ae636e9db Mon Sep 17 00:00:00 2001 From: DanPuzzuoli Date: Wed, 7 Dec 2022 14:37:06 -0800 Subject: [PATCH 15/21] fixing formatting --- qiskit_dynamics/backend/backend_utils.py | 12 +- qiskit_dynamics/backend/dynamics_backend.py | 85 ++++----- .../dynamics/backend/test_dynamics_backend.py | 168 ++++++++++-------- 3 files changed, 133 insertions(+), 132 deletions(-) diff --git a/qiskit_dynamics/backend/backend_utils.py b/qiskit_dynamics/backend/backend_utils.py index 073494ea2..53ecae8be 100644 --- a/qiskit_dynamics/backend/backend_utils.py +++ b/qiskit_dynamics/backend/backend_utils.py @@ -114,14 +114,14 @@ def _get_memory_slot_probabilities( Args: probability_dict: A list of probabilities for the otucomes of state measurement. Keys - are assumed to all be strings of integers of the same length. + are assumed to all be strings of integers of the same length. memory_slot_indices: Indices of which memory slots store the digits of the keys of - probability_dict. + probability_dict. num_memory_slots: Total number of memory slots for results. If None, - defaults to the maximum index in memory_slot_indices. The default value - of unused memory slots is 0. + defaults to the maximum index in memory_slot_indices. The default value + of unused memory slots is 0. max_outcome_value: Maximum value that can be stored in a memory slot. All outcomes higher - than this will be rounded down. + than this will be rounded down. Returns: Dict: Keys are memory slot outcomes, values are the probabilities of those outcomes. @@ -152,7 +152,7 @@ def _sample_probability_dict( Args: probability_dict: Dictionary representing probability distribution, with keys being - outcomes, values being probabilities. + outcomes, values being probabilities. shots: Number of shots. seed: Seed to use in rng construction. diff --git a/qiskit_dynamics/backend/dynamics_backend.py b/qiskit_dynamics/backend/dynamics_backend.py index f6d789cbb..5ae2dd786 100644 --- a/qiskit_dynamics/backend/dynamics_backend.py +++ b/qiskit_dynamics/backend/dynamics_backend.py @@ -195,8 +195,8 @@ def set_options(self, **fields): if key == "initial_state": if value != "ground_state" and not isinstance(value, (Statevector, DensityMatrix)): raise QiskitError( - """initial_state must be either "ground_state", - or a Statevector or DensityMatrix instance.""" + 'initial_state must be either "ground_state", or a Statevector or ' + "DensityMatrix instance." ) elif key == "meas_level" and value != 2: raise QiskitError("Only meas_level == 2 is supported by DynamicsBackend.") @@ -403,30 +403,30 @@ def from_backend( in the constructed :class:`DynamicsBackend`, with all other qubits will being dropped from the model. - Configuration of the underlying :class:`.Solver` is controlled via the ``rotating_frame``, + Configuration of the underlying :class:`.Solver` is controlled via the ``rotating_frame``, ``evaluation_mode``, and ``rwa_cutoff_freq`` options. In contrast to :class:`.Solver` initialization, ``rotating_frame`` defaults to the string ``"auto"``, which allows this method to choose the rotating frame based on ``evaluation_mode``: - * If a dense evaluation mode is chosen, the rotating frame will be set to the + * If a dense evaluation mode is chosen, the rotating frame will be set to the ``static_hamiltonian`` indicated by the Hamiltonian in ``backend.configuration()``. - * If a sparse evaluation mode is chosen, the rotating frame will be set to the diagonal of + * If a sparse evaluation mode is chosen, the rotating frame will be set to the diagonal of ``static_hamiltonian``. - Otherwise the ``rotating_frame``, ``evaluation_mode``, and ``rwa_cutoff_freq`` aer passed + Otherwise the ``rotating_frame``, ``evaluation_mode``, and ``rwa_cutoff_freq`` aer passed directly to the :class:`.Solver` initialization. - + **Technical notes** - * The whole ``configuration`` and ``defaults`` attributes of the original backend will not - be copied into the constructed :class:`DynamicsBackend` instance, only the required data - stored within these attributes will be extracted. If required, for backwards - compatibility, ``'configuration'`` and ``'defaults'`` options can be set, which will be + * The whole ``configuration`` and ``defaults`` attributes of the original backend will not + be copied into the constructed :class:`DynamicsBackend` instance, only the required data + stored within these attributes will be extracted. If required, for backwards + compatibility, ``'configuration'`` and ``'defaults'`` options can be set, which will be returned via the :meth:`.configuration` and :meth:`.defaults` methods. - * Gates and calibrations are not copied into the constructed :class:`DynamicsBackend`. - Due to inevitable model inaccuracies, gates calibrated on a real device will not - have the same performance on the constructed :class:`DynamicsBackend`. As such, the + * Gates and calibrations are not copied into the constructed :class:`DynamicsBackend`. Due + to inevitable model inaccuracies, gates calibrated on a real device will not have the same + performance on the constructed :class:`DynamicsBackend`. As such, the :class:`DynamicsBackend` will be constructed with an empty ``InstructionScheduleMap``, and must be recalibrated. @@ -444,44 +444,18 @@ def from_backend( DynamicsBackend Raises: - QiskitError if any required parameters are missing from the passed backend. - - Notes: - - Added configuration/defaults methods for "backwards compatibility". They are not - strictly required by DynamicsBackend, but they are required of backends passed to - DynamicsBackend.from_backend, so I think it's probably natural for cases utilizing - from_backend for these to be present. These are settable as options, defaulting to - None. - - Configuration/defaults are not being copied over. They have way too many parameters - that aren't relevant, and some which would need to be deleted (like default gates). - - The new target is only being instantiated with dt. - - - - - To do: - - Issue with solver_kwargs is the user may will not have access to the hamiltonian - terms, and hence can't effectively specify the rotating frame they want to be in. What - to do about this? Could add string handling, like rotating_frame='static_hamiltonian'? - - Can we somehow provide the user with "standard" configurations? E.g. if sparse, it - will automatically simulate in the diagonal of the static hamiltonian? - - The user can set the rotating frame of the model AFTER construction as well (by - getting it from the solver). - - Could maybe change arg to "solver_configuration", and it can either be a string - like "sparse", "dense", and the appropriate frame is automatically entered, or it - can be a dict and explicitly be passed as - - To test: - - all validation checks in from_backend, including option setting for - configuration/defaults + QiskitError: If any required parameters are missing from the passed backend. """ if not hasattr(backend, "configuration"): raise QiskitError( - """DynamicsBackend.from_backend requires that the backend argument have a - configuration method.""" + "DynamicsBackend.from_backend requires that the backend argument has a " + "configuration method." ) if not hasattr(backend, "defaults"): raise QiskitError( - """DynamicsBackend.from_backend requires that the backend argument have a defaults method.""" + "DynamicsBackend.from_backend requires that the backend argument has a defaults " + "method." ) config = backend.configuration() @@ -492,14 +466,16 @@ def from_backend( subsystem_list = sorted(subsystem_list) if subsystem_list[-1] >= config.n_qubits: raise QiskitError( - f"""subsystem_list contained {subsystem_list[-1]}, which is out of bounds for config.n_qubits == {config.n_qubits}.""" + f"subsystem_list contained {subsystem_list[-1]}, which is out of bounds for " + f"config.n_qubits == {config.n_qubits}." ) else: subsystem_list = list(range(config.n_qubits)) if not hasattr(config, "hamiltonian"): raise QiskitError( - "DynamicsBackend.from_backend requires that backend.configuration() has a hamiltonian attribute." + "DynamicsBackend.from_backend requires that backend.configuration() has a " + "hamiltonian attribute." ) hamiltonian_dict = config.hamiltonian @@ -514,7 +490,8 @@ def from_backend( # get time step size if not hasattr(config, "dt"): raise QiskitError( - "DynamicsBackend.from_backend requires that backend.configuration() has a dt attribute." + "DynamicsBackend.from_backend requires that backend.configuration() has a dt " + "attribute." ) dt = config.dt @@ -538,9 +515,9 @@ def from_backend( dt=dt, rotating_frame=rotating_frame, evaluation_mode=evaluation_mode, - rwa_cutoff_freq=rwa_cutoff_freq + rwa_cutoff_freq=rwa_cutoff_freq, ) - + return cls( solver=solver, target=Target(dt=dt), @@ -674,8 +651,8 @@ def _get_acquire_data(schedules, valid_subsystem_labels): # validate if len(schedule_acquire_times) == 0: raise QiskitError( - """At least one measurement saving a a result in a MemorySlot - must be present in each schedule.""" + "At least one measurement saving a a result in a MemorySlot " + "must be present in each schedule." ) for acquire_time in schedule_acquire_times[1:]: @@ -690,8 +667,8 @@ def _get_acquire_data(schedules, valid_subsystem_labels): measurement_subsystems.append(inst.channel.index) else: raise QiskitError( - f"""Attempted to measure subsystem {inst.channel.index}, - but it is not in subsystem_list.""" + f"Attempted to measure subsystem {inst.channel.index}, but it is not in " + "subsystem_list." ) memory_slot_indices.append(inst.mem_slot.index) diff --git a/test/dynamics/backend/test_dynamics_backend.py b/test/dynamics/backend/test_dynamics_backend.py index 0269f64df..e387d6a16 100644 --- a/test/dynamics/backend/test_dynamics_backend.py +++ b/test/dynamics/backend/test_dynamics_backend.py @@ -463,18 +463,18 @@ def setUp(self): "wq2": 31745180964.17169, "wq3": 30510620255.52735, "wq4": 32160826850.25662, - } + }, } configuration.dt = 2e-9 / 9 configuration.u_channel_lo = [ - [UchannelLO(1, (1+0j))], - [UchannelLO(0, (1+0j))], - [UchannelLO(2, (1+0j))], - [UchannelLO(1, (1+0j))], - [UchannelLO(3, (1+0j))], - [UchannelLO(2, (1+0j))], - [UchannelLO(4, (1+0j))], - [UchannelLO(3, (1+0j))] + [UchannelLO(1, (1 + 0j))], + [UchannelLO(0, (1 + 0j))], + [UchannelLO(2, (1 + 0j))], + [UchannelLO(1, (1 + 0j))], + [UchannelLO(3, (1 + 0j))], + [UchannelLO(2, (1 + 0j))], + [UchannelLO(4, (1 + 0j))], + [UchannelLO(3, (1 + 0j))], ] defaults = SimpleNamespace() @@ -483,7 +483,7 @@ def setUp(self): 5267216864.382969, 5052402469.794663, 4855916030.466884, - 5118554567.140891 + 5118554567.140891, ] # configuration and defaults need to be methods @@ -499,7 +499,7 @@ def test_no_configuration_error(self): # delete configuration delattr(self.valid_backend, "configuration") - with self.assertRaisesRegex(QiskitError, "configuration method"): + with self.assertRaisesRegex(QiskitError, "has a configuration method"): DynamicsBackend.from_backend(backend=self.valid_backend) def test_no_defaults_error(self): @@ -507,7 +507,7 @@ def test_no_defaults_error(self): delattr(self.valid_backend, "defaults") - with self.assertRaisesRegex(QiskitError, "defaults method"): + with self.assertRaisesRegex(QiskitError, "has a defaults"): DynamicsBackend.from_backend(backend=self.valid_backend) def test_subsystem_list_out_of_bounds(self): @@ -523,21 +523,18 @@ def test_no_hamiltonian(self): with self.assertRaisesRegex(QiskitError, "hamiltonian attribute"): DynamicsBackend.from_backend(backend=self.valid_backend) - + def test_no_dt(self): """Test error is raised if no dt in configuration.""" delattr(self.valid_backend.configuration(), "dt") - with self.assertRaisesRegex(QiskitError, "dt attribute"): + with self.assertRaisesRegex(QiskitError, "has a dt"): DynamicsBackend.from_backend(backend=self.valid_backend) - + def test_building_model(self): """Test construction from_backend without additional options to solver.""" - backend = DynamicsBackend.from_backend( - self.valid_backend, - subsystem_list=[0, 1] - ) + backend = DynamicsBackend.from_backend(self.valid_backend, subsystem_list=[0, 1]) self.assertTrue(backend.target.dt == 2e-9 / 9) @@ -549,39 +546,46 @@ def test_building_model(self): "d1": 5267216864.382969, "u0": 5267216864.382969, "u1": 5175383639.513607, - "u2": 5052402469.794663 - } + "u2": 5052402469.794663, + }, ) self.assertTrue(isinstance(solver.model.static_operator, Array)) - - N0 = np.diag(np.kron([1., 1., 1.], [0., 1., 2.])) - N1 = np.diag(np.kron([0., 1., 2.], [1., 1., 1.])) - a0 = np.kron(np.eye(3), np.diag([1., np.sqrt(2)], 1)) + + N0 = np.diag(np.kron([1.0, 1.0, 1.0], [0.0, 1.0, 2.0])) + N1 = np.diag(np.kron([0.0, 1.0, 2.0], [1.0, 1.0, 1.0])) + a0 = np.kron(np.eye(3), np.diag([1.0, np.sqrt(2)], 1)) a0dag = a0.transpose() - a1 = np.kron(np.diag([1., np.sqrt(2)], 1), np.eye(3)) + a1 = np.kron(np.diag([1.0, np.sqrt(2)], 1), np.eye(3)) a1dag = a1.transpose() - - frame_operator = (32517894442.809513 * N0 + (-2111793476.4003937 / 2) * (N0 * N0 - N0) - + 33094899612.019604 * N1 + (-2089442135.2015743 / 2) * (N1 * N1 - N1) + + frame_operator = ( + 32517894442.809513 * N0 + + (-2111793476.4003937 / 2) * (N0 * N0 - N0) + + 33094899612.019604 * N1 + + (-2089442135.2015743 / 2) * (N1 * N1 - N1) + 10495754.104003914 * (a0 @ a1dag + a0dag @ a1) ) self.assertAllClose(frame_operator, solver.model.rotating_frame.frame_operator) - self.assertAllClose(solver.model.static_operator/1e9, np.zeros(9)) + self.assertAllClose(solver.model.static_operator / 1e9, np.zeros(9)) + + expected_operators = np.array( + [ + 971545899.0879812 * (a0 + a0dag), + 980381253.7440838 * (a1 + a1dag), + 980381253.7440838 * (a0 + a0dag), + 971545899.0879812 * (a1 + a1dag), + 949475607.7681785 * (a1 + a1dag), + ] + ) + self.assertAllClose(expected_operators / 1e9, solver.model.operators / 1e9) - expected_operators = np.array([971545899.0879812 * (a0 + a0dag), 980381253.7440838 * (a1 + a1dag), - 980381253.7440838 * (a0 + a0dag), 971545899.0879812 * (a1 + a1dag), - 949475607.7681785 * (a1 + a1dag)]) - self.assertAllClose(expected_operators/1e9, solver.model.operators/1e9) - def test_building_model_sparse(self): """Test construction from_backend in sparse mode.""" backend = DynamicsBackend.from_backend( - self.valid_backend, - subsystem_list=[0, 1], - evaluation_mode="sparse" + self.valid_backend, subsystem_list=[0, 1], evaluation_mode="sparse" ) self.assertTrue(backend.target.dt == 2e-9 / 9) @@ -594,38 +598,49 @@ def test_building_model_sparse(self): "d1": 5267216864.382969, "u0": 5267216864.382969, "u1": 5175383639.513607, - "u2": 5052402469.794663 - } + "u2": 5052402469.794663, + }, ) self.assertTrue(isinstance(solver.model.static_operator, csr_matrix)) - - N0 = np.diag(np.kron([1., 1., 1.], [0., 1., 2.])) - N1 = np.diag(np.kron([0., 1., 2.], [1., 1., 1.])) - a0 = np.kron(np.eye(3), np.diag([1., np.sqrt(2)], 1)) + + N0 = np.diag(np.kron([1.0, 1.0, 1.0], [0.0, 1.0, 2.0])) + N1 = np.diag(np.kron([0.0, 1.0, 2.0], [1.0, 1.0, 1.0])) + a0 = np.kron(np.eye(3), np.diag([1.0, np.sqrt(2)], 1)) a0dag = a0.transpose() - a1 = np.kron(np.diag([1., np.sqrt(2)], 1), np.eye(3)) + a1 = np.kron(np.diag([1.0, np.sqrt(2)], 1), np.eye(3)) a1dag = a1.transpose() - - frame_operator = np.diag(32517894442.809513 * N0 + (-2111793476.4003937 / 2) * (N0 * N0 - N0) - + 33094899612.019604 * N1 + (-2089442135.2015743 / 2) * (N1 * N1 - N1)) + + frame_operator = np.diag( + 32517894442.809513 * N0 + + (-2111793476.4003937 / 2) * (N0 * N0 - N0) + + 33094899612.019604 * N1 + + (-2089442135.2015743 / 2) * (N1 * N1 - N1) + ) static_operator = 10495754.104003914 * (a0 @ a1dag + a0dag @ a1) self.assertAllClose(frame_operator, solver.model.rotating_frame.frame_operator) - self.assertAllCloseSparse(static_operator/1e9, solver.model.static_operator.todense()/1e9) + self.assertAllCloseSparse( + static_operator / 1e9, solver.model.static_operator.todense() / 1e9 + ) - expected_operators = np.array([971545899.0879812 * (a0 + a0dag), 980381253.7440838 * (a1 + a1dag), - 980381253.7440838 * (a0 + a0dag), 971545899.0879812 * (a1 + a1dag), - 949475607.7681785 * (a1 + a1dag)]) - self.assertAllClose(expected_operators/1e9, [x.todense()/1e9 for x in solver.model.operators]) + expected_operators = np.array( + [ + 971545899.0879812 * (a0 + a0dag), + 980381253.7440838 * (a1 + a1dag), + 980381253.7440838 * (a0 + a0dag), + 971545899.0879812 * (a1 + a1dag), + 949475607.7681785 * (a1 + a1dag), + ] + ) + self.assertAllClose( + expected_operators / 1e9, [x.todense() / 1e9 for x in solver.model.operators] + ) def test_building_model_case2(self): """Test construction from_backend without additional options to solver, case 2.""" - backend = DynamicsBackend.from_backend( - self.valid_backend, - subsystem_list=[0, 4] - ) + backend = DynamicsBackend.from_backend(self.valid_backend, subsystem_list=[0, 4]) self.assertTrue(backend.target.dt == 2e-9 / 9) @@ -636,30 +651,39 @@ def test_building_model_case2(self): "d0": 5175383639.513607, "d4": 5118554567.140891, "u0": 5267216864.382969, - "u7": 4855916030.466884 - } + "u7": 4855916030.466884, + }, ) self.assertTrue(isinstance(solver.model.static_operator, Array)) - - N0 = np.diag(np.kron([1., 1., 1.], [0., 1., 2.])) - N4 = np.diag(np.kron([0., 1., 2.], [1., 1., 1.])) - a0 = np.kron(np.eye(3), np.diag([1., np.sqrt(2)], 1)) + + N0 = np.diag(np.kron([1.0, 1.0, 1.0], [0.0, 1.0, 2.0])) + N4 = np.diag(np.kron([0.0, 1.0, 2.0], [1.0, 1.0, 1.0])) + a0 = np.kron(np.eye(3), np.diag([1.0, np.sqrt(2)], 1)) a0dag = a0.transpose() - a4 = np.kron(np.diag([1., np.sqrt(2)], 1), np.eye(3)) + a4 = np.kron(np.diag([1.0, np.sqrt(2)], 1), np.eye(3)) a4dag = a4.transpose() - - frame_operator = (32517894442.809513 * N0 + (-2111793476.4003937 / 2) * (N0 * N0 - N0) - + 32160826850.25662 * N4 + (-2111988556.5086775 / 2) * (N4 * N4 - N4) + + frame_operator = ( + 32517894442.809513 * N0 + + (-2111793476.4003937 / 2) * (N0 * N0 - N0) + + 32160826850.25662 * N4 + + (-2111988556.5086775 / 2) * (N4 * N4 - N4) ) self.assertAllClose(frame_operator, solver.model.rotating_frame.frame_operator) - self.assertAllClose(solver.model.static_operator/1e9, np.zeros(9)) + self.assertAllClose(solver.model.static_operator / 1e9, np.zeros(9)) + + expected_operators = np.array( + [ + 971545899.0879812 * (a0 + a0dag), + 982930801.9780478 * (a4 + a4dag), + 980381253.7440838 * (a0 + a0dag), + 976399854.3087951 * (a4 + a4dag), + ] + ) + self.assertAllClose(expected_operators / 1e9, solver.model.operators / 1e9) - expected_operators = np.array([971545899.0879812 * (a0 + a0dag), 982930801.9780478 * (a4 + a4dag), - 980381253.7440838 * (a0 + a0dag), - 976399854.3087951 * (a4 + a4dag)]) - self.assertAllClose(expected_operators/1e9, solver.model.operators/1e9) class Test_default_experiment_result_function(QiskitDynamicsTestCase): """Test default_experiment_result_function.""" From 2ef0ac2f394b037788b49918e58ab762e53b3a5f Mon Sep 17 00:00:00 2001 From: DanPuzzuoli Date: Wed, 7 Dec 2022 15:06:14 -0800 Subject: [PATCH 16/21] editting documentation --- qiskit_dynamics/backend/dynamics_backend.py | 39 +++++++++++---------- 1 file changed, 20 insertions(+), 19 deletions(-) diff --git a/qiskit_dynamics/backend/dynamics_backend.py b/qiskit_dynamics/backend/dynamics_backend.py index 5ae2dd786..e899a1168 100644 --- a/qiskit_dynamics/backend/dynamics_backend.py +++ b/qiskit_dynamics/backend/dynamics_backend.py @@ -394,14 +394,29 @@ def from_backend( ) -> "DynamicsBackend": """Construct a :class:`.DynamicsBackend` instance from an existing ``Backend`` instance. + .. warning:: + + Due to inevitable model inaccuracies, gates calibrated on a real backend will not have + the same performance on the :class:`.DynamicsBackend` instance returned by this method. + As such, gates and calibrations are not be copied into the constructed + :class:`.DynamicsBackend`. + The ``backend`` must have the ``configuration`` and ``defaults`` attributes. The ``configuration`` must containing a Hamiltonian description, step size ``dt``, number of qubits ``n_qubits``, and ``u_channel_lo`` (when control channels are present). The - ``defaults`` must contain ``qubit_freq_est`` and ``meas_freq_est``. + ``defaults`` must contain ``qubit_freq_est``, as well as ``meas_freq_est`` if measurement + channels appear explicitly in the Hamiltonian. + + .. note:: + + The ``configuration`` and ``defaults`` attributes of the original backend are not copied + into the constructed :class:`DynamicsBackend` instance, only the required data stored + within these attributes will be extracted. If required, for backwards compatibility, + ``'configuration'`` and ``'defaults'`` options can be set, which will be returned via + the :meth:`.configuration` and :meth:`.defaults` methods. - The optional argument ``subsystem_list`` specifies which subset of qubits will be modelled - in the constructed :class:`DynamicsBackend`, with all other qubits will being dropped from - the model. + The optional argument ``subsystem_list`` specifies which subset of qubits to model in the + constructed :class:`DynamicsBackend`. All other qubits are dropped from the model. Configuration of the underlying :class:`.Solver` is controlled via the ``rotating_frame``, ``evaluation_mode``, and ``rwa_cutoff_freq`` options. In contrast to :class:`.Solver` @@ -413,23 +428,9 @@ def from_backend( * If a sparse evaluation mode is chosen, the rotating frame will be set to the diagonal of ``static_hamiltonian``. - Otherwise the ``rotating_frame``, ``evaluation_mode``, and ``rwa_cutoff_freq`` aer passed + Otherwise the ``rotating_frame``, ``evaluation_mode``, and ``rwa_cutoff_freq`` are passed directly to the :class:`.Solver` initialization. - - **Technical notes** - - * The whole ``configuration`` and ``defaults`` attributes of the original backend will not - be copied into the constructed :class:`DynamicsBackend` instance, only the required data - stored within these attributes will be extracted. If required, for backwards - compatibility, ``'configuration'`` and ``'defaults'`` options can be set, which will be - returned via the :meth:`.configuration` and :meth:`.defaults` methods. - * Gates and calibrations are not copied into the constructed :class:`DynamicsBackend`. Due - to inevitable model inaccuracies, gates calibrated on a real device will not have the same - performance on the constructed :class:`DynamicsBackend`. As such, the - :class:`DynamicsBackend` will be constructed with an empty ``InstructionScheduleMap``, and - must be recalibrated. - Args: backend: The ``Backend`` instance to build the :class:`.DynamicsBackend` from. subsystem_list: The list of qubits in the backend to include in the model. From 34182b62e50f8df6d46185fc242893dbae147827 Mon Sep 17 00:00:00 2001 From: DanPuzzuoli Date: Thu, 8 Dec 2022 08:45:42 -0800 Subject: [PATCH 17/21] fixing issue with 3.7 dictionary handling --- test/dynamics/backend/test_dynamics_backend.py | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/test/dynamics/backend/test_dynamics_backend.py b/test/dynamics/backend/test_dynamics_backend.py index e387d6a16..767461634 100644 --- a/test/dynamics/backend/test_dynamics_backend.py +++ b/test/dynamics/backend/test_dynamics_backend.py @@ -767,18 +767,20 @@ def test_drive_channels(self): def test_drive_and_meas_channels(self): """Test case drive and meas channels.""" channels = ["d0", "d1", "d2", "m0", "m3"] - expected_output = {f"d{idx}": self.defaults.qubit_freq_est[idx] for idx in range(3)} | { - f"m{idx}": self.defaults.meas_freq_est[idx] for idx in [0, 3] - } + expected_output = {f"d{idx}": self.defaults.qubit_freq_est[idx] for idx in range(3)} + expected_output.update({f"m{idx}": self.defaults.meas_freq_est[idx] for idx in [0, 3]}) self._test_with_setUp_example(channels=channels, expected_output=expected_output) def test_drive_and_u_channels(self): """Test case drive and u channels.""" channels = ["d0", "d1", "d2", "u1", "u2"] - expected_output = {f"d{idx}": self.defaults.qubit_freq_est[idx] for idx in range(3)} | { - "u1": 2.1 * self.defaults.qubit_freq_est[3], - "u2": 1.1 * self.defaults.qubit_freq_est[4] - 1.1 * self.defaults.qubit_freq_est[2], - } + expected_output = {f"d{idx}": self.defaults.qubit_freq_est[idx] for idx in range(3)} + expected_output.update( + { + "u1": 2.1 * self.defaults.qubit_freq_est[3], + "u2": 1.1 * self.defaults.qubit_freq_est[4] - 1.1 * self.defaults.qubit_freq_est[2], + } + ) self._test_with_setUp_example(channels=channels, expected_output=expected_output) def test_unrecognized_channel_type(self): From e37b581a21481c7b5ad52940d8f204e378ef05cb Mon Sep 17 00:00:00 2001 From: DanPuzzuoli Date: Tue, 14 Feb 2023 10:24:30 -0800 Subject: [PATCH 18/21] linting --- test/dynamics/backend/test_dynamics_backend.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/test/dynamics/backend/test_dynamics_backend.py b/test/dynamics/backend/test_dynamics_backend.py index 60627a3f7..0578510bd 100644 --- a/test/dynamics/backend/test_dynamics_backend.py +++ b/test/dynamics/backend/test_dynamics_backend.py @@ -31,7 +31,10 @@ from qiskit_dynamics import Solver, DynamicsBackend from qiskit_dynamics.array import Array from qiskit_dynamics.backend import default_experiment_result_function -from qiskit_dynamics.backend.dynamics_backend import _get_acquire_instruction_timings, _get_backend_channel_freqs +from qiskit_dynamics.backend.dynamics_backend import ( + _get_acquire_instruction_timings, + _get_backend_channel_freqs, +) from ..common import QiskitDynamicsTestCase From ec44719d78a838b4423875bcd3f1866debb13947 Mon Sep 17 00:00:00 2001 From: DanPuzzuoli Date: Thu, 16 Feb 2023 13:03:57 -0800 Subject: [PATCH 19/21] lint --- qiskit_dynamics/backend/dynamics_backend.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qiskit_dynamics/backend/dynamics_backend.py b/qiskit_dynamics/backend/dynamics_backend.py index e3bfb67a3..da0fd6b59 100644 --- a/qiskit_dynamics/backend/dynamics_backend.py +++ b/qiskit_dynamics/backend/dynamics_backend.py @@ -492,7 +492,7 @@ def control_channel( control_channels.append(pulse.ControlChannel(self.options.control_channel_map[x])) return control_channels - + def configuration(self) -> PulseBackendConfiguration: """Get the backend configuration.""" return self.options.configuration From 38b2dd8d92258329a18787ad0434a0138e8ed5ea Mon Sep 17 00:00:00 2001 From: DanPuzzuoli Date: Thu, 16 Feb 2023 13:26:05 -0800 Subject: [PATCH 20/21] removing unnecessary variable assignment --- qiskit_dynamics/backend/dynamics_backend.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/qiskit_dynamics/backend/dynamics_backend.py b/qiskit_dynamics/backend/dynamics_backend.py index da0fd6b59..96d832b5f 100644 --- a/qiskit_dynamics/backend/dynamics_backend.py +++ b/qiskit_dynamics/backend/dynamics_backend.py @@ -106,11 +106,11 @@ class DynamicsBackend(BackendV2): impact the meaning of other options. * ``configuration``: A :class:`PulseBackendConfiguration` instance or ``None``. This option defaults to ``None``, and is not required for the functioning of this class, but is provided - for backwards compatibility. A set configuration will be returned by + for compatibility. A set configuration will be returned by :meth:`DynamicsBackend.configuration()`. * ``defaults``: A :class:`PulseDefaults` instance or ``None``. This option defaults to ``None``, - and is not required for the functioning of this class, but is provided for backwards - compatibility. A set defaults will be returned by :meth:`DynamicsBackend.defaults()`. + and is not required for the functioning of this class, but is provided for compatibility. A + set defaults will be returned by :meth:`DynamicsBackend.defaults()`. """ def __init__( @@ -530,7 +530,7 @@ def from_backend( The ``configuration`` and ``defaults`` attributes of the original backend are not copied into the constructed :class:`DynamicsBackend` instance, only the required data stored - within these attributes will be extracted. If required, for backwards compatibility, + within these attributes will be extracted. If required, for compatibility, ``'configuration'`` and ``'defaults'`` options can be set, which will be returned via the :meth:`.configuration` and :meth:`.defaults` methods. @@ -598,13 +598,12 @@ def from_backend( "hamiltonian attribute." ) - hamiltonian_dict = config.hamiltonian ( static_hamiltonian, hamiltonian_operators, hamiltonian_channels, subsystem_dims, - ) = parse_backend_hamiltonian_dict(hamiltonian_dict, subsystem_list) + ) = parse_backend_hamiltonian_dict(config.hamiltonian, subsystem_list) subsystem_dims = [subsystem_dims[idx] for idx in subsystem_list] # get time step size From 0dbb4f33f67e2a2a67db7a239085eb7a36f0280e Mon Sep 17 00:00:00 2001 From: DanPuzzuoli Date: Thu, 16 Feb 2023 17:35:26 -0800 Subject: [PATCH 21/21] modifying DynamicsBackend.from_backend to retrieve as much information as possible from the target, and updating the documentation to show where everything comes from --- qiskit_dynamics/backend/dynamics_backend.py | 145 ++++++++++++------ .../dynamics/backend/test_dynamics_backend.py | 101 +++++++----- 2 files changed, 161 insertions(+), 85 deletions(-) diff --git a/qiskit_dynamics/backend/dynamics_backend.py b/qiskit_dynamics/backend/dynamics_backend.py index 96d832b5f..8dd62e052 100644 --- a/qiskit_dynamics/backend/dynamics_backend.py +++ b/qiskit_dynamics/backend/dynamics_backend.py @@ -511,7 +511,7 @@ def from_backend( rwa_cutoff_freq: Optional[float] = None, **options, ) -> "DynamicsBackend": - """Construct a :class:`.DynamicsBackend` instance from an existing ``Backend`` instance. + """Construct a DynamicsBackend instance from an existing Backend instance. .. warning:: @@ -520,19 +520,43 @@ def from_backend( As such, gates and calibrations are not be copied into the constructed :class:`.DynamicsBackend`. - The ``backend`` must have the ``configuration`` and ``defaults`` attributes. The - ``configuration`` must containing a Hamiltonian description, step size ``dt``, number of - qubits ``n_qubits``, and ``u_channel_lo`` (when control channels are present). The - ``defaults`` must contain ``qubit_freq_est``, as well as ``meas_freq_est`` if measurement - channels appear explicitly in the Hamiltonian. + The ``backend`` must contain sufficient information in the ``target``, ``configuration``, + and/or ``defaults`` attributes to be able to run simulations. The following table indicates + which parameters are required, along with their primary and secondary sources: + + .. list-table:: Backend parameter locations + :widths: 10 25 25 + :header-rows: 1 + + * - Parameter + - Primary source + - Secondary source + * - ``hamiltonian`` dictionary. + - ``configuration.hamiltonian`` + - N/A + * - Control channel frequency specification. + - ``configuration.u_channel_lo`` + - N/A + * - Number of qubits in the backend model. + - ``target.num_qubits`` + - ``configuration.n_qubits`` + * - Pulse schedule sample size ``dt``. + - ``target.dt`` + - ``configuration.dt`` + * - Drive channel frequencies. + - ``target.qubit_properties`` + - ``defaults.qubit_freq_est`` + * - Measurement channel frequencies, if measurement channels explicitly appear in the + model. + - ``defaults.meas_freq_est`` + - N/A .. note:: - The ``configuration`` and ``defaults`` attributes of the original backend are not copied - into the constructed :class:`DynamicsBackend` instance, only the required data stored - within these attributes will be extracted. If required, for compatibility, - ``'configuration'`` and ``'defaults'`` options can be set, which will be returned via - the :meth:`.configuration` and :meth:`.defaults` methods. + The ``target``, ``configuration``, and ``defaults`` attributes of the original backend + are not copied into the constructed :class:`DynamicsBackend` instance, only the required + data stored within these attributes will be extracted. If necessary, these attributes + can be set and configured by the user. The optional argument ``subsystem_list`` specifies which subset of qubits to model in the constructed :class:`DynamicsBackend`. All other qubits are dropped from the model. @@ -567,35 +591,40 @@ def from_backend( QiskitError: If any required parameters are missing from the passed backend. """ + # get available target, config, and defaults objects + backend_target = getattr(backend, "target", None) + if not hasattr(backend, "configuration"): raise QiskitError( "DynamicsBackend.from_backend requires that the backend argument has a " "configuration method." ) - if not hasattr(backend, "defaults"): - raise QiskitError( - "DynamicsBackend.from_backend requires that the backend argument has a defaults " - "method." - ) + backend_config = backend.configuration() - config = backend.configuration() - defaults = backend.defaults() + backend_defaults = None + if hasattr(backend, "defaults"): + backend_defaults = backend.defaults() # get and parse Hamiltonian string dictionary + if backend_target is not None: + backend_num_qubits = backend_target.num_qubits + else: + backend_num_qubits = backend_config.n_qubits + if subsystem_list is not None: subsystem_list = sorted(subsystem_list) - if subsystem_list[-1] >= config.n_qubits: + if subsystem_list[-1] >= backend_num_qubits: raise QiskitError( f"subsystem_list contained {subsystem_list[-1]}, which is out of bounds for " - f"config.n_qubits == {config.n_qubits}." + f"backend with {backend_num_qubits} qubits." ) else: - subsystem_list = list(range(config.n_qubits)) + subsystem_list = list(range(backend_num_qubits)) - if not hasattr(config, "hamiltonian"): + if backend_config.hamiltonian is None: raise QiskitError( "DynamicsBackend.from_backend requires that backend.configuration() has a " - "hamiltonian attribute." + "hamiltonian." ) ( @@ -603,20 +632,15 @@ def from_backend( hamiltonian_operators, hamiltonian_channels, subsystem_dims, - ) = parse_backend_hamiltonian_dict(config.hamiltonian, subsystem_list) + ) = parse_backend_hamiltonian_dict(backend_config.hamiltonian, subsystem_list) subsystem_dims = [subsystem_dims[idx] for idx in subsystem_list] - # get time step size - if not hasattr(config, "dt"): - raise QiskitError( - "DynamicsBackend.from_backend requires that backend.configuration() has a dt " - "attribute." - ) - dt = config.dt - # construct model frequencies dictionary from backend channel_freqs = _get_backend_channel_freqs( - backend_config=config, backend_defaults=defaults, channels=hamiltonian_channels + backend_target=backend_target, + backend_config=backend_config, + backend_defaults=backend_defaults, + channels=hamiltonian_channels, ) # build the solver @@ -626,6 +650,13 @@ def from_backend( else: rotating_frame = np.diag(static_hamiltonian) + # get time step size + if backend_target is not None and backend_target.dt is not None: + dt = backend_target.dt + else: + # config is guaranteed to have a dt + dt = backend_config.dt + solver = Solver( static_hamiltonian=static_hamiltonian, hamiltonian_operators=hamiltonian_operators, @@ -880,13 +911,17 @@ def _to_schedule_list( def _get_backend_channel_freqs( - backend_config: PulseBackendConfiguration, backend_defaults: PulseDefaults, channels: List[str] + backend_target: Optional[Target], + backend_config: PulseBackendConfiguration, + backend_defaults: Optional[PulseDefaults], + channels: List[str], ) -> Dict[str, float]: """Extract frequencies of channels from a backend configuration and defaults. Args: + backend_target: A backend target object or ``None``. backend_config: A backend configuration object. - backend_defaults: A backend defaults object. + backend_defaults: A backend defaults object or ``None``. channels: Channel labels given as strings, assumed to be unique. Returns: @@ -911,38 +946,50 @@ def _get_backend_channel_freqs( else: raise QiskitError("Unrecognized channel type requested.") - # validate required attributes are present - if drive_channels and not hasattr(backend_defaults, "qubit_freq_est"): - raise QiskitError("DriveChannels in model but defaults does not have qubit_freq_est.") + # extract and validate channel frequency parameters + if drive_channels: + # get drive channel frequencies + drive_frequencies = [] + if (backend_target is not None) and (backend_target.qubit_properties is not None): + drive_frequencies = [q.frequency for q in backend_target.qubit_properties] + elif backend_defaults is not None: + drive_frequencies = backend_defaults.qubit_freq_est + else: + raise QiskitError( + "DriveChannels in model but frequencies not available in target or defaults." + ) - if meas_channels and not hasattr(backend_defaults, "meas_freq_est"): - raise QiskitError("MeasureChannels in model but defaults does not have meas_freq_est.") + if meas_channels: + if backend_defaults is not None: + meas_frequencies = backend_defaults.meas_freq_est + else: + raise QiskitError("MeasureChannels in model but defaults does not have meas_freq_est.") - if u_channels and not hasattr(backend_config, "u_channel_lo"): - raise QiskitError("ControlChannels in model but configuration does not have u_channel_lo.") + # backend_config.u_channel_lo is guaranteed to be a list + u_channel_lo = backend_config.u_channel_lo # populate frequencies channel_freqs = {} for channel in drive_channels: idx = int(channel[1:]) - if idx >= len(backend_defaults.qubit_freq_est): + if idx >= len(drive_frequencies): raise QiskitError(f"DriveChannel index {idx} is out of bounds.") - channel_freqs[channel] = backend_defaults.qubit_freq_est[idx] + channel_freqs[channel] = drive_frequencies[idx] for channel in meas_channels: idx = int(channel[1:]) - if idx >= len(backend_defaults.meas_freq_est): + if idx >= len(meas_frequencies): raise QiskitError(f"MeasureChannel index {idx} is out of bounds.") - channel_freqs[channel] = backend_defaults.meas_freq_est[idx] + channel_freqs[channel] = meas_frequencies[idx] for channel in u_channels: idx = int(channel[1:]) - if idx >= len(backend_config.u_channel_lo): + if idx >= len(u_channel_lo): raise QiskitError(f"ControlChannel index {idx} is out of bounds.") freq = 0.0 - for u_channel_lo in backend_config.u_channel_lo[idx]: - freq += backend_defaults.qubit_freq_est[u_channel_lo.q] * u_channel_lo.scale + for channel_lo in u_channel_lo[idx]: + freq += drive_frequencies[channel_lo.q] * channel_lo.scale channel_freqs[channel] = freq diff --git a/test/dynamics/backend/test_dynamics_backend.py b/test/dynamics/backend/test_dynamics_backend.py index 944a0237a..fcedb414e 100644 --- a/test/dynamics/backend/test_dynamics_backend.py +++ b/test/dynamics/backend/test_dynamics_backend.py @@ -27,6 +27,7 @@ from qiskit.quantum_info import Statevector, DensityMatrix from qiskit.result.models import ExperimentResult, ExperimentResultData from qiskit.providers.models.backendconfiguration import UchannelLO +from qiskit.providers.backend import QubitProperties from qiskit_dynamics import Solver, DynamicsBackend from qiskit_dynamics.array import Array @@ -641,14 +642,6 @@ def test_no_configuration_error(self): with self.assertRaisesRegex(QiskitError, "has a configuration method"): DynamicsBackend.from_backend(backend=self.valid_backend) - def test_no_defaults_error(self): - """Test that error is raised if configuration but no defaults present in backend.""" - - delattr(self.valid_backend, "defaults") - - with self.assertRaisesRegex(QiskitError, "has a defaults"): - DynamicsBackend.from_backend(backend=self.valid_backend) - def test_subsystem_list_out_of_bounds(self): """Test error is raised if subsystem_list contains values above config.n_qubits.""" @@ -658,16 +651,9 @@ def test_subsystem_list_out_of_bounds(self): def test_no_hamiltonian(self): """Test error is raised if configuration does not have a hamiltonian.""" - delattr(self.valid_backend.configuration(), "hamiltonian") + self.valid_backend.configuration().hamiltonian = None - with self.assertRaisesRegex(QiskitError, "hamiltonian attribute"): - DynamicsBackend.from_backend(backend=self.valid_backend) - - def test_no_dt(self): - """Test error is raised if no dt in configuration.""" - delattr(self.valid_backend.configuration(), "dt") - - with self.assertRaisesRegex(QiskitError, "has a dt"): + with self.assertRaisesRegex(QiskitError, "requires that"): DynamicsBackend.from_backend(backend=self.valid_backend) def test_building_model(self): @@ -720,6 +706,24 @@ def test_building_model(self): ) self.assertAllClose(expected_operators / 1e9, solver.model.operators / 1e9) + def test_building_model_target_override(self): + """Test that the parameters retrievable from the target are preferred.""" + + target = Target( + dt=0.1, qubit_properties=[QubitProperties(frequency=float(x)) for x in range(5)] + ) + + self.valid_backend.target = target + backend = DynamicsBackend.from_backend(self.valid_backend, subsystem_list=[0, 1]) + + self.assertTrue(backend.target.dt == 0.1) + + solver = backend.options.solver + self.assertDictEqual( + solver._schedule_converter._carriers, + {"d0": 0.0, "d1": 1.0, "u0": 1.0, "u1": 0.0, "u2": 2.0}, + ) + def test_building_model_sparse(self): """Test construction from_backend in sparse mode.""" @@ -882,11 +886,14 @@ def setUp(self): ] self.config = config - def _test_with_setUp_example(self, channels, expected_output): + def _test_with_setUp_example_no_target(self, channels, expected_output): """Test with defaults and config from setUp.""" self.assertDictEqual( _get_backend_channel_freqs( - backend_config=self.config, backend_defaults=self.defaults, channels=channels + backend_target=None, + backend_config=self.config, + backend_defaults=self.defaults, + channels=channels, ), expected_output, ) @@ -895,14 +902,14 @@ def test_drive_channels(self): """Test case with just drive channels.""" channels = ["d0", "d1", "d2"] expected_output = {f"d{idx}": self.defaults.qubit_freq_est[idx] for idx in range(3)} - self._test_with_setUp_example(channels=channels, expected_output=expected_output) + self._test_with_setUp_example_no_target(channels=channels, expected_output=expected_output) def test_drive_and_meas_channels(self): """Test case drive and meas channels.""" channels = ["d0", "d1", "d2", "m0", "m3"] expected_output = {f"d{idx}": self.defaults.qubit_freq_est[idx] for idx in range(3)} expected_output.update({f"m{idx}": self.defaults.meas_freq_est[idx] for idx in [0, 3]}) - self._test_with_setUp_example(channels=channels, expected_output=expected_output) + self._test_with_setUp_example_no_target(channels=channels, expected_output=expected_output) def test_drive_and_u_channels(self): """Test case drive and u channels.""" @@ -914,33 +921,27 @@ def test_drive_and_u_channels(self): "u2": 1.1 * self.defaults.qubit_freq_est[4] - 1.1 * self.defaults.qubit_freq_est[2], } ) - self._test_with_setUp_example(channels=channels, expected_output=expected_output) + self._test_with_setUp_example_no_target(channels=channels, expected_output=expected_output) def test_unrecognized_channel_type(self): """Test error is raised if unrecognized channel type.""" with self.assertRaisesRegex(QiskitError, "Unrecognized"): _get_backend_channel_freqs( + backend_target=None, backend_config=SimpleNamespace(), backend_defaults=SimpleNamespace(), channels=["r1"], ) - def test_no_u_channel_lo_attribute_error(self): - """Test error if no u_channel_lo attribute for config.""" - - with self.assertRaisesRegex(QiskitError, "configuration does not have"): - _get_backend_channel_freqs( - backend_config=SimpleNamespace(), backend_defaults=self.defaults, channels=["u1"] - ) - def test_no_qubit_freq_est_attribute_error(self): """Test error if no qubit_freq_est in defaults.""" - with self.assertRaisesRegex(QiskitError, "defaults does not have"): + with self.assertRaisesRegex(QiskitError, "frequencies not available in target or defaults"): _get_backend_channel_freqs( + backend_target=None, backend_config=SimpleNamespace(), - backend_defaults=SimpleNamespace(), + backend_defaults=None, channels=["d0"], ) @@ -949,8 +950,9 @@ def test_no_meas_freq_est_attribute_error(self): with self.assertRaisesRegex(QiskitError, "defaults does not have"): _get_backend_channel_freqs( + backend_target=None, backend_config=SimpleNamespace(), - backend_defaults=SimpleNamespace(), + backend_defaults=None, channels=["m0"], ) @@ -958,23 +960,50 @@ def test_missing_u_channel_error(self): """Raise error if missing u channel.""" with self.assertRaisesRegex(QiskitError, "ControlChannel index 4"): _get_backend_channel_freqs( - backend_config=self.config, backend_defaults=self.defaults, channels=["u4"] + backend_target=None, + backend_config=self.config, + backend_defaults=self.defaults, + channels=["u4"], ) def test_drive_out_of_bounds(self): """Raise error if drive channel index too high.""" with self.assertRaisesRegex(QiskitError, "DriveChannel index 10"): _get_backend_channel_freqs( - backend_config=self.config, backend_defaults=self.defaults, channels=["d10"] + backend_target=None, + backend_config=self.config, + backend_defaults=self.defaults, + channels=["d10"], ) def test_meas_out_of_bounds(self): """Raise error if drive channel index too high.""" with self.assertRaisesRegex(QiskitError, "MeasureChannel index 6"): _get_backend_channel_freqs( - backend_config=self.config, backend_defaults=self.defaults, channels=["m6"] + backend_target=None, + backend_config=self.config, + backend_defaults=self.defaults, + channels=["m6"], ) + def test_no_defaults(self): + """Test a case where defaults are not needed.""" + target = Target( + dt=0.1, + qubit_properties=[QubitProperties(frequency=0.0), QubitProperties(frequency=1.0)], + ) + + config = SimpleNamespace() + config.u_channel_lo = [] + + channel_freqs = _get_backend_channel_freqs( + backend_target=target, + backend_config=config, + backend_defaults=None, + channels=["d0", "d1"], + ) + self.assertDictEqual(channel_freqs, {"d0": 0.0, "d1": 1.0}) + class Test_get_acquire_instruction_timings(QiskitDynamicsTestCase): """Tests for _get_acquire_instruction_timings behaviour not covered by DynamicsBackend tests."""