# Bundle ckts into single job
qc_list = [qc1, qc2]
job = execute(qc_list, backend)
job.result().get_counts()
# Counts come here, as list of counts objects
job.result().get_counts(qc1) # gives only one ckt results
# to transpile, assemble and run qckts seperatl
tqc_list = transpile(qc_list, vigo) # returns a list of transpiled quantm ckts
qobj = assemble(tqc_list, vigo)
vigo.run(qobj)
job.result().get_counts()

# Visualise qckts using latex
qc.draw('latex') #needs pylatexenc, pillow
print(qc.draw('latex_source')) # for latex code

# Convert Quantum ckt to and from QASM
print(qc.qasm()) # returns the qasm string
# or
qc.qasm(formatted = True) 
# or
qc.qasm(formatted = True, filename='') 


# to create from qasm
new_qc = QuantumCircuit.from_qasm_file('my_circuit.qasm')

# Monitoring a job
job = execute(qc,backend)
# to see the status
job.status() #avoid this, can be rejected by server
# or
job.wait_for_final_state()


# using qiskit job monitor
from qiskit.tools import job_monitor
job_monitor(job)
# or use qiskit job watcher magic
import qiskit.tools.jupyter
%qiskit_job_watcher

# implement a multi-control toffoli gate
qc.ccx(0,1,2)
#or
qc.mct([0,1,2,3],4) # MCT- multicontrol toffoli, ([Control], target)

# use a specific version of qiskit
qiskit.__qiskit_version__

# Difference bt gate and instruction
# qiskit's gate object represents a unitary qGate, whereas instruction objs have non unitary parts
gate = qc.to_gate() # convert ckt to gate
new_qc.append(gate.power(5),[0,1])
#or
instructoin = qc.to_intruction()
instructoin.reverse_ops()
new_qc.append

# adding non unitary parts to qckt
qc = QuantumCircuit(2)
qc.initialize([0,1],0) # it is an initialize gate
qc.h(0)
qc.cx(0,1)
qc.measure_all()
qc.h(0).c_if(qc.clbits[0].register,1) # put h, conditioned on a classical register
qc.draw()

instructoin = qc.to_instruction()
# can't do gate = qc.to_gate(), because it is non-unitary
# but we can still append it to another ckt
new_qc.append(instructoin,[0,1],[0,1])
new_qx.decompose().draw() # to see original instruction

# control unitary part of ckt
# measuring or ressetting a ckt is non-unitary
controlled_qc = qc.control() # create control ckt, thus adding more qubits automatically to access it
# or, to add more control qubits
controlled_qc = qc.control(2)
new_qc = new_qc.compose(controlled_qc, range(4))
new_qc.decompose().draw()

# Combine 2 qckts
new_qc = qc1 + qc2
new_qc.draw()

# but to add ckts with different no. of qubits, we need to have qc1 and qc2 with same register name, or else we can do below
# use compose method
new_qc = qc1.compose(qc2, [1,2])

# Parametrised ckts in qiskit
a = Parameter("a")
qc.cul(a,0,1)
b = Parameter("b")
qc.assign_parameters({a:b}, inplace=True) # to replace a with b in the ckt
transpiled_qc = transpile(qc, basis_gates-['cx','u3'], optimization_level=3)
specific_transpiled_qc = transpiled_qc.assign_parameters({b:2})

# convert a unitary matrix to a set of one & two qubit gates, so that we can run code on older qcomputers
U = [[...],
[...],
[...],
[...]]
qc.unitary(U,[0,1])
trans_qc = transpile(qc, basis_gates=['cx','u3']) # cx and u3 are universal gates, so we can convert any ckt to these gates

# What is the unitary simulator


# What is a qsphere
plot_state_qsphere(sv.data)

#use mock backends in qiskit
from qiskit.test.mock import FakeVigo
fake_vigo = FakeVigo()
# this backend can be used without an account or que
results = execute(qc, fake_vigo).result()
counts = results.get_counts()
plot.........

# view device properties and config in qiskit
from qiskit.tools.jupyter import *
vigo = provider.get_backend('ibmq_vigo')
vigo # displays widget
vigo.configuration()
# or 
vigo.configuration().coupling_map
# etc
vigo.properties() # or ().t1(3)
vigo.defaults()

# simulate sv in qiskit
# sv describes the state of the computer by a vector with 2^n elements, n=no. of qubits
from qiskit.quantum_info import Statevector
sv = Statevector.from_label('000') #all 3 qubits initialized to 0
sv # gives output, statevector objects  
sv = sv.evolve(qc) # Evolve the object through the circuits, returns the simulated state of the qubits, after we run the ckt
sv.data # to get the state vector array

# Retrieve old job from IBM quantum
backend.jobs() # returns 10 most recent jobs
# or we can do
backend.jobs(limit=2) # for 2 jobs

job = backend.jobs(limit=2)[0] # to store the results of the first job 
counts = job.result().get_counts()

# another way to retrieve the job, using the job id
job = backend.retrieve_job('JOB_ID_HERE')
job.properties().qubits(0)

# invert unitary part of ckt
my_gate = qc.to_gate()
my_inverse_gate = my_gate.inverse()
my_inverse_gate.name = 'inverted gate'
qc2 = QuantumCircuit(3)
qc2.append(my_inverse_gate, [0,1,2])

inverse_qc = qc.inverse() # doesnt work when we have non-unitary part

# Not in the Series, Use dark mode for matplotlib plots
import matplotlib.pyplot as plt
plt.style.use('dark_background')
plot_histogram(job.result().get_counts())

# Save Circuit Drawings to Different File Types
qc.draw().savefig('fileName.png', dpi=700) # for saving matplotlib image as a png file, dpi is optional
qc.draw().savefig('fileName.svg') # for saving as svg
qc.draw('latex').save('fileName.png', dpi=700) # to save latex as image

# to save as text
with open('fileName.txt','w') as f:
    f.write(str(qc.draw('text')))

# Construct a Quantum Volume Circuit

# import the quantum volume constructor from the qiskit library
from qiskit.circuit.library import QuantumVolume
qv = QuantumVolume(4).decompose()

# learn more about the Quantum Volume here
# https://learn.qiskit.org/course/quantum-hardware/measuring-quantum-volume