from qiskit import *
from qiskit.visualization import *
from math import *

# A simple example Quantum Circuit that is used in many code snippets below
example_qc = QuantumCircuit(2, 1)
example_qc.h(0)
example_qc.cx(0, 1)
example_qc.measure(1, 0)
None

# Specify number of qubits and classical bits
qc = QuantumCircuit(2, 1)      # Quantum circuit with 2 qubits and 1 classical bit

# In any of these examples add qc.draw() at the end to see better what is happening:
# qc.draw()

# Specify only number of qubits
# Number of classical bits is zero by default
qc = QuantumCircuit(2)         # Quantum circuit with 2 qubits, and 0 classical bits

# Quantum circuit with two quantum registers (a and b) and one classical register (c)
a = QuantumRegister(2, "a")    # 2 qubits in register, name is "a"
b = QuantumRegister(3, "b")    # 3 qubits in register, name is "b"
c = ClassicalRegister(1, "c")  # 1 classical bit in register, name is "c"
qc = QuantumCircuit(a, b, c)   # Quantum circuit with register a, b, and c

# Give a name to the quantum circuit (not shown in draw or print)
qc = QuantumCircuit(2, 2, name="my-quantum-circuit")

# Apply a global phase to the quantum circuit
# This is reported in draw() and print() and this is relevant when composing circuits
qc = QuantumCircuit(2, 2, global_phase=pi/4)

# Initialize to state provided as explicit vector
qc = QuantumCircuit(2)
qc.initialize([1, 0], 0)   # Initialize qubit number 0 to [1, 0] = |0>
qc.initialize([0, 1], 1)   # Initialize qubit number 1 to [0, 1] = |1>

# Many example end with "None" to avoid printing the return value of the last statement
# In this case, qc.intialize() returns an InstructionSet object
None

# Reset qubit
qc = QuantumCircuit(1)
qc.reset(0)
None

# Initalize to string label
# TODO: This is supposed to work, but it throws an exception
# qc = QuantumCircuit(2)
# qc.initialize('01', qc.qubits)  # Initialize qubit 0 to |1> and qubit 1 to |0>
# qc.initialize('+-', qc.qubits)  # Initialize qubit 0 to |+> and qubit 1 to |->
# qc.initialize('rl', qc.qubits)  # Initialize qubit 0 to |r> = |i> and qubit 1 to |l> = |-i>
None

# Apply a gate to a single qubit
qc = QuantumCircuit(1)
qc.x(0)   # Apply X = NOT gate to qubit number 0
None      # qc.x function returns an InstructionSet which I don't want to be printed

# Apply a gate to two qubits
qc = QuantumCircuit(2)
qc.cx(0, 1)    # Apply CX = controlled X = controlled NOT gate, qubit 0 is control, qubit 1 is target
None

# Apply a gate to three qubits
qc = QuantumCircuit(3)
qc.ccx(0, 1, 2)   # Apply CCX = controlled controlled X = Toffoli gate, qubits 0 and 1 are control, qubit 2 is target 
None

# Single qubit gates

qc = QuantumCircuit(1)
qc.x(0)         # X gate = NOT gate = 180 degree rotation around X-axis
qc.y(0)         # Y gate = 180 degree rotation around Y-axis
qc.z(0)         # Z gate = 180 degree rotation around Z-axis
qc.s(0)         # S gate = 90 degree positive rotation around Z-axis
qc.sdg(0)       # S-dagger gate = 90 degree negative rotation around Z-axis
qc.t(0)         # T gate = 45 degree positive rotation around Z-axis
qc.tdg(0)       # T-dagger gate = 45 degree negative rotation around Z-axis
qc.h(0)         # H gate = Hadamard gate
qc.i(0)         # I gate = Identity gate (does nothing)
qc.id(0)        # Identity gate (synonym for I gate)

# Parameterized gates
qc.p(pi/4, 0)   # P gate = Phase gate: first argument is phase
qc.rx(pi/4, 0)  # Rotate around X-axis by some angle
qc.ry(pi/4, 0)  # Rotate around Y-axis by some angle
qc.rz(pi/4, 0)  # Rotate around Z-axis by some angle

None

# Two qubit gates
qc = QuantumCircuit(2)

# "True" two qubit gates
qc.swap(0, 1)        # Swap gate
qc.iswap(0, 1)       # Iswap gate

# Controlled single qubit gates 
# First argument is control qubit second argument is target qubit
qc.cx(0, 1)          # Controlled-X = Controlled-NOT
qc.cnot(0, 1)        # Controller-NOT = Controlled-X (synonym for cx)
qc.cy(0, 1)          # Controlled-Y
qc.cz(0, 1)          # Controlled-Z (Note control and target are symmetric due to phase kick-back)
qc.ch(0, 1)          # Controlled-H = Controlled-Hadamard

# Controlled rotations around various axes / controlled phase
# First arg is phase, second is control qubit, third is target qubit
qc.crx(pi/8, 0, 1)   # Controlled rotation around x-axis
qc.cry(pi/8, 0, 1)   # Controlled rotation around y-axis
qc.crz(pi/8, 0, 1)   # Controlled rotation around z-axis
qc.cp(pi/8, 0, 1)    # Controlled-Phase 
qc.csx(0, 1)         # Controlled-SX gate = Controlled square-root-of-X gate

None

# Three qubit gates

# Double controlled single qubit gates
# First and second argument are control qubits, third argument is target qubit
qc = QuantumCircuit(3)
qc.ccx(0, 1, 2)      # Controlled-controlled-X = controlled-controlled-NOT = Tofolli gate

# Controlled two qubit gates
# First argument is control qubit, second and third argument are target qubits
qc.cswap(0, 1, 2)    # Controlled-swap
qc.fredkin(0, 1, 2)  # Fredkin gate = synonym for controlled-swap

None

# TODO: more gates
# cu gate
# cu1 gate
# cu3 gate
# dcx gate
# ecr gate
# mcp gate
# mcrx gate
# mcry gate
# mcrz gate
# mct gate
# mcu1 gate
# mcx gate
# ms gate
# pauli gate
# r gate
# rccx gate
# rcccx gate
# rv gate
# rxx gate
# ryy gate
# rzz gate
# rzx gate
# sx gate
# sxdg gate
# u gate
# u1 gate
# u2 gate
# uc gate
# ucrx gate
# ucry gate
# ucrz gate

# Measure one qubit in existing classical bit
# Measure qubit number 0 into classical bit number 0
qc = QuantumCircuit(1, 1)
qc.measure(0, 0)   
None

# Measure multiple qubits into multiple existing classical bits
# Measure qubit number 0 into classical bit number 0, and qubit number 1 into classical bit number 1
qc = QuantumCircuit(2, 2)
qc.measure([0, 1], [0, 1])  
None

# Measure quantum register into existing classical register
quantum_register = QuantumRegister(2)
classical_register = ClassicalRegister(2)
qc = QuantumCircuit(quantum_register, classical_register)
qc.measure(quantum_register, classical_register)
None

# Create barrier and measure all qubits into newly created classical bits "meas"
qc = QuantumCircuit(2)
qc.measure_all()
None

# Create barrier and measure all qubits that have at least one gate into newly created classical bits "meas"
qc = QuantumCircuit(2)
qc.x(0)
qc.measure_active()
None

# Use Python control logic to procedurally generate a circuit
nr_qubits = 6
qc = QuantumCircuit(nr_qubits)
for i in range(nr_qubits-1):
    qc.h(i)
    qc.cx(i, nr_qubits-1)

# Draw a quantum circuit
example_qc.draw()

# Draw multiple quantum circuits
# Without the display calls, only the last draw is shown
display(example_qc.draw())
display(example_qc.draw())

# Draw a scaled quantum circuit
example_qc.draw(scale=0.5)

# Draw a quantum circuit as text
example_qc.draw(output="text")

# Print a quantum circuit (same as draw as text)
print(example_qc)

# Execute by invoking execute method, passing in quantum circuit and backend
backend = Aer.get_backend("qasm_simulator")
job = execute(example_qc, backend)
result = job.result()

# Execute by compiling to object code and then invoking backend run method, passing in object code
backend = Aer.get_backend("qasm_simulator")
qobj = assemble(example_qc)
job = backend.run(qobj)
result = job.result()

# Features of qasm_simulator backend:
# - Simulates a real (noisy) hardware device
# - No access to state vector, only to measurement results
# - TODO: more
backend = Aer.get_backend("qasm_simulator")   # Note: synonymous with "aer_simulator" TODO: is that so?
result = execute(example_qc, backend).result()

# Features of statevector_simulator backend:
# - TODO
backend = Aer.get_backend("statevector_simulator")   # Note: synonymous with "aer_simulator_statevector"
result = execute(example_qc, backend).result()

# Features of unitary_simulator backend:
# - TODO
backend = Aer.get_backend("unitary_simulator")   # Note: synonymous with "aer_simulator_unitary"
result = execute(example_qc, backend).result()

# Plot histogram of measurement results
# Available with all backends
backend = Aer.get_backend("qasm_simulator")
job = execute(example_qc, backend)
result = job.result()
counts = result.get_counts()
plot_histogram(counts)

# Plot Bloch multivector for a quantum state
# Only available on backends where the internal statevector is available
backend = Aer.get_backend("statevector_simulator")
job = execute(example_qc, backend)
result = job.result()
statevector = result.get_statevector()
plot_bloch_multivector(statevector)

# Plot a state city for a quantum state
# Only available on backends where the internal statevector is available
qc = QuantumCircuit(2)
qc.h(0)
qc.z(0)
qc.cx(0, 1)
backend = Aer.get_backend("statevector_simulator")
job = execute(qc, backend)
result = job.result()
statevector = result.get_statevector()
plot_state_city(statevector)

# Plot a hinton diagram for the density matrix of a quantum state
# Only available on backends where the internal statevector is available
qc = QuantumCircuit(2)
qc.h(0)
qc.z(0)
qc.cx(0, 1)
backend = Aer.get_backend("statevector_simulator")
job = execute(qc, backend)
result = job.result()
statevector = result.get_statevector()
plot_state_hinton(statevector)

# Plot the paulivec representation of a quantum state
# Only available on backends where the internal statevector is available
qc = QuantumCircuit(2)
qc.h(0)
qc.z(0)
qc.cx(0, 1)
backend = Aer.get_backend("statevector_simulator")
job = execute(qc, backend)
result = job.result()
statevector = result.get_statevector()
plot_state_paulivec(statevector)

# Plot the qsphere representation of a quantum state
# Only available on backends where the internal statevector is available
qc = QuantumCircuit(2)
qc.h(0)
qc.z(0)
qc.cx(0, 1)
backend = Aer.get_backend("statevector_simulator")
job = execute(qc, backend)
result = job.result()
statevector = result.get_statevector()
plot_state_qsphere(statevector)

# TODO: Schedule a jon on real hardware

# TODO: Monitor progress of a job

# TODO: Gate class, circuit to gate, ...

# TODO: classes Parameter, ParameterVector, ParameterExpression, 