Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Symbolic simulator #292

Merged
merged 11 commits into from
Apr 4, 2023
3 changes: 2 additions & 1 deletion tangelo/helpers/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,9 +73,10 @@ def new_func(*args, **kwargs):


# List all built-in backends supported
all_backends = {"qulacs", "qiskit", "cirq", "braket", "projectq", "qdk", "pennylane"}
all_backends = {"qulacs", "qiskit", "cirq", "braket", "projectq", "qdk", "pennylane", "sympy"}
all_backends_simulator = {"qulacs", "qiskit", "cirq", "qdk"}
sv_backends_simulator = {"qulacs", "qiskit", "cirq"}
symbolic_backends = {"sympy"}

# Dictionary mapping package names to their identifier in this module
packages = {p: p for p in all_backends}
Expand Down
3 changes: 2 additions & 1 deletion tangelo/linq/target/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,11 @@
from .target_qiskit import QiskitSimulator
from .target_qulacs import QulacsSimulator
from .target_qdk import QDKSimulator
from .target_sympy import SympySimulator
from tangelo.helpers.utils import all_backends_simulator


target_dict = {"qiskit": QiskitSimulator, "cirq": CirqSimulator, "qdk": QDKSimulator, "qulacs": QulacsSimulator}
target_dict = {"qiskit": QiskitSimulator, "cirq": CirqSimulator, "qdk": QDKSimulator, "qulacs": QulacsSimulator, "sympy": SympySimulator}
alexfleury-sb marked this conversation as resolved.
Show resolved Hide resolved

# Generate backend info dictionary
backend_info = {sim_id: target_dict[sim_id].backend_info() for sim_id in all_backends_simulator}
111 changes: 111 additions & 0 deletions tangelo/linq/target/target_sympy.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
# Copyright 2023 Good Chemistry Company.
#
# 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.

import numpy as np

from tangelo.linq import Circuit
from tangelo.linq.target.backend import Backend
from tangelo.linq.translator import translate_circuit as translate_c
from tangelo.linq.translator import translate_operator


class SympySimulator(Backend):

def __init__(self, n_shots=None, noise_model=None):
super().__init__(n_shots, noise_model)

def simulate_circuit(self, source_circuit: Circuit, return_statevector=False, initial_statevector=None):
"""This simulator manipulates symbolic expressions, i.e. gates can have
unspecified parameters (strings interpreted as variables). As with the
other simulators, it performs state preparation corresponding to the
input circuit, returns the frequencies of the different observables, and
either the statevector or None depending on if return_statevector is set
to True.

Args:
source_circuit (Circuit): A circuit in the abstract format to be
translated for the target backend.
return_statevector (bool): Option to return the statevector as well,
if available.
initial_statevector (array/matrix or sympy.physics.quantum.Qubit): A
valid statevector in the format supported by the target backend.

Returns:
dict: A dictionary mapping multi-qubit states to their corresponding
frequency.
sympy.Matrix: The symbolic statevector, if requested
by the user (if not, set to None).
"""

from sympy import simplify
ValentinS4t1qbit marked this conversation as resolved.
Show resolved Hide resolved
from sympy.physics.quantum import qapply
from sympy.physics.quantum.qubit import Qubit, matrix_to_qubit, \
qubit_to_matrix, measure_all

translated_circuit = translate_c(source_circuit, "sympy")

# Transform the initial_statevector if it is provided.
if initial_statevector is None:
python_statevector = Qubit("0"*(source_circuit.width))
elif isinstance(initial_statevector, Qubit):
python_statevector = initial_statevector
elif isinstance(initial_statevector, (np.ndarray, np.matrix)):
python_statevector = matrix_to_qubit(initial_statevector)
else:
raise ValueError(f"The {type(initial_statevector)} type for initial_statevector is not supported.")

# Deterministic circuit, run once.
state = qapply(translated_circuit * python_statevector)
self._current_state = state
python_statevector = qubit_to_matrix(state)

measurements = measure_all(state)

frequencies = dict()
for vec, prob in measurements:
prob = simplify(prob, tolerance=1e-4)
bistring = "".join(str(bit) for bit in reversed(vec.qubit_values))
frequencies[bistring] = prob

return (frequencies, python_statevector) if return_statevector else (frequencies, None)

def expectation_value_from_prepared_state(self, qubit_operator, n_qubits, prepared_state=None):
"""Compute an expectation value using a representation of the state
using sympy functionalities.

Args:
qubit_operator (QubitOperator): a qubit operator in tangelo format
n_qubits (int): Number of qubits.
prepared_state (array/matrix or sympy.physics.quantum.Qubit): A
numpy or a sympy object representing the state. Internally, a
numpy object is transformed into the sympy representation.
Default is None, in this case it is set to the current state in
the simulator object.

Returns:
sympy.core.add.Add: Eigenvalue represented as a symbolic sum.
"""

from sympy import simplify
from sympy.physics.quantum import qapply, Dagger

prepared_state = self._current_state if prepared_state is None else prepared_state
operator = translate_operator(qubit_operator, source="tangelo", target="sympy", n_qubits=n_qubits)
eigenvalue = qapply(Dagger(prepared_state) * operator * prepared_state)

return simplify(eigenvalue)

@staticmethod
def backend_info():
return {"statevector_available": True, "statevector_order": "lsq_first", "noisy_simulation": False}
82 changes: 82 additions & 0 deletions tangelo/linq/tests/test_symbolic_simulator.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
# Copyright 2023 Good Chemistry Company.
#
# 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.

"""A test class to check that the simulator class functionalities are behaving
as expected for the symbolic backend.
"""

import unittest
from math import pi

from tangelo.helpers.utils import assert_freq_dict_almost_equal
from tangelo.linq import Gate, Circuit
from tangelo.helpers.utils import installed_backends
from tangelo.linq.target.target_sympy import SympySimulator


class TestSymbolicSimulate(unittest.TestCase):
ValentinS4t1qbit marked this conversation as resolved.
Show resolved Hide resolved

@unittest.skipIf("sympy" not in installed_backends, "Test Skipped: Sympy backend not available \n")
def test_simple_simulate(self):
"""Test simulate of a simple rotation gate with a symbolic parameter."""

from sympy import symbols, cos, sin

simple_circuit = Circuit([Gate("RY", 0, parameter="alpha")])
backend = SympySimulator()
probs, _ = backend.simulate(simple_circuit, return_statevector=False)

alpha = symbols("alpha", real=True)

self.assertDictEqual(probs, {"0": (cos(alpha/2))**2, "1": (sin(alpha/2))**2})
alexfleury-sb marked this conversation as resolved.
Show resolved Hide resolved

@unittest.skipIf("sympy" not in installed_backends, "Test Skipped: Sympy backend not available \n")
def test_simulate_with_control(self):
"""Test simulate of a control rotation gate with a symbolic parameter."""

from sympy import symbols, cos, sin

backend = SympySimulator()

no_action_circuit = Circuit([Gate("CRY", 1, 0, parameter="alpha")])
no_action_probs, _ = backend.simulate(no_action_circuit, return_statevector=False)

self.assertDictEqual(no_action_probs, {"00": 1.})

action_circuit = Circuit([Gate("X", 0), Gate("CRY", 1, 0, parameter="alpha")])
action_probs, _ = backend.simulate(action_circuit, return_statevector=False)
alpha = symbols("alpha", real=True)

self.assertDictEqual(action_probs, {"10": (cos(alpha/2))**2, "11": (sin(alpha/2))**2})

@unittest.skipIf("sympy" not in installed_backends, "Test Skipped: Sympy backend not available \n")
def test_evaluate_bell_state(self):
"""Test the numerical evaluation to a known state (Bell state)."""

backend = SympySimulator()

variable_bell_circuit = Circuit([Gate("RY", 0, parameter="alpha"), Gate("CNOT", 1, 0)])
variable_bell_probs, _ = backend.simulate(variable_bell_circuit, return_statevector=False)

# Replace alpha by pi/2.
numerical_bell_probs = {
bitstring: prob.subs(list(prob.free_symbols)[0], pi/2) for
bitstring, prob in variable_bell_probs.items()
}

assert_freq_dict_almost_equal(numerical_bell_probs, {"00": 0.5, "11": 0.5}, atol=1e-3)


if __name__ == "__main__":
unittest.main()
17 changes: 17 additions & 0 deletions tangelo/linq/tests/test_translator_circuit.py
Original file line number Diff line number Diff line change
Expand Up @@ -501,6 +501,23 @@ def circuit(ops):
# Compare statevectors
np.testing.assert_array_almost_equal(v1, reference_big_lsq, decimal=6)

@unittest.skipIf("sympy" not in installed_backends, "Test Skipped: Sympy backend not available \n")
def test_to_sympy(self):
"""Translate abtract format to sympy format."""

from sympy.physics.quantum.gate import HadamardGate, XGate, YGate, ZGate, CNotGate

# Equivalent native sympy circuit.
ref_circ = ZGate(3) * YGate(1) * XGate(0) * CNotGate(0, 1) * HadamardGate(2)

gates = [Gate("H", 2), Gate("CNOT", 1, control=0), Gate("X", 0), Gate("Y", 1), Gate("Z", 3)]
abs_circ = Circuit(gates)

# Generate the sympy circuit by translating from the abstract one.
translated_circuit = translate_c(abs_circ, "sympy")

self.assertEqual(translated_circuit, ref_circ)


if __name__ == "__main__":
unittest.main()
16 changes: 11 additions & 5 deletions tangelo/linq/tests/test_translator_perf.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@

from tangelo.linq import Gate, Circuit
from tangelo.toolboxes.operators import QubitOperator
from tangelo.helpers.utils import installed_backends
from tangelo.helpers.utils import symbolic_backends
from tangelo.linq import translate_operator, translate_circuit
from tangelo.linq.translator.translate_qubitop import FROM_TANGELO as FROM_TANGELO_OP
from tangelo.linq.translator.translate_qubitop import TO_TANGELO as TO_TANGELO_OP
Expand All @@ -47,12 +47,15 @@
class PerfTranslatorTest(unittest.TestCase):

def test_perf_operator(self):
""" Performance test with a reasonable large input for operator """
""" Performance test with a reasonable large input for operator.
Symbolic backends are not included in this test.
"""

print(f'\n[Performance Test :: linq operator format conversion]')
print(f'\tInput size: n_qubits={n_qubits_op}, n_terms={n_terms}\n')

for f in FROM_TANGELO_OP:
perf_backends = FROM_TANGELO_OP.keys() - symbolic_backends
for f in perf_backends:
try:
tstart = time.time()
target_op = translate_operator(tangelo_op, source="tangelo", target=f)
Expand All @@ -69,12 +72,15 @@ def test_perf_operator(self):
continue

def test_perf_circuit(self):
""" Performance test with a reasonable large input for quantum circuit """
""" Performance test with a reasonable large input for quantum circuit.
Symbolic backends are not included in this test.
"""

print(f'\n[Performance Test :: linq circuit format conversion]')
print(f'\tInput size: n_qubits={tangelo_c.width}, n_gates={tangelo_c.size}\n')

for f in FROM_TANGELO_C:
perf_backends = FROM_TANGELO_C.keys() - symbolic_backends
for f in perf_backends:
try:
tstart = time.time()
target_c = translate_circuit(tangelo_c, source="tangelo", target=f)
Expand Down
2 changes: 2 additions & 0 deletions tangelo/linq/translator/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
from .translate_qubitop import translate_operator
from .translate_circuit import translate_circuit
from .translate_pennylane import get_pennylane_gates
from .translate_sympy import get_sympy_gates


def get_supported_gates():
Expand All @@ -40,5 +41,6 @@ def get_supported_gates():
supported_gates["cirq"] = sorted(get_cirq_gates().keys())
supported_gates["braket"] = sorted(get_braket_gates().keys())
supported_gates["pennylane"] = sorted(get_pennylane_gates().keys())
supported_gates["sympy"] = sorted(get_sympy_gates().keys())

return supported_gates
4 changes: 3 additions & 1 deletion tangelo/linq/translator/translate_circuit.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
from tangelo.linq.translator.translate_qiskit import translate_c_to_qiskit, translate_c_from_qiskit
from tangelo.linq.translator.translate_qulacs import translate_c_to_qulacs
from tangelo.linq.translator.translate_pennylane import translate_c_to_pennylane
from tangelo.linq.translator.translate_sympy import translate_c_to_sympy


FROM_TANGELO = {
Expand All @@ -34,7 +35,8 @@
"qdk": translate_c_to_qsharp,
"qiskit": translate_c_to_qiskit,
"qulacs": translate_c_to_qulacs,
"pennylane": translate_c_to_pennylane
"pennylane": translate_c_to_pennylane,
"sympy": translate_c_to_sympy
}

TO_TANGELO = {
Expand Down
6 changes: 4 additions & 2 deletions tangelo/linq/translator/translate_qubitop.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,14 +20,16 @@
from tangelo.linq.translator.translate_qulacs import translate_op_from_qulacs, translate_op_to_qulacs
from tangelo.linq.translator.translate_pennylane import translate_op_from_pennylane, translate_op_to_pennylane
from tangelo.linq.translator.translate_projectq import translate_op_from_projectq, translate_op_to_projectq
from tangelo.linq.translator.translate_sympy import translate_op_to_sympy


FROM_TANGELO = {
"qiskit": translate_op_to_qiskit,
"cirq": translate_op_to_cirq,
"qulacs": translate_op_to_qulacs,
"pennylane": translate_op_to_pennylane,
"projectq": translate_op_to_projectq
"projectq": translate_op_to_projectq,
"sympy": translate_op_to_sympy
}

TO_TANGELO = {
Expand Down Expand Up @@ -71,7 +73,7 @@ def translate_operator(qubit_operator, source, target, n_qubits=None):
raise NotImplementedError(f"Qubit operator conversion from {source} to {target} is not supported.")

# For translation functions that need an explicit number of qubits.
if target in {"qiskit"}:
if target in {"qiskit", "sympy"}:
# The count_qubits function has no way to detect the number of
# qubits when an operator is only a tensor product of I.
if qubit_operator == QubitOperator((), qubit_operator.constant):
Expand Down
Loading