diff --git a/Python/AWG.py b/Python/AWG.py
index b4af20e89807154408d91592dd2a15152a426273..d1012460533a338f023dca229c2b4cfd7ca3fae1 100644
--- a/Python/AWG.py
+++ b/Python/AWG.py
@@ -31,6 +31,7 @@ class AWG:
         self.mem_size = int64(0)  # maximum memory size of the AWG
         self.full_scale = int32(0)  # full scale scaling of output voltage, used for data calculation
         self.channel = [0,0,0,0]  # activated channels
+        self.ch_amp = [0,0,0,0]  # channel output amplitude
         self.mode = ""  # current mode AWG is running on
 
     def open(self, remote=False):
@@ -69,7 +70,7 @@ class AWG:
         """
         flag = 0
         msg = "Checking error at " + message + " ... "
-        if custom_msg != "":
+        if message != "":
             flag = 1
             sys.stdout.write(msg)
         err_reg = uint32(0)
@@ -114,7 +115,7 @@ class AWG:
         spcm_dwSetParam_i32(self.card, SPC_M2CMD, M2CMD_CARD_FORCETRIGGER)
         self.check_error()
 
-    def set_sampling_rate(self, sr):
+    def set_sampling_rate(self, sr: int):
         """
         set sampling rate
         :param sr: 64bit integer between 50MHz and 625MHz
@@ -137,6 +138,7 @@ class AWG:
 
         if self.channel[ch] == 0:
             self.channel[ch] = 1
+            self.ch_amp = amplitude
             spcm_dwSetParam_i64(self.card, SPC_CHENABLE, int64(2**ch))  # see CHANNEL0-3 in regs.py for detail
             spcm_dwSetParam_i32(self.card, SPC_ENABLEOUT0 + ch * (SPC_ENABLEOUT1 - SPC_ENABLEOUT0), 1)
             spcm_dwSetParam_i32(self.card, SPC_AMP0 + ch * (SPC_AMP1 - SPC_AMP0), amplitude)
@@ -190,13 +192,13 @@ class AWG:
 
     def get_aligned_array(self, size):
         """
-        returns an array at a page-aligned memory location, necessary for data buffering
+        returns a numpy array at a page-aligned memory location
         :param size: number of samples used for data calculation
         :return: numpy array starting at the correct location
         """
         data_length_bytes = uint32(size * 2 * np.sum(self.channel))
         buffer = pvAllocMemPageAligned(data_length_bytes)  # buffer now holds a page-aligned location
-        buffer_data = cast(addressof(pvBuffer), ptr16)  # cast it to int16 array
+        buffer_data = cast(addressof(buffer), ptr16)  # cast it to int16 array
         array = np.frombuffer(buffer_data, dtype=int16)
         return array
 
@@ -210,7 +212,7 @@ class AWG:
         spcm_dwSetParam_i32(self.card, SPC_SEQMODE_MAXSEGMENTS,nseg)  # set number of sequences the memory is divided into
         self.check_error()
 
-    def write_segment(self, data: np.array, segment: int):
+    def write_segment(self, data: np.ndarray, segment: int):
         """
         write data onto a specified segment.
         :param data: numpy array containing waveform data
diff --git a/Python/py_header/__pycache__/regs.cpython-310.pyc b/Python/py_header/__pycache__/regs.cpython-310.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..8664756a74b00597dc56933500d23d077426532f
Binary files /dev/null and b/Python/py_header/__pycache__/regs.cpython-310.pyc differ
diff --git a/Python/py_header/__pycache__/regs.cpython-38.pyc b/Python/py_header/__pycache__/regs.cpython-38.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..01fa7161fa1b336493a916bd535258f90c2fa7c8
Binary files /dev/null and b/Python/py_header/__pycache__/regs.cpython-38.pyc differ
diff --git a/Python/py_header/__pycache__/regs.cpython-39.pyc b/Python/py_header/__pycache__/regs.cpython-39.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..89a5c1540b0ea82d078bb6060faec9bfae5e853b
Binary files /dev/null and b/Python/py_header/__pycache__/regs.cpython-39.pyc differ
diff --git a/Python/py_header/__pycache__/spcerr.cpython-310.pyc b/Python/py_header/__pycache__/spcerr.cpython-310.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..013a87ab998fd70fbab80d2882e3c0b9b054d06f
Binary files /dev/null and b/Python/py_header/__pycache__/spcerr.cpython-310.pyc differ
diff --git a/Python/py_header/__pycache__/spcerr.cpython-38.pyc b/Python/py_header/__pycache__/spcerr.cpython-38.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..195d3a2db0050471ebd5e05fa46693580b513d72
Binary files /dev/null and b/Python/py_header/__pycache__/spcerr.cpython-38.pyc differ
diff --git a/Python/py_header/__pycache__/spcerr.cpython-39.pyc b/Python/py_header/__pycache__/spcerr.cpython-39.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..d39980bfeea4f8cb9dcc87249b520d5649faedeb
Binary files /dev/null and b/Python/py_header/__pycache__/spcerr.cpython-39.pyc differ
diff --git a/Python/waveform.py b/Python/waveform.py
new file mode 100644
index 0000000000000000000000000000000000000000..538c959690aba982989df7823863dc9e080558e8
--- /dev/null
+++ b/Python/waveform.py
@@ -0,0 +1,191 @@
+from AWG import *
+import time
+
+
+class Waveform:
+    def __init__(self, cf: int, df: int, n: int, sample_rate: int):
+        """
+        helper class to store basic waveform information.
+        :param cf: center frequency tone of tweezer array.
+        :param df: differential frequency between neighboring tweezers.
+        :param n: number of tweezers to the left/right of center frequency tone, total number of tweezer is 2n+1.
+        :param sample_rate: sampling rate of the AWG to generate correct number of samples.
+        """
+        # define some useful numbers
+        scale = 2**11  # Mingkun uses 2^11, caltech uses 2^15, maybe this is scaling up a float to int?
+        num_tz = 2*n + 1  # total number of tweezers to be generated
+        max_amp = scale / np.sqrt(num_tz)  # again, saw this from multiple sources, not sure why
+
+        # self.amplitude = max_amp * np.ones(num_tz)
+        self.amplitude: np.ndarray = max_amp  # uniform amplitude for now, this will eventually be tweaked finely with experiments
+        self.omega: np.ndarray = 2*np.pi * np.linspace(cf - n*df, cf + n*df, num_tz)  # frequency tones
+        self.phi: np.ndarray = 2*np.pi * np.random.rand(num_tz)  # random initial phases from 0-2pi, also will be tweaked finely with experiments
+        self.sample_rate: int = sample_rate
+        # self.debug = {
+        #     "mat1": 0,
+        #     "mat2": 0,
+        #     "mat3": 0
+        # }
+
+
+def create_static_array(wfm: Waveform, sample_len: int) -> np.ndarray:
+    """
+    create a static-array-generating waveform with user set number of samples
+    :param wfm: waveform object already initialized with basic parameters.
+    :param sample_len: total number of samples to generate, must be multiples of 512. Note that more sample != higher resolution.
+    :return: returns a 1D array with static-array-generating waveform.
+    """
+    # construct time axis, t_total(s) = sample_len / sample_rate, dt = t_total / sample_len
+    t = np.arange(sample_len) / wfm.sample_rate
+
+    # calculate individual sin waves, sig_mat[i] corresponds to data for ith tweezer
+    sin_mat = wfm.amplitude * np.sin(np.outer(wfm.omega,t) + np.expand_dims(wfm.phi, axis=1))  # shape=(number of tweezers x sample_len)
+
+    # sum up all rows to get final signal
+    return np.sum(sin_mat, axis=0)
+
+
+def create_path_table(wfm: Waveform) -> np.ndarray:
+    """
+    create a dim-3 look up table where the table[i,j] contains a sine wave to move tweezer i to tweezer j
+    :param wfm: waveform object already initialized with basic parameters.
+    :return: dim-3 ndarray
+    """
+    # setup basic variables
+    twopi = 2*np.pi
+    vmax = KILO(20) * MEGA(1)  # convert units, 20 kHz/us -> 20e3 * 1e6 Hz/s
+    dw_max = wfm.omega[-1] - wfm.omega[0]  # Longest move in frequency
+    t_max = 2 * dw_max / vmax  # Longest move sets the maximum moving time
+    a_max = -vmax * 2 / t_max  # maximum acceleration, negative sign because of magic
+    sample_len_max = int(np.ceil(t_max * 4/5 * wfm.sample_rate))  # get number of samples required for longest move, this sets the size of lookup table
+    sample_len_max += (512 - sample_len_max % 512)  # make overall length a multiple of 512 so AWG doesn't freak out
+
+    # now we calculate all possible trajectories, go to Group Notes/Projects/Rearrangement for detail
+    n = len(wfm.omega)  # total number of tweezers
+    phi_paths = np.zeros((n, n, sample_len_max))  # lookup table to store all moves
+    t = np.arange(sample_len_max) / wfm.sample_rate  # time series
+    # iterate! I think this part can be vectorized as well... but unnecessary.
+    for i, omega_i in enumerate(wfm.omega):
+        for j, omega_j in enumerate(wfm.omega):  # j is the target position, i is starting position
+            if i == j: continue  # skip diagonal entries, duh
+            dw = omega_j - omega_i  # delta omega in the equation
+            adw = abs(dw)
+
+            # I advise reading through the notes page first before going further
+            phi_j = wfm.phi[j] % twopi  # wrap around two pi
+            phi_i = wfm.phi[i] % twopi
+            dphi = phi_j - phi_i  # delta phi in the equation
+            if dphi < 0: dphi = abs(dphi) + twopi - phi_i  # warp around for negative phase shift
+            t_tot = np.sqrt(abs(4 * dw / a_max))  # calculate minimum time to complete move
+            t_tot = ((t_tot - 6*dphi/adw) // (12*np.pi/adw) + 1) * 12*np.pi/adw  # extend move time to arrive at the correct phase
+            a = 4*dw/(t_tot**2)  # adjust acceleration accordingly to ensure we still get to omega_j
+            end = int(np.ceil(t_tot * wfm.sample_rate))  # convert to an index in samples
+            half = int(end / 2)  # index of sample half-way through the move where equation changes
+            t1 = t[:half]  # first half of the move, slicing to make life easier
+            t2 = t[half:end] - t_tot/2  # time series for second half of the move
+
+            # do calculation
+            phi_paths[i,j, :half] = wfm.phi[i] + omega_i*t1 + a/6*t1**3  # t<=T/2
+            phi_paths[i,j, half:end] = phi_paths[i,j,half-1] + (omega_i+a/2*(t_tot/2)**2)*t2 + a/2*t_tot/2*t2**2 - a/6*t2**3  # t>=T/2
+            phi_paths[i,j, end:] = omega_j*t[end:] + (phi_j - omega_j*t_tot) % twopi  # fill the rest with parameters of target wave
+
+    # now compile everything into sine wave
+    phi_paths = wfm.amplitude * np.sin(phi_paths)
+    return phi_paths
+
+
+def create_moving_array(sin_mat: np.ndarray, path_table: np.ndarray, paths: np.ndarray) -> np.ndarray:
+    """
+    create a rearranging signal that moves tweezers as specified by paths
+    :param wfm: waveform object already initialized with basic parameters.
+    :param sin_mat: 2d array where ith entry contains static sin wave for ith tweezer. See create_static_array for detail.
+    :param path_table: lookup table returned from create_path_table().
+    :param paths: 1d array filled with tuples indicating moving trajectories. Example: np.array([(1,0),(2,1)]) moves tweezer1->0,tweezer2->1
+    :return: 1D array with rearrangement-generating waveform.
+    """
+    # for i,j in paths:
+    #     sin_mat[i] = phi_paths[i,j]
+    # fyi, line below is equivalent to the for loop above
+    sin_mat[paths[:,0]] = path_table[paths[:,0], paths[:,1]]  # copy dynamic trajectories from path_table,
+    sin_mat[np.setdiff1d(paths[:,1], paths[:,0])] = 0  # turn off tweezers that need to be turned off, moving 3-2, 2-1, 1-0 will turn off 0.
+    return np.sum(sin_mat, axis=0)  # sum up all rows to get final signal
+    # return np.sum(sin_mat, axis=0), sin_mat/wfm.amplitude
+
+
+def old_code():
+    """
+    collection of unused code that might be useful later...
+    """
+    # def create_phase_paths_old(wfm):
+    #     # setup basic variables
+    #     vmax = KILO(20) * MEGA(1)  # 20 kHz/us -> 20 kHz * 1e6 / s
+    #     dw_max = wfm.omega[-1] - wfm.omega[0]  # Longest move in frequency
+    #     t_max = 2 * dw_max / vmax  # Longest move sets the maximum moving time
+    #     a = -vmax * 2 / t_max  # constant acceleration, negative sign because of magic
+    #     sample_len = int(np.ceil(t_max * wfm.sample_rate))  # get number of samples required for longest move
+    #     sample_len += (512 - sample_len % 512)  # make overall length a multiple of 512
+    #     # t = np.zeros(padding+sample_len)
+    #     t = np.arange(sample_len) / wfm.sample_rate  # get time series
+    #
+    #     # generate phi for every possible move
+    #     n = len(wfm.omega)  # total number of tweezers
+    #     phi_paths = np.zeros((n, n, sample_len))  # map to store all moves
+    #     for i, omega_i in enumerate(wfm.omega):
+    #         for j, omega_j in enumerate(wfm.omega):  # I set j to be the target position, i to be starting position
+    #             if i == j: continue  # skil diagonal entries
+    #             dw = omega_j - omega_i  # delta omega
+    #             t_tot = np.sqrt(abs(4 * dw / a))  # total time to travel dw, t_tot <= t_max
+    #             end = int(np.ceil(t_tot * wfm.sample_rate))  # total number of samples to move dw
+    #             half = int(end / 2)  # index of sample half-way through the move
+    #             t1 = t[:half]  # time series for first half of the move
+    #             t2 = t[half:end] - t_tot/2  # time series for second half of the move
+    #             # do calculation
+    #             phi_paths[i,j, :half] = wfm.phi[i] + omega_i*t1 + a/6*t1**3  # t<=T/2 note we are changing the ith tweezer
+    #             phi_paths[i,j, half:end] = phi_paths[i,j,half-1] + (omega_i+a/2*(t_tot/2)**2)*t2 + a/2*t_tot/2*t2**2 - a/6*t2**3  # t>=T/2
+    #             # phi_paths[i,j, half:end] = phi_paths[i,j,half-1] + (omega_i+a*(t_tot/2)**2)*t2 + a/2*t_tot/2*t2**2 - a/6*t2**3  # t>=T/2
+    #             phi_paths[i,j, end:] = omega_j*t[end:]  # fill the rest of the array with target frequency wave
+    #             # phi_paths[i,j, end:] = phi_paths[i,j, end-1]*t[end:]  # fill the rest of the array with same value
+    #     return phi_paths
+
+    # sig_mat = wfm.amplitude * np.sin(np.outer(wfm.omega,t) + wfm.phi[:, np.newaxis])  # shape=(number of tweezers x sample_len)
+
+    # self.debug["mat1"] = np.zeros((n, n, 2))  # for phase debugging
+    # for diagnostic purposes
+    # path_idx[i, j] = (t_tot, end)
+    # phi_const[i,j, :half] = wfm.phi[i]
+    # phi_const[i,j, half:end] = phi_const[i,j,half-1] +
+    # print(a, t_tot)
+    # return phi_paths, wfm.amplitude * np.sin(np.outer(wfm.omega,t) + np.expand_dims(wfm.phi, axis=1)), path_idx
+
+    pass
+
+
+def main():
+    # sample usage
+    np.random.seed(0)
+    cf = MEGA(80)
+    df = MEGA(0.2)
+    n = 10
+    # period = 1/cf
+    sample = 512*1000
+    rate = MEGA(625)
+    # rate = sample*cf/1e5
+    print("Sampling rate: ", rate/1e6)
+    print("center f: ", cf, "df: ", df, "total n: ", 2*n+1)
+
+    wave = Waveform(cf, df, n, rate)
+    static_sig = create_static_array(wave, sample)
+    table = create_path_table(wave)
+    # mat_copy = mat.copy()
+    # mat /= wave.amplitude
+    t1 = time.perf_counter()
+    repath = np.array([(1,0), (2,1), (3,2), (4,3), (5,4), (6,5), (7,6), (8,7), (9,8), (10,9)])
+    move_sig = create_moving_array(create_static_array(wave, table.shape[2]), table, repath)
+    print("rearrange signal generation time: ", time.perf_counter() - t1)
+
+    np.savetxt("static_signal.txt", static_sig, delimiter=',')
+    np.savetxt("move_signal.txt", move_sig, delimiter=',')
+    # np.savetxt("initial_phi.txt", wave.phi, delimiter=',')
+    # np.savetxt("sig_mat.txt", sig_mat, delimiter=',')
+
+