# Path: openfermioncirq/gates/fermionic_simulation.py
# def sum_of_interaction_operator_gate_generators(
#         n_modes: int,
#         gates: Dict[Tuple[int, ...], Union[float, cirq.Gate]],
# ) -> openfermion.InteractionOperator:
#     """The interaction operator that is the sum of the generators of the
#     specified fermionic simulation gates.
# 
#     The gates are specified as a dictionary whose items are (indices, gate),
#     where
#         * indices is a tuple of ints specifying the modes on which the gate
#           acts;
#         * gate is one of type
#           - float, which is interpreted as a constant, regardless of the
#             indices,
#           - cirq.ZPowGate, which is interpreted as a "linear" fermionic
#             simulation gate,
#           - openfermioncirq.InteractionOperatorFermionicGate.
# 
#     Args:
#         n_modes: The number of modes.
#         gates: The gates.
# 
#     Returns:
#         The interaction operator.
#     """
#     # assumes gate indices in JW order
#     operator = openfermion.InteractionOperator.zero(n_modes)
# 
#     for indices, gate in gates.items():
#         if not indices:
#             operator.constant += gate
#         elif isinstance(gate, cirq.ZPowGate):
#             coeff = gate._exponent * np.pi
#             operator.constant += gate._exponent * gate._global_shift * np.pi
#             operator.one_body_tensor[indices * 2] += coeff
#         elif isinstance(gate, InteractionOperatorFermionicGate):
#             gate.interaction_operator_generator(operator=operator,
#                                                 modes=indices)
#         else:
#             raise TypeError(f'Gate type {gate} not supported.')
# 
#     return operator
# 
# def state_swap_eigen_component(x: str, y: str, sign: int = 1, angle: float = 0):
#     """The +/- eigen-component of the operation that swaps states x and y.
# 
#     For example, state_swap_eigen_component('01', '10', ±1) with angle θ returns
#         ┌                               ┐
#         │0, 0,           0,            0│
#         │0, 0.5,         ±0.5 e^{-iθ}, 0│
#         │0, ±0.5 e^{iθ}, 0.5,          0│
#         │0, 0,           0,            0│
#         └                               ┘
# 
#     Args:
#         x: The first state to swap, as a bitstring.
#         y: The second state to swap, as a bitstring. Must have high index than
#             x.
#         sign: The sign of the off-diagonal elements (indicated by +/-1).
#         angle: The phase of the complex off-diagonal elements. Defaults to 0.
# 
#     Returns: The eigen-component.
# 
#     Raises:
#         ValueError:
#             * x and y have different lengths
#             * x or y contains a character other than '0' and '1'
#             * x and y are the same
#             * sign is not -1 or 1
#         TypeError: x or y is not a string
#     """
#     if not (isinstance(x, str) and isinstance(y, str)):
#         raise TypeError('not (isinstance(x, str) and isinstance(y, str))')
#     if len(x) != len(y):
#         raise ValueError('len(x) != len(y)')
#     if set(x).union(y).difference('01'):
#         raise ValueError('Arguments must be 0-1 strings.')
#     if x == y:
#         raise ValueError('x == y')
#     if sign not in (-1, 1):
#         raise ValueError('sign not in (-1, 1)')
# 
#     dim = 2**len(x)
#     i, j = int(x, 2), int(y, 2)
# 
#     component = np.zeros((dim, dim), dtype=np.complex128)
#     component[i, i] = component[j, j] = 0.5
#     component[j, i] = sign * 0.5 * 1j**(angle * 2 / np.pi)
#     component[i, j] = sign * 0.5 * 1j**(-angle * 2 / np.pi)
#     return component


import itertools
import cirq
import cirq.contrib.acquaintance as cca
import numpy as np
import openfermion
import pytest
import scipy.linalg as la
import sympy
import openfermioncirq as ofc
from typing import cast, Tuple
from openfermioncirq.gates.fermionic_simulation import (
    sum_of_interaction_operator_gate_generators,
    state_swap_eigen_component,
)
])
def test_state_swap_eigen_component(index_pair, n_qubits):
    state_pair = tuple(format(i, '0' + str(n_qubits) + 'b') for i in index_pair)
    i, j = index_pair
    dim = 2**n_qubits
    for sign in (-1, 1):
        actual_component = state_swap_eigen_component(state_pair[0],
                                                      state_pair[1], sign)
        expected_component = np.zeros((dim, dim))
        expected_component[i, i] = expected_component[j, j] = 0.5
        expected_component[i, j] = expected_component[j, i] = sign * 0.5
        assert np.allclose(actual_component, expected_component)


@pytest.mark.parametrize('n_modes, seed',
                         [(7, np.random.randint(1 << 30)) for _ in range(2)])
def test_interaction_operator_interconversion(n_modes, seed):
    operator = openfermion.random_interaction_operator(n_modes,
                                                       real=False,
                                                       seed=seed)
    gates = ofc.fermionic_simulation_gates_from_interaction_operator(operator)
    other_operator = sum_of_interaction_operator_gate_generators(n_modes, gates)
    operator = openfermion.normal_ordered(operator)
    other_operator = openfermion.normal_ordered(other_operator)
    assert operator == other_operator


def test_interaction_operator_from_bad_gates():
    for gates in [{(): 'bad'}, {(0,): cirq.X}]:
        with pytest.raises(TypeError):

# Path: openfermioncirq/gates/fermionic_simulation.py
# def sum_of_interaction_operator_gate_generators(
#         n_modes: int,
#         gates: Dict[Tuple[int, ...], Union[float, cirq.Gate]],
# ) -> openfermion.InteractionOperator:
#     """The interaction operator that is the sum of the generators of the
#     specified fermionic simulation gates.
# 
#     The gates are specified as a dictionary whose items are (indices, gate),
#     where
#         * indices is a tuple of ints specifying the modes on which the gate
#           acts;
#         * gate is one of type
#           - float, which is interpreted as a constant, regardless of the
#             indices,
#           - cirq.ZPowGate, which is interpreted as a "linear" fermionic
#             simulation gate,
#           - openfermioncirq.InteractionOperatorFermionicGate.
# 
#     Args:
#         n_modes: The number of modes.
#         gates: The gates.
# 
#     Returns:
#         The interaction operator.
#     """
#     # assumes gate indices in JW order
#     operator = openfermion.InteractionOperator.zero(n_modes)
# 
#     for indices, gate in gates.items():
#         if not indices:
#             operator.constant += gate
#         elif isinstance(gate, cirq.ZPowGate):
#             coeff = gate._exponent * np.pi
#             operator.constant += gate._exponent * gate._global_shift * np.pi
#             operator.one_body_tensor[indices * 2] += coeff
#         elif isinstance(gate, InteractionOperatorFermionicGate):
#             gate.interaction_operator_generator(operator=operator,
#                                                 modes=indices)
#         else:
#             raise TypeError(f'Gate type {gate} not supported.')
# 
#     return operator
# 
# def state_swap_eigen_component(x: str, y: str, sign: int = 1, angle: float = 0):
#     """The +/- eigen-component of the operation that swaps states x and y.
# 
#     For example, state_swap_eigen_component('01', '10', ±1) with angle θ returns
#         ┌                               ┐
#         │0, 0,           0,            0│
#         │0, 0.5,         ±0.5 e^{-iθ}, 0│
#         │0, ±0.5 e^{iθ}, 0.5,          0│
#         │0, 0,           0,            0│
#         └                               ┘
# 
#     Args:
#         x: The first state to swap, as a bitstring.
#         y: The second state to swap, as a bitstring. Must have high index than
#             x.
#         sign: The sign of the off-diagonal elements (indicated by +/-1).
#         angle: The phase of the complex off-diagonal elements. Defaults to 0.
# 
#     Returns: The eigen-component.
# 
#     Raises:
#         ValueError:
#             * x and y have different lengths
#             * x or y contains a character other than '0' and '1'
#             * x and y are the same
#             * sign is not -1 or 1
#         TypeError: x or y is not a string
#     """
#     if not (isinstance(x, str) and isinstance(y, str)):
#         raise TypeError('not (isinstance(x, str) and isinstance(y, str))')
#     if len(x) != len(y):
#         raise ValueError('len(x) != len(y)')
#     if set(x).union(y).difference('01'):
#         raise ValueError('Arguments must be 0-1 strings.')
#     if x == y:
#         raise ValueError('x == y')
#     if sign not in (-1, 1):
#         raise ValueError('sign not in (-1, 1)')
# 
#     dim = 2**len(x)
#     i, j = int(x, 2), int(y, 2)
# 
#     component = np.zeros((dim, dim), dtype=np.complex128)
#     component[i, i] = component[j, j] = 0.5
#     component[j, i] = sign * 0.5 * 1j**(angle * 2 / np.pi)
#     component[i, j] = sign * 0.5 * 1j**(-angle * 2 / np.pi)
#     return component


# Path: openfermioncirq/gates/fermionic_simulation_test.py
import itertools
import cirq
import cirq.contrib.acquaintance as cca
import numpy as np
import openfermion
import pytest
import scipy.linalg as la
import sympy
import openfermioncirq as ofc
from typing import cast, Tuple
from openfermioncirq.gates.fermionic_simulation import (
    sum_of_interaction_operator_gate_generators,
    state_swap_eigen_component,
)

])
def test_state_swap_eigen_component(index_pair, n_qubits):
    state_pair = tuple(format(i, '0' + str(n_qubits) + 'b') for i in index_pair)
    i, j = index_pair
    dim = 2**n_qubits
    for sign in (-1, 1):
        actual_component = state_swap_eigen_component(state_pair[0],
                                                      state_pair[1], sign)
        expected_component = np.zeros((dim, dim))
        expected_component[i, i] = expected_component[j, j] = 0.5
        expected_component[i, j] = expected_component[j, i] = sign * 0.5
        assert np.allclose(actual_component, expected_component)


@pytest.mark.parametrize('n_modes, seed',
                         [(7, np.random.randint(1 << 30)) for _ in range(2)])
def test_interaction_operator_interconversion(n_modes, seed):
    operator = openfermion.random_interaction_operator(n_modes,
                                                       real=False,
                                                       seed=seed)
    gates = ofc.fermionic_simulation_gates_from_interaction_operator(operator)
    other_operator = sum_of_interaction_operator_gate_generators(n_modes, gates)
    operator = openfermion.normal_ordered(operator)
    other_operator = openfermion.normal_ordered(other_operator)
    assert operator == other_operator


def test_interaction_operator_from_bad_gates():
    for gates in [{(): 'bad'}, {(0,): cirq.X}]:
        with pytest.raises(TypeError):

            sum_of_interaction_operator_gate_generators(5, gates)