Source code for qermit.taskgraph.utils

# Copyright 2019-2023 Quantinuum
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.


from collections import OrderedDict
from copy import copy
from typing import Dict, Iterable, List, Optional, Tuple, Union

from numpy import ndarray
from pytket.backends.backendresult import BackendResult
from pytket.circuit import Bit, Circuit
from pytket.pauli import QubitPauliString
from pytket.utils import (
    QubitPauliOperator,
    expectation_from_counts,
)
from pytket.utils.operators import _coeff_convert
from sympy import Symbol  # type: ignore


[docs] class SymbolsDict(object): """ A helper class for standardising interfacing with Circuit Symbolics in qermit. Methods take different containers that hold some kind of symbols representation and return a SymbolsDict object. Methods access self._symbolic_map or use other accessors to modify or add new symbols. """
[docs] def __init__(self) -> None: """ Default constructor, creates an empty OrderedDict() object for future symbols to be added to. """ self._symbolic_map: Dict[Symbol, Union[None, float]] = OrderedDict()
[docs] @classmethod def symbols_from_circuit(cls, circuit: Circuit) -> "SymbolsDict": """ Given a pytket Circuit, returns a SymbolsDict object capturing given circuits free symbols. :param circuit: Pytket circuit with potential symbols. """ mit_symbols = cls() for sym in circuit.free_symbols(): mit_symbols.add_symbol(sym) return mit_symbols
[docs] @classmethod def symbols_from_dict( cls, symbol_dict: Dict[Symbol, Union[None, float]] ) -> "SymbolsDict": """ Assigns to mit_symbols attribute _symbolic_map straight from passed dictionary of Symbol to None/float. :param symbol_dict: Dictionary from Circuit symbolics to values. """ mit_symbols = cls() mit_symbols._symbolic_map = symbol_dict return mit_symbols
[docs] @classmethod def symbols_from_list( cls, symbols_list: Iterable[Union[Symbol, str]] ) -> "SymbolsDict": """ Adds all symbols (or string representing Symbol) as dict entries with no value. :param symbols_list: A list of strings representing Symbols or Symbols. """ mit_symbols = cls() for sym in symbols_list: mit_symbols.add_symbol(sym) return mit_symbols
@property def symbols_list(self) -> Iterable[Symbol]: """ Returns all symbols held in dictionary of symbols in SymbolsDict object. :return: Iterable containing all keys from _symbolic_map, i.e. all Symbols """ for s in self._symbolic_map.keys(): yield s
[docs] def add_symbol(self, symbol: Union[str, Symbol]): """ Adds any passed Symbol (in string form or sympy Symbol type) as a key to dictionary with None value assigned. :param symbol: Symbol to be added to self._symbolic_map """ if isinstance(symbol, str): sym = Symbol(symbol) self._symbolic_map[sym] = None elif isinstance(symbol, Symbol): self._symbolic_map[symbol] = None else: msg = f""" Argument symbol is of invalid type: {type(symbol)}. """ raise TypeError(msg)
[docs] def get_symbolic_map(self, symbol_values: ndarray) -> Dict[Symbol, float]: """ Assigns given values in parameters to keys in self._symbolic_map in order, for a new dictionary object. Returns just this dictionary type. :param symbol_values: Ordered values to match to ordered keys for new dict object. :return: New dict object mapping symbol to value. """ _map = {} for symbol, value in zip(self._symbolic_map.keys(), symbol_values): _map[symbol] = value return _map
[docs] def set_values(self, symbol_values: ndarray): """ Assigns given values in parameters to keys in self._symbolic_map in order, for a new dictionary object. :param symbol_values: Orderd values to match to ordered keys for new dict object. :return: New dict object mapping symbol to value. """ _map = {} for symbol, value in zip(self._symbolic_map.keys(), symbol_values): _map[symbol] = value self._symbolic_map = _map
[docs] def add_value(self, symbol: Symbol, value: float): """ Assigns value to self._symbolic_map[symbol]. If symbol not in object then throws an error. :param symbol: Symbol to have value assigned. :param value: Value to assign to symbol. """ if symbol in self._symbolic_map: self._symbolic_map[symbol] = value else: raise ValueError("Symbol {} not in object.".format(symbol))
[docs] def __str__(self): return f"<SymbolsDict::{len(self._symbolic_map)}>"
[docs] def __repr__(self): return str(self)
[docs] class MeasurementCircuit(object): """ Stores a single measurement circuit that captures one or multiple observable estimations for some Ansatz Circuit. """
[docs] def __init__( self, symbolic_circuit: Circuit, symbols: Optional[SymbolsDict] = None ): """ Stores information required to instantiate any MeasurementCircuit with parameterised symbols. :param symbolic_circuit: Measurement circuit, may or may not have symbolics. :param symbols: SymbolsDict object holding symbols and values for all symbols in Circuit. Default none if circuit not symbolic. """ self._symbolic_circuit: Circuit = symbolic_circuit if not symbols: self._symbols: SymbolsDict = SymbolsDict.symbols_from_circuit( symbolic_circuit ) elif isinstance(symbols, SymbolsDict): self._symbols = symbols else: raise ValueError("Passed symbols object of incorrect type.")
@property def circuit(self) -> Circuit: """ Returns measurement circuit stored in oracle. :return: Circuit in oracle """ return self._symbolic_circuit @property def symbols(self) -> Tuple[Symbol, ...]: """ Converts symbols_list property held in SymbolsDict to a tuple and returns it. :return: All Symbols in object """ return tuple(self._symbols.symbols_list)
[docs] def get_parametric_circuit(self) -> Circuit: """ Substitutes parameters held in SymbolDict into copy of circuit and returns. :return: Substituted circuit """ _circuit = self._symbolic_circuit.copy() _circuit.symbol_substitution(self._symbols._symbolic_map) return _circuit
MeasurementInfo = Tuple[QubitPauliString, List[Bit], bool]
[docs] class ObservableTracker: """ Stores all measurement circuits required to get observable expectations for each QubitPauliString in a given QubitPauliOperator. """
[docs] def __init__(self, qubit_pauli_operator: QubitPauliOperator = QubitPauliOperator()): """ Default constructor, creates an empty dict object for mapping QubitPauliStrings to measurement circuits and the qubits measured to get expectation, along with an empty list for storing measurement circuits and a list for storing partitions. :param qubit_pauli_operator: QubitPauliOperator for which given ObservableTracker is expected to retain measurement circuits all QubitPauliString keys for before any Backend execution. """ self._qubit_pauli_operator = qubit_pauli_operator # indices being index in measurement circuits self._qps_to_indices: Dict[ QubitPauliString, List[Tuple[int, List[Bit], bool]] ] = dict() for k in self._qubit_pauli_operator._dict.keys(): self._qps_to_indices[k] = list() self._measurement_circuits: List[MeasurementCircuit] = list() self._partitions: List[List[QubitPauliString]] = list()
[docs] def from_ObservableTracker(to_copy: "ObservableTracker") -> "ObservableTracker": """ Copies each class attribute from to_copy to self. Returns self. :param to_copy: An alternative ObservableTracker for making a copy of. :return: New ObservableTracker object """ # these variables could be mutated in the first ObservableTracker and effect # this one # To fix, copy everything new_obj = ObservableTracker(copy(to_copy._qubit_pauli_operator)) new_obj._qps_to_indices = copy(to_copy._qps_to_indices) new_obj._measurement_circuits = copy(to_copy._measurement_circuits) new_obj._partitions = copy(to_copy._partitions) return new_obj
[docs] def __str__(self): return ( f"<ObservableTracker::{len(self._measurement_circuits)}MeasurementCircuits>" )
[docs] def __repr__(self): return str(self)
[docs] def clear(self) -> None: """ Erases all held information that is not the qubit pauli operator. """ self._qps_to_indices.clear() for k in self._qubit_pauli_operator._dict.keys(): self._qps_to_indices[k] = list() self._measurement_circuits.clear() self._partitions.clear()
[docs] def modify_coefficients( self, new_coefficients: List[Tuple[QubitPauliString, float]] ): """ Updates coefficients in held QubitPauliOperator with new coefficients. Each QubitPauliString must already be in self._qubit_pauli_operator :param new_coefficients: Each Tuple contains a QubitPauliString a new coefficient. """ for coeff in new_coefficients: if coeff[0] not in self._qubit_pauli_operator._dict: raise ValueError( "Given string {} not held in ObservableTracker object.".format( coeff[0] ) ) self._qubit_pauli_operator._dict[coeff[0]] = _coeff_convert(coeff[1])
[docs] def extend_operator(self, new_operator: QubitPauliOperator): """ Extends self._qubit_pauli_operator to include tuples in passed operator. :param new_operator: Each QubitPauliString and coefficient added to held operator. """ self._qubit_pauli_operator += new_operator
[docs] def remove_strings(self, strings: List[QubitPauliString]): """ Removes passed qubit pauli strings from held QubitPauliOperator and dict from string to index. :param strings: Qubit Pauli Strings no longer required to be measured by ObservableTracker """ for qps in strings: self._qps_to_indices.pop(qps, None) self._qubit_pauli_operator._dict.pop(qps, None)
@property def qubit_pauli_operator(self): """ Returns stored qubit pauli operator :return: QubitPauliOperator object stored in class """ return self._qubit_pauli_operator
[docs] def add_measurement_circuit( self, circuit: MeasurementCircuit, measurement_info: List[MeasurementInfo] ): """ Adds given measurement circuit to stored _measurement_circuits attribute and for each qubit pauli string and qubits in associated strings, updates dictionary between string and its measurement circuit + bit to measure and whether result should be inverted. :param circuit: Measurement circuit to run to get results. :param measurement_info: Each entry contains a QubitPauliString, the bits required to take expectation over in resulting result and a bool signifying whether expectation should be inverted when taking result. """ self._measurement_circuits.append(circuit) index = len(self._measurement_circuits) - 1 # assume that if strings have the same circuit, they must commute self._partitions.append([m[0] for m in measurement_info]) for s in measurement_info: string = s[0] bits = s[1] invert = s[2] if s[0] not in self._qps_to_indices: raise ValueError( "ObservableTracker object does not track {}.".format(s[0]) ) self._qps_to_indices[string].append((index, bits, invert))
[docs] def get_measurement_circuits( self, string: QubitPauliString ) -> List[MeasurementCircuit]: """ Returns the measurements required to be run for a single QubitPauliString's expectation. :param string: QubitPauliString of interest. :return: Measurement Circuit run to find expection of QubitPauliString for some undefined ansatz circuit. """ indices = [t[0] for t in self._qps_to_indices[string]] circuits = [self._measurement_circuits[i] for i in indices] return circuits
[docs] def check_string(self, string: QubitPauliString) -> bool: """ Returns true if given QubitPauliString has a measurement circuit stored in self._measurement_circuits. :param string: Operator measurement circuit existence being checked for. :return: True if string has measurement circuit, false if not. """ if string not in self._qps_to_indices: return False if len(self._qps_to_indices[string]) > 0: return True else: return False
[docs] def get_empty_strings(self) -> List[QubitPauliString]: """ Returns all strings in operator that don't have some assigned MeasurementCircuit. :return: Strings that require some MeasurementCircuit to be set """ output = [] for string in self._qubit_pauli_operator._dict: if not self.check_string(string): output.append(string) return output
@property def measurement_circuits(self) -> List[MeasurementCircuit]: """ Returns all measurement circuits aded to ObservableTracker via get_measurement_circuit. :return: All measurement circuits held in ObservableTracker self._measurement_circuits attirbute. """ return self._measurement_circuits
[docs] def get_expectations(self, results: List[BackendResult]) -> QubitPauliOperator: """ For given list of results, returns a QubitPauliOperator giving an expectation for each QubitPauliString held in self._qps_to_indices. Expectation derived by taking parity of counts. :param results: Result objects to derive counts and then an expectation from. :return: Expectation for each QubitPauliString in self._qps_to_indices """ max_index = len(results) - 1 results_dict = dict() # find expectation for each qubit pauli string stored in dict for qps in self._qps_to_indices: expectation = 0 # measure info stores result index of interest, bits of choice and # whether result should be inverted for measure_info in self._qps_to_indices[qps]: result_index = measure_info[0] # suggests something has gone wrong with MitEx piping of tasks if result_index > max_index: raise ValueError( "Desired index {} greater than max index {} of results.".format( result_index, max_index ) ) result = results[result_index] bits = measure_info[1] invert = measure_info[2] counts = result.get_counts(bits) expectation += ((-1) ** invert) * expectation_from_counts(counts) # once expectation has been derived from all suitable results, add to dictionary coeff = self._qubit_pauli_operator[qps] results_dict[qps] = expectation * coeff # package in QubitPauliOperator as MitEx uses return QubitPauliOperator(results_dict)