# Quantum Computing
### Lab 01
- <i class="fa fa-file-text"></i> [Nvidia Quick Start](https://nvidia.github.io/cuda-quantum/latest/using/quick_start.html)
- <i class="fa fa-file-text"></i> [Github Completed Labs](https://github.com/airesx2/cudaq/tree/main/quick-start-to-quantum)
>**Quantum computing** is a multidisciplinary field comprising aspects of computer science, physics, and mathematics that utilizes quantum mechanics to solve complex problems faster than on classical computers.
-AWS
>**CUDA-Q** is a platform designed for hybrid application development. That is, CUDA-Q allows programming in a heterogeneous environment that leverages not only quantum processors and quantum emulators, but also CPUs and GPUs. CUDA-Q is interoperable with CUDA and the CUDA software ecosystem for GPU-accelerated applications. CUDA-Q consists of both C++ and Python extensions.
-Nvidia Quick Start
- Quantum computers use qubits, which can exist in a superposition of the states 0 and 1
- In classical computing, bits store information in either state 0 or 1
- State 0 is represented by a vector pointing up `b=0`
- State 1 is represented by a vecvtor pointing down `b=1`

- In Quantum computing , quibits can be in one of infinite shapes... so, they can be presented as a vector on a sphere

- State 0 is denoted by `|0⟩` "ket zero" or "zero ket"
- State 1 is denoted `|1⟩` "ket one" or "one ket"
- Then, there are an infinite number of states ∞
:::info
**State Formula**: `∣ψ⟩=α∣0⟩+β∣1⟩` (when the qubit in nonzero; superposition) AND `|a|² + |b|² = 1
` is true
Alpha and beta are complex values...
- `α` is the amplitude of `|0⟩`
- `β` is the amplitude of `|1⟩`
|+⟩ and |−⟩ are both superposition states that 50% chance of being measured |0⟩ or |1⟩, but they behave differently
```python=
def plus_measure_x():
## EDIT CODE BELOW THIS LINE
#|+⟩ state can be created when h gate applied to |0⟩
one_qubit = cudaq.qubit()
h(one_qubit)
mx(one_qubit)
## EDIT CODE ABOVE THIS LINE
@cudaq.kernel
def minus_measure_x():
## EDIT CODE BELOW THIS LINE
#|-⟩ state can be created when |0⟩ flipped to |1
#with x gate, and then applied h gate
one_qubit = cudaq.qubit()
x(one_qubit)
h(one_qubit)
mx(one_qubit)
## EDIT CODE ABOVE THIS LINE
```
:::
**Exercise 1**
```python=
# First we define vectors for the coefficients alpha and beta
# of the states |0>, |1>, |+>, and |i>, where each state is written
# in the form |psi> = alpha|0> + beta|1> . Here alpha and beta are
# of the form:
# alpha = a_real + (a_imaginary)i
# beta = b_real + (b_imaginary)i
# with real values for the coefficients
# a_real, a_imaginary, b_real, b_imaginary
import math
sqrt2_inv = 1 / math.sqrt(2)
c0 = [complex(1, 0), complex(0, 0)] # State |0⟩ = 1|0⟩ + 0|1⟩
c1 = [complex(0, 0), complex(1, 0)] # State |1⟩ = 0|0⟩ + 1|1⟩
c2 = [complex(sqrt2_inv, 0), complex(sqrt2_inv,0)] # |+⟩ = 1/√2·|0⟩ + 1/√2·|1⟩
c3 = [complex(sqrt2_inv, 0), complex(0, sqrt2_inv)] # |i⟩ = 1/√2·|0⟩ + i/√2·|1⟩
```

:::info
**The general structure of a quantum program...**
- Encode info into the quantum state by initializing qubit(s)
- Manipulate the quantum state of the qubit(s) with quantum gate(s)
- Extract info from the quantum state by measuring the state of the qubit(s)
:::

:::info
**Gates**
...Quantum gates are linear !!!
- Applying a U -gate refers to matrix multiplication by a(ny) unitary matrix U
**X GATE**

- Rotates a state around the X axis of the Bloch sphere by 180 degrees
- X gate is implemented with the syntax `x` (can be used for initializing the one state!)
**H GATE**

- Rotates the Bloch sphere 180 degrees about a different axis causing |0⟩ to move to |+⟩ and |1⟩ to rotate to |−⟩ , and vice versa
**Y GATE**
- Rotates the Bloch sphere 180 degrees about the Y axis
**Z GATE**
- Rotates the Bloch sphere 180 degrees about the X axis
**T GATE**
- Rotates a statevector 45 degrees about the Z axis
**U3 GATE**
- Universal rotation gate that allows for defining arbitrary rotation gates

:::
:::info
**Born's Rule:** if a qubit is in the quantum state `|ψ⟩=α|0⟩+β|1⟩` with α and β complex values satisfying `|α|^2+|β|^2=1`, then when measured, the outcome will be 0 with probability `|α|^2` and 1 with probability `|β|^2`
- The more shots, the closer the probability is to the Born's Rule prediction
:::
**X gate flips the qubit**
```python=
@cudaq.kernel
def bitflip():
# Allocate 1 qubit initialized in the zero state
qubit = cudaq.qvector(1)
# Manipulate the quantum state by applying the bit flip gate (the `x` gate)
x(qubit[0]) # Apply the x-gate to the first qubit, which is indexed with 0
```
**Exercise 2**
```python=
#Visualizing the action of the minus_kernel
sphere = cudaq.add_to_bloch_sphere(cudaq.get_state(minus_kernel))
cudaq.show(sphere)
```
**Exercise 3**
```python=
# Sampling the minus kernel
shots = 10000 #the larger the number the closer to 50/50!
results = cudaq.sample(minus_kernel, shots_count=shots)
print("Results from sampling {} times: {}".format(shots, results))
# Often it will be useful to identify the most probable outcome
# and the probability of this outcome
most_probable_result = results.most_probable()
probability = results.probability(most_probable_result)
print("Most probable result: " + most_probable_result)
print("Measured with probability " + str(probability), end='\n\n')
```
|+⟩ and |−⟩ has indistinguishable quasi-probability distributions upon measurement with `mz`
- equal amplitudes
- different relative phases (pi)
- |+⟩ outputs 0; |−⟩ outputs 1
### Lab 02
:::info
**Notation for a 2-qubit state**
Computational basis states...
`|00⟩,|01⟩,|10⟩, and |11⟩`
- `|10⟩` represents the state of a system of 2 qubits... the left 1 is qubit q0 's state... the right 0 is qubit q1 's state
General form of a quantum state of 2 qubits...

The term **statevector** refers to
- A quantum state `|ψ⟩`
- List of probability amplitudes of a quantum state `|ψ⟩`

:::
**Exercise 1**
```python=
# EXERCISE 1
num_qubits = 2
amplitudes = cudaq.get_state(nested_quantum_program, num_qubits)
print('|Psi>= {} |10> + {} |11>'.format(np.round(amplitudes[0],precision), np.round(amplitudes[1],precision)))
```
**Exercise 2**
```python=
num_qubits = 3
shots = 1000
@cudaq.kernel
def alternating_signs(qubit_count: int):
quibits = cudaq.qvector(qubit_count)
curr = 0
while curr < qubit_count:
if curr % 2 == 0:
h(quibits[curr])
curr+=1
else:
x(quibits[curr])
h(quibits[curr])
curr+=1
# Draw the circuit
print(cudaq.draw(alternating_signs,num_qubits))
# Verify state
# Compute the state of the system prior to measurement
state = cudaq.get_state(alternating_signs, num_qubits)
# Print
precision = 4
print('Statevector array of coefficients:', np.round(np.array(state), precision))
```
:::info
**Phase**
- Helps explain the phenomena of interference
- Every phase has a phase factor...
that can be deduced from the term eiφ in the standard representation of a quantum state:

- The state |−⟩ and −|−⟩ only differ by a scalar multiple of −1 ... When states only differ by a factor of −1 , they are said to differ by a **global phase**
- Unlike |+⟩ and |−⟩
- Interchangable / Equivalent
- Cannot distinguished from one another through any measurements
:::
>[!Important]Reflection
Something that really stuck with me was what my boss said to me yesterday. "An engineer wastes half their day trying to get the environment working before they can even start coding. This whole thing is a big waste of time" He told me this when I had complained to him about spending an entire day trying to install cudaq with WSL on my Windows. I thought it was really funny and it lightened up my mood, but it also gave me clarity on my hard work despite having extremely fraustrating parts. It gave me the motivation to keep going, especially when my boss suddenly dropped an advanced topic (quantum computing) onto my plate. I was reluctant to part ways with ML at first but in the end, I learned to adapt with my work and pick up hard material quickly. Qubits and all of its complicated states formulas proved to be a challenge; however, I quickly found enjoyment in learning about them as I dived deeper into to interactive labs and quantum material. The past two days were a learning curve that I eventually came to appreciate.
>[time=Fri, Jun 20, 2025 ]
:::info
**CNOT Gate**
`CNOT` gate is a controlled-not gate, always operating on 2 or more qubits (one of them being the "target"; the rest of them being "control")
Circuit Diagram...
`CNOT` gate: ━ connecting target & control qubits
target qubits: ⨁
control qubits: ●
1 control qubit: `x.ctrl(control_qubit, target_qubit)
`
2+ control qubit: `x.ctrl(control_qubits_list, target_qubit)`
CNOT Matrix:

If all the control qubits are in state |1⟩, the `CNOT` gate will apply a bitflip to the target qubit
If any of the control cubits are in state |0⟩, the `CNOT` gate will not take affect on the target qubit
Ex.
`CNOT(q0, q1)` & `CNOT([q1,q2],q0)`

Ex.
```python=
# CNOT applied to the states |00>, |01>, |10>, |11>
#If the first is on, the second gets CNOT gated
print('In this example CNOT is applied with the control qubit q0 and the target q1.')
@cudaq.kernel
def apply_cnot(control: cudaq.qubit, target: cudaq.qubit):
"""Apply an CNOT gate to the control and target qubits"""
x.ctrl(control, target)
@cudaq.kernel
def initialize(qubits: cudaq.qview, state: list[int]):
"""Kernel to initialize the state indicated by the given list
0: |0>, 1: |1>"""
for idx in range(len(state)):
if state[idx] == 1:
x(qubits[idx])
@cudaq.kernel
def CNOT_example(state: list[int]):
"""Apply CNOT to the state given by the state numbers"""
qubits = cudaq.qvector(len(state))
# Initialize state
initialize(qubits, state)
# Apply CNOT to the first two bits
if len(state) > 1:
apply_cnot(qubits[0], qubits[1])
# Test computational basis states
for i in range(4):
bits = format(i,'b')
bit_list = [int(bit) for bit in bits.zfill(2)]
result = cudaq.sample(CNOT_example, bit_list).most_probable()
print(f'CNOT applied to the state |{bits.zfill(2)}> results in |{result}>')
```
:::
**Exercise 3 Part a**
```python=
@cudaq.kernel
def alternating_cnots(qubit0: cudaq.qubit, qubit1: cudaq.qubit):
"""Apply a sequence of 3 CNOTs with alternating controls and targets on the 2 qubits given as arguments"""
# We will see later that it doesn't matter which qubit you start out with as the first control, as long as
# you alternate the control and target qubits with each of the 3 applications of the cnot gate
# Edit code below this line
x.ctrl(qubit0, qubit1) #1st CNOT
x.ctrl(qubit1, qubit0) #2nd CNOT
x.ctrl(qubit0, qubit1) #3rd CNOT
# Edit code above this line
@cudaq.kernel
def three_alternating_cnots():
"""Kernel for the circuit drawn above"""
# Allocate qubits
q = cudaq.qvector(2)
# Initialize qubits q0 and q1 in the plus and one states, respectively
h(q[0])
x(q[1])
# Apply alternating CNOTs
alternating_cnots(q[0], q[1])
results = cudaq.sample(three_alternating_cnots)
print('The distribution of states after sampling is: {}'.format(results))
```
:::info
**SWAP gate**
Instead of manually alternating states, the `swap` gate can be used as the dedicated notation...
`swap(q_0, q_1)`

:::
**Exercise 3 Part b**
```python=
@cudaq.kernel
def apply_cnot(control: cudaq.qubit, target: cudaq.qubit):
"""Apply an CNOT gate to the control and target qubits"""
x.ctrl(control, target)
@cudaq.kernel
def initialize_plus_zero(qubits: cudaq.qview):
"""Kernel to initialize the state indicated by the given list of bits"""
# Place qubits[0] in the plus state
h(qubits[0])
@cudaq.kernel
def CNOT_exercise():
"""Apply CNOT to |+0> with control q0 and target q1"""
qubits =cudaq.qvector(2)
# Initialize state
initialize_plus_zero(qubits)
# Apply CNOT to the first two bits
apply_cnot(qubits[0], qubits[1])
results = cudaq.sample(CNOT_exercise)
print('CNOT applied to the state |+0> results in the distribution of states: {}'.format(results))
```
:::info
**Entanglement**: Qubits are entangled if one of them depends on the other
:::
**Exercise 4**
```python=
# EXERCISE 4
num_qubits = 3
@cudaq.kernel
def initial_state(qubits : cudaq.qview):
for index in range(len(qubits)):
if index % 2 !=0:
x(qubits[index])
h(qubits)
@cudaq.kernel
def interference(qubit_count: int):
qvector = cudaq.qvector(qubit_count)
initial_state(qvector) # Initialize the state
# Apply x.ctrl with control q_0 and target q_1. Then apply a Hadamard gate to q_1.
# Edit the code below this line
x.ctrl(qvector[0], qvector[1])
h(qvector[1])
# Edit the code above this line
results = cudaq.sample(interference, num_qubits, shots_count = 1000)
print(results)
```
:::info
**Interference**: When we apply gates to change quantum states, we are also changing the probability amplitudes associated with each of the basis states
:::
**Exercise 6**
```python=
#Had a lot of trouble with implementing modular_mult_5_21
#b/c it is hard to guess what combination of gates
#will successfully simulate the problem
#I tried the algorithm in the linked article and
#looked at the graphs provided in the lab
#None of them produce the right answer for all the inputs
# Need to revisit the math behind modular multiplication
# in circuits or consider debugging
# individual gate behavior next
#In this version, only 20 produces the right answer
#(generated by claud AI)
import random
@cudaq.kernel
def modular_mult_5_21(qubits : cudaq.qvector):
""""Kernel based off of the circuit diagram in
https://physlab.org/wp-content/uploads/2023/05/Shor_s_Algorithm_23100113_Fin.pdf
Modifications were made to change the ordering of the qubits.
"""
# Edit code below this line
x.ctrl(qubits[4], qubits[0])
x.ctrl(qubits[2], qubits[0])
x(qubits[4])
x(qubits[2])
x.ctrl(qubits[0], qubits[4])
x.ctrl(qubits[0], qubits[2])
# Edit code above this line
@cudaq.kernel
def encode_integer(qubits: cudaq.qvector, binary_rep: list[int]):
"""Kernel takes as input a list of qubits and the binary representation
of an integer as a list. The kernel adds X-gates to the qubits to encode
the binary list, placing an X gate on qubit i if there is a 1 in the ith location
on the binary_rep list"""
# Edit code below this line
for i in range(len(binary_rep)):
if binary_rep[i] == 1:
x(qubits[i])
# Edit code above this line
def decimal_to_binary_list(number):
# Check if the input number is valid (non-negative integer)
if number < 0:
raise ValueError("Number must be a non-negative integer.")
# Convert the number to binary using bin() function and strip the '0b' prefix
binary_string = bin(number)[2:]
# Convert the binary string to a list of integers (0s and 1s)
binary_list = [int(bit) for bit in binary_string]
return binary_list
@cudaq.kernel
def mult_y_by_5_mod21(binary_list: list[int]):
# Allocate qubits
qubits = cudaq.qvector(5)
# Initialize qubits in the state representing 1, 4, 5, 16, 17, or 20
encode_integer(qubits, binary_list)
# Apply the multiplication by 5 mod 21 kernel
modular_mult_5_21(qubits)
values = [1,4,5,16, 17, 20]
number = random.choice(values) # selects a number from the list values randomly
# Convert number into a binary representation stored as a list of 0s and 1s
binary_list = decimal_to_binary_list(number)
results = cudaq.sample(mult_y_by_5_mod21, binary_list, shots_count = 200)
print("Multiplying {} by 5 mod 21 results in the bitstring {}".format(number,results.most_probable()))
```
*
**Exercise 7**
```python=
#Based on flawed solution of exercise 6
@cudaq.kernel
def modular_exp_kernel(exponent: int):
"""Kernel computes 5^x mod 21
Parameters:
-----------
exponent : int
the value x for the computation 5^x mod 21
Returns:
--------
binary_rep : string
binary representation for 5^x mod 21 as a string of 0s and 1s
"""
# Allocate and intialize qubits
qubits = cudaq.qvector(5)
# Edit code below this line
# Encode y = 1
encode_integer(qubits, [0, 0, 0, 0, 1])
# Multiply y = 1 by 5 exp times
for _ in range(exponent):
modular_mult_5_21(qubits)
# Edit code above this line
def modular_exp(exponent: int):
sample_result = cudaq.sample(modular_exp_kernel, exponent, shots_count = 100).most_probable()
return sample_result
for x in range(0,7):
result = modular_exp(x)
print("5^{} mod 21 in binary representation is {}".format(x, result))
```
>[!Important]Reflection
Today was one of the most tiring and unproductive days of my internship so far. Lab 2 from NVIDIA's quick start tutorial was a struggle for me and I made very little progress. I've squeezed out every resource I have into making my code work and helping me understand the code but nothing seems to be working. Hopefully, tomorrow I can debug stubborn exercise 6 and finish up lab 2 as a brand new day begins... Although there were many fraustrating hours, I had some enlightening moments as well. My boss and I had a conversation about quantum computers and how they are unpopular right now yet extremely powerful. Our discussion led me to doing a side little research on quantum computers on sale. It was interesting to see the million dollar prices and the scarce availability of them. I'm really glad to have had the opportunity to learn about such a fascinating field and I hope to see it grow in the future-- maybe one day i'll own my very own Advantage2 System :)
>[time=Mon, Jun 23, 2025 ]
### Lab 03
:::info
**Variational quantum algorithm**
1. Initialize params
2. Encode data into the quantum state by initializing qubit(s)
3. Extract data from the quantum state by measuring the state of the qubit(s) & computing the expectation value of the cost function
4. Run a classical optimizer to either determine a new state of parameters & repeat from step 2... or converge to an optimal solution

:::
**Exercise 1**
```python=
@cudaq.kernel
def initial_state(qubits : cudaq.qview):
""" Apply gates to the qubits to prepare the GHZ state
Parameters
qubits: cudaq.qvector
qubits for the walker
"""
# Edit the code below this line
h(qubits[0])
x.ctrl(qubits[0], qubits[1])
x.ctrl(qubits[0], qubits[2])
x.ctrl(qubits[0], qubits[3])
# Edit the code above this line
```
**Exercise 2**
```python=
# Define a kernel on 4 qubits for the DEC operation that
# maps |x> to |x-1> mod 16 and verify that it works as expected for |0001>
@cudaq.kernel
def DEC(qubits : cudaq.qview):
# Edit the code below this line
x(qubits[3])
x.ctrl(qubits[3], qubits[2])
x.ctrl([qubits[3], qubits[2]], qubits[1])
x.ctrl([qubits[3], qubits[2], qubits[1]], qubits[0])
# Edit the code above this line
# Create a kernel that applies the DEC to the 4 qubit state |0000>
@cudaq.kernel
def check_decrementer_kernel():
qubits = cudaq.qvector(4)
# Initialize the qubits to |0001>
x(qubits[3])
DEC(qubits)
result = cudaq.sample(check_decrementer_kernel).most_probable()
print('Decrementer kernel |0001> -> |{}>'.format(result))
```
:::info
**Classical Random Walks VS. Discrete Time Quantum Walk (DTQW)**
CLASSICIAL RANDOM WALKS
- Probabilities dictate movement (summing probabilities)
- Your position is the sum of many independent random variables
- This is a Random walk
- Useful in any domain involving randomness, uncertainty, or diffusion
- Usually normal (or Gaussian) distribution
- More paths to the middle(close to 0), less paths to outer positions -> higher probability toward the middle, lesser probability toward the sides
- [Small Demo!](https://www.youtube.com/shorts/MnBBV73KbDo)
>The central limit theorem says that the sampling distribution of the mean will always be normally distributed, as long as the sample size is large enough
-Scribbr
DISCRETE TIME QUANTUM WALKS (DTQW)
- Quantum analogue of a classical random walk
relies on amplitudes
- Never in one position, instead in superposition of many positions
- Summing complex amplitudes (complex numbers)
- Each path has a direction + phase
- If the amplitudes line up → they add (constructive interference); If they clash → they cancel (destructive interference)
- Walk evolves *unitarily*
- Uses interference between paths -> faster spread over the position space
- Useful for quantum search algorithms + generating probability distributions
- Bimodal or ballistic distribution
- As the steps build up, Center positions get many paths interfering against each other -> total cancels out; Edge positions have paths interfering in sync -> total adds up
- Resulting in dip in the middle and peaks on the both sides
:::
Discrete Time Quantum Walk
- quantum analogue of a classical random walk
relies on amplitudes
- Never in one position, instead in superposition of many positions
summing complex amplitudes
walk evolves unitarily
uses interference between paths -> faster spread over the position space
useful for quantum search algorithms + generating probability distributions
bimodal or ballistic distribution
**Exercise 3**
```python=
# EXERCISE 3
# Fill in the code to carry out the S- step
# Pick your favorite values
theta = np.pi/2 #CHANGE ME
phi = np.pi*4 #CHANGE ME
lam = 3.9 #CHANGE ME
# Set the number of qubits
num_qubits = 4
@cudaq.kernel
def DTQW_one_step(num_qubits: int, theta : float, phi : float, lam : float):
walker_qubits = cudaq.qvector(num_qubits)
coin_qubit = cudaq.qvector(1)
# Initial walker state |0101>
x(walker_qubits[1])
x(walker_qubits[3])
# Initial coin state
h(coin_qubit[0]) #Uncomment this line to start with a superposition of heads and tails instead of |0>
# One quantum walk step
# Coin operation F=u3
u3(theta, phi, lam, coin_qubit)
# Walker's position change
# Shift right (S+) when the coin is |1>
cudaq.control(INC, coin_qubit[0], walker_qubits)
# Shift left (S-) when the coin is |0>
# EDIT CODE BELOW THIS LINE
x(coin_qubit[0]) #flip to 1 state
cudaq.control(DEC, coin_qubit[0], walker_qubits)
x(coin_qubit[0]) #flip back
# EDIT CODE ABOVE THIS LINE
mz(walker_qubits)
# Visualize the kernel for the quantum walk
print(cudaq.draw(DTQW_one_step, num_qubits, theta, phi, lam))
# Sample the kernel for the quantum walk
result = cudaq.sample(DTQW_one_step, num_qubits, theta, phi, lam, shots_count=1000)
print(result)
```
**Exercise 4**
```python=
# Set a variable for the number of time steps
num_time_steps = 8000 # CHANGE ME to see the effect of different length walks
# Pick your favorite values
theta = np.pi/4 #CHANGE ME
phi = np.pi/4 #CHANGE ME
lam = 0.0 #CHANGE ME
# Set the number of qubits
num_qubits = 4
# EDIT CODE BELOW THIS LINE
@cudaq.kernel()
def DTQW_multi_step(num_qubits: int, theta : float, phi : float, lam : float, num_time_steps : int):
walker_qubits = cudaq.qvector(num_qubits)
coin_qubit = cudaq.qvector(1)
# Initial walker state |0101>
x(walker_qubits[1])
x(walker_qubits[3])
# Initial coin state
#h(coin_qubit[0]) #Uncomment this line to start with a superposition of heads and tails instead of |0>
# Flip the coin num_time_steps and shift the walker accordingly
# A simple for loop works !
# Initially, I thought there would be a problem with unrolling but Cudaq seems to handle that
for i in range(num_time_steps):
u3(theta, phi, lam, coin_qubit)
# Shift right (S+) when the coin is |1>
cudaq.control(INC, coin_qubit[0], walker_qubits)
# Shift left (S-) when the coin is |0>
x(coin_qubit[0])
cudaq.control(DEC, coin_qubit[0], walker_qubits)
x(coin_qubit[0])
# Measure the state of the walker
mz(walker_qubits)
# EDIT CODE ABOVE THIS LINE
# Sample the kernel for the quantum walk
result_multi_step = cudaq.sample(DTQW_multi_step, num_qubits, theta, phi, lam, num_time_steps, shots_count=1000)
print(result_multi_step)
# Draw the histogram of the results after one step
plot_results(result_multi_step, num_qubits)
```
### Lab 04
:::info
**Definition of probability amplitudes**: 
:::
:::info
**Hamiltonion H**:

- 16×16 matrix that operates on a 4-qubit quantum state through matrix multiplication
- An operation that can transform |j⟩ to j
- Used for computing the avg value of a quantum state if identified the computational basis states with integers
- H properties:

- The expectation value of this Hamiltonian with respect to a state ϕ:

:::
**Expectation value of a DTQW**
```python=
# Set a variable for the number of time steps
num_time_steps = 6
# Pick your favorite values
# Proudly picked the perfect three numbers
theta = np.pi/3.477 #CHANGE ME
phi = np.pi/3.9 #CHANGE ME
lam = 500 #CHANGE ME
# Set the number of position qubits
num_qubits = 4
@cudaq.kernel()
def DTQW_for_expectation_value_computation(num_qubits: int, parameters : list[float], num_time_steps : int):
walker_qubits = cudaq.qvector(num_qubits)
coin_qubit = cudaq.qvector(1)
theta = parameters[0]
phi = parameters[1]
lam = parameters[2]
# Initial walker state |9> = |1001> for possibly faster convergence
#x(walker_qubits[3])
#x(walker_qubits[2])
# initial coin state
h(coin_qubit[0])
#for i in range(1, num_qubits):
# x.ctrl(walker_qubits[0], walker_qubits[i])
# Flip the coin num_time_steps and shift the walker accordingly
for _ in range(num_time_steps):
# One quantum walk step
# Coin operation F=u3
u3(theta, phi, lam, coin_qubit)
# Walker's position change
# Shift right (S+) when the coin is |1>
cudaq.control(INC, coin_qubit[0], walker_qubits)
# Shift left (S-) when the coin is |0>
x(coin_qubit[0])
cudaq.control(DEC, coin_qubit[0], walker_qubits)
x(coin_qubit[0])
# Sample the kernel for the quantum walk
result_aiming_for_mean_of_3 = cudaq.sample(DTQW_for_expectation_value_computation, num_qubits, [theta, phi, lam], num_time_steps, shots_count=10000)
print('sampling results with the coin qubit:', result_aiming_for_mean_of_3)
# Define a function to draw the histogram of the results ignoring the coin qubit
def plot_results_without_coin_qubit(result, num_qubits):
# Define a dictionary of results
# Function to convert binary string to integer
def binary_to_int(binary_string):
return int(binary_string, 2)
# Initialize the dictionary with all possible bit strings of length 4 for the x axis
result_dictionary = {}
# Generate all possible bit strings of length num_qubits
for i in range(2**num_qubits):
bitstr = bin(i)[2:].zfill(num_qubits)
result_dictionary[bitstr] = 0
# Update the results dictionary of results from the circuit sampling
for k,v in result.items():
k_without_coin = k[:-1]
result_dictionary[k_without_coin] = v
# Convert the dictionary to lists for x and y values
x = list(result_dictionary.keys())
y = list(result_dictionary.values())
# Create the histogram
plt.xlabel("Positions")
plt.ylabel("Frequency")
# Create the histogram
plt.bar(x, y, color='#76B900')
# Rotate x-axis labels for readability
plt.xticks(rotation=45)
# Show the plot
plt.tight_layout()
plt.show()
# Draw the histogram of the results after one step
plot_results_without_coin_qubit(result_aiming_for_mean_of_3, num_qubits)
# Compute the expectation value
exp_value = cudaq.observe(DTQW_for_expectation_value_computation, position_hamiltonian, num_qubits, [theta, phi, lam], num_time_steps).expectation()
print('The expectation value <ψ|H|ψ> using parameters ({:.6f}, {:.6f}, and {:.6f}), is {} '.format(theta, phi, lam, exp_value))
```
>[!Important]Reflection
Finally, after 4 work days, I've stumbled over installing cudaq and finished all 4 labs by Nvidia Quickstart. Yesterday was a pain but luckily, today was much better and I basically breezed over lab 4. Well, except when I had to fight with the comparisons between Classical Normal Walk and DTQW. When I was reporting my progress to my boss, he asked me why the Classical Normal Walk always produced a Gaussian distribution graph. I thought it was just by coincidence, but it turned out to be based on the central limit theorem. I fell into a loophole of the logistics behind both walks and went through a frantic research moment. It was stressful to have my boss nagging at my knowledge almost all throughout my lunchtime although I guess this is what working in the real world is like. In the end, I did eventually understand very clearly why DTQW produced bimodel graph and got some traumatic preview of AP physics. Quantum computing is way more mentally exhausting than I thought, but I think it's starting to get fun.
>[time=Tues, Jun 24, 2025 ]
:::info
**Grover's Algorithm**
The Grover's Algorithm is a quantum search alogrithm for unstructured searches and provides quadratic improvement over classical algorithms by running fewer steps
> **1**. Begin with initializing n qubi register to state |0⟩
>
> **2**. Apply the H gate to each qubit to ensure the register is in uniform superposition state
> (this makes sure that there is equal probability of measuring and equal amplitude)
> 
>
> **3**. Phase oracle Of that applies -1 conditional phase shift for the target items
> (X gates applied qubits in the target bit string that have a 0, temporarily converting it into a "universal bitstring" (eg. 111). z gates recoginize this (111) bitsrting and flip its state [eg. - to +]. Same X gates applied again to turn the bitstring back to its original form, leaving the marked state phase flipped)
>
> **4**. Apply the Grover's diffusion operator
> ("Inversion about the mean": Same logic as oracle... first masking the binary string, then boosts the amplitude of the marked state and reduces the other states by reflecting the state around the average amplitude)
>-> Apply H to each register qubit
> -> Apply X to each register qubit
> -> Apply multi-controlled-Z to each register qubit
> -> Apply X to each register qubit
> -> Apply H to each register qubit

eg.
>Target State: 11; Avg Amplitude: 0.25
>| State | Old Amp | New Amp (2×0.25 − old) |
>| -------- | -------- | -------- |
>| 00 | +0.5 | 0 |
>| 01 | +0.5 | 0 |
>| 10 | +0.5 | 0 |
>| 11 | -0.5 | 1.0 |
>
>Overall unitary operation (oracle + diffuser):

*REPEAT steps 3 and 4 [π/4 √N] times to reach best success probability
>
> **5**. Measure the qubits with respect to standard basis measurements
>
> **6**. Check if the solution is valid and output the resulting string
>
Grover's Algorithm Example

*Generated by ChatGPT
Sources~
<i class="fa fa-file-text"></i> [IBM Quantum Learning](https://learning.quantum.ibm.com/course/fundamentals-of-quantum-algorithms/grovers-algorithm)
<i class="fa fa-file-text"></i> [Microsoft Learn](https://learn.microsoft.com/en-us/azure/quantum/concepts-grovers)
<i class="fa fa-file-text"></i> [Geeks for Geeks](https://www.geeksforgeeks.org/introduction-to-grovers-algorithm/)
:::
>[!Important]Reflection
Who would have known cudaq would be more picky than me at a seafood restaurant? I've been more than happy to have escaped the clutches of Nvidia's Quickstart Tutorial and I finally got to code a simple brute force program per inspired by my boss. During the coding process, I faced a lot of issues with cudaq not accepting certain commands Cudaq kept on spitting out huge chunks of error messages for every small thing and it was really infurating. At last, I conquered cudaq's fiestiness and successfully debugged my code, producing my first independent cudaq project! Prior to this, I did a lot of research on the Grover's algorithm which I used to code the brute force. I found quantum algorithms unfamiliar because they did differ alot from classical algorithms. Even so, it was interesting to learn a new way of coding-- working with fascinating qubits. Unfortunately, today i resorted to coding on Google Collab, but maybe another day I will face WSL enviornments head on and finally win the fight. Look forward tomorrow where I create brute force 2.0!!
>[time=Wed, Jun 25, 2025 ]
:::info
**Hashing**: A process of converting data (eg. passwords) into fixed length strings by using mathematical hashing functions. Calculated hash values are irreversible and help protect passwords from being decoded even in the case of a data breach.
- Salting: Adding unique random strings to the end of inputs before hashing it. By ensuring users with the same password get different hash values, the chances of hash collisions reduce.
- Peppering: Adding a random value unique to each system to the end of input before hashing it, preventing hackers from reusuing stolen hash passwords across different platforms
>🖥️Fun Fact!
>Computing a 16 bit password hash would require trying...
>2²⁵⁶ inputs with a classical brute force
>but only 2¹²⁸ with a quantum brute force (Grover's Algorithm)
<i class="fa fa-file-text"></i> [Simplilearn Hashing Video](https://www.youtube.com/watch?v=jmtzX-NPFDc)
:::
>[!Important]Reflection
I've found my new favorite hobby... password busting with cudaq. After being bombarded with loads of errors yesterday, I finally made friends with cudaq and created some pretty fun programs. I crawled back to my past of AP CSP (taken in freshmen year) and revisited good old hashing and salting with brute force. I had forgotten the logistics of it, so I watched a handy video about hashing which was really helpful and interesting. Something I learned over the course of these weeks is that building onto one big project really helps me understand the code and build onto my knowledge. Creating better drafts of my brute force code from yesterday allowed me to further grasp the complex layers of quantum algorithms. I've been so focused on my work that even during my bathroom breaks, the only things on my mind are the words "oracle" and "diffusion". Out of all the versions of brute force programs (from java to cudaq), cudaq is for sure my favorite. It made me realize the potential in quantum computing's future.
>[time=Thurs, Jun 26, 2025 ]
>[!Important]Reflection
I'm writing this Friday reflection on Monday because I ran out of time last week. I remember that it was a productive friday where I made a handful of mini projects that related to cybersecurity. It was also one of those days where the gears clicked in and I finally understood the complex code in front of me. After my boss bluntly called out my lack of explaining skills, I made the concious effort to pay more attention to detail and was able to report back to my boss with confidence. I was thrilled to merge my passion of cybersecurity and my new skills in those assignment as it really enhanced my knowledge in both fields. My favorite project was simulating a injection attack with malicious cudaq gates which challenged me technically and creatively. At the end of the day, I got my experience my first activity engaging with other coworker's in their work. A friendly coworker sitting behind me asked me to test his newly built AI model for flaws. It was a simple task, but it gave me a glimpse into the collaborative and supportive environment of this internship.
>[time=Fri, Jun 27, 2025 ]
>[!Important]Reflection
Now that it's the last week of my internship, I've got into the mentality of quality over quantity. I started this ML/cudaq intrusion detector project as a way for me to bridge the two things I learned this month. It's a little wonky since the ML wasn't image trained like I was used to, and cudaq is being as syntax picky as ever. I had finished a decent version of it by noon, but I ended up working on it for the entire day. It was exciting to see my accuracy number jump from 50 to 90 with a simple addition of new feature checks. Throughout the day, I focused on the accuracy of the program and trying to raise the numbers to 98-99%. Lots of fancy python tools were introduced to me and I got to know even more ML models. I think it's cool that I managed to pull this off to showcase all my skills in one. Also, today I got my certificate of internship signed by my boss himself! Everything feels x10 more official now:)
>[time=Mon, Jun 30, 2025 ]
>[!Important]Reflection
Now that it's the last week of my internship, I've got into the mentality of quality over quantity. I started this ML/cudaq intrusion detector project as a way for me to bridge the two things I learned this month. It's a little wonky since the ML wasn't image trained like I was used to, and cudaq is being as syntax picky as ever. I had finished a decent version of it by noon, but I ended up working on it for the entire day. It was exciting to see my accuracy number jump from 50 to 90 with a simple addition of new feature checks. Throughout the day, I focused on the accuracy of the program and trying to raise the numbers to 98-99%. Lots of fancy python tools were introduced to me and I got to know even more ML models. I think it's cool that I managed to pull this off to showcase all my skills in one. Also, today I got my certificate of internship signed by my boss himself! Everything feels x10 more official now:)
>[time=Mon, Jun 30, 2025 ]
>[!Important]Reflection
Over these weeks, I've really built up my confidence as an intern at this company, maybe even too much. Today I got called into my boss's office first thing in the morning and got extremely humbled. I've always known that speech wasn't my strong suit but I didn't think it would ever affect my work experience. No matter how hard I tried to explain cudaq to my boss, he wouldn't understand. Being able to teach a complete beginner your craft and making sure they can comprehend it is a very important skill. My boss said that no matter how impressive your skills are, your inability to explain your knowledge properly diminishes it. It's harsh but I get it, and I now know it's something I need to work on. Back on a more positive note, a new intern started shifts today. It really helped to have someone to share my work with and actually cooperate with in a work environment, aside from interacting with my boss. Discussing code with them helped me understand it even further and I got a lot of knowledge from them as well. It's a shame that I would only get to work with him for a week.
>[time=Tues, Jul 1, 2025 ]
:::info
**Breaking Encryption with Quantum**
When attempting to break encryption, a computer needs to factor very large numbers, evaluating through billions of number combinations. Doing so with a classical computer can take an extremely long time; however, it would take quantum computers way less time by leveraging properties like superposition and entanglement. The *Shor's Algorithm* is a quantum algorithm that can evaluate a prime factor of an integer through mathematical formulas, which allows for fast computation with a quantum computer. With the advancement of quantum computing, Shor's Algorithm poses a threat to our digital world of encryption.
<i class="fa fa-file-text"></i> [Minute Physics](https://www.youtube.com/watch?v=lvTqbM5Dq4Q)

^The alg leaves a superposition of numbers (in repeating pattern)in which the frequency(1/p) needs to be determind and then p can be found

^Period p represents the number of times the function repeats

^Plugging in an unlikely guess into this formula generates a guess that is more likely to be a factor of N

^Turning p into a factor
**Summary steps of Shor's Algorithm**
Goal: fine the 2 prime factors of N (N = primeP x primeQ)
1. Take a random guess for a number g (1 < g < N)
2. Create function: `f(a) = g^a mod N`
3. Find period frequency (1/p) with QFT to find p, period(the no. of times function repeats)
- if plugging into the function and is 1, that signifies a full loop (a = period)
5. Now you know: `g^p ≡ 1 mod N → so N divides (g^p - 1)`
6. Plug into `gcd(g^(p/2)± 1,N)` for mathematical guess
7. If p is odd, restart from step 1 or if even, continue
8. If factor (primeP) found, N/primeP gets you primeQ!
https://www.awesomescreenshot.com/image/55420135?key=1489092ca32fb134926edf2b5ed27bdb
^Credits Ve youtube video
:::
# SOURCES
* readme: https://hackmd.io/Jjy_NLeWTT-EGxlED_woYg?view#Lab-01
* example1: https://colab.research.google.com/drive/1gZbQtcL9CHULqzfHj0p9PeqVnldeEcqk#scrollTo=Yf87idodIgkk
* example2: https://colab.research.google.com/drive/1YmFshrkdAtBLU0x0jwraGAOgUqb9nXfz
* example3: https://colab.research.google.com/drive/1yOi2B59diB5XiFCqNUqad-EMOngfqny8#scrollTo=s7jN3OSkNbtk
* github: https://github.com/airesx2/Internship-2025/tree/main/cudaq_learning