Synchronization problem with Multi-instrument Python API

Hello, I am trying to create two pulses from the two channels of the AWG and then observe it with the oscilloscope. Although in my code I call the functions “awg.sync_phase()” and “m.sync()”, It creates the pulses in the correct places, but then I see that they shift. So the synchronization gets disrupted. I took a video from another oscilloscope for the purpose of watching how the pulses evolve. You can see the red one is Channel A, which is a pulse around 500 microseconds, and the yellow one is Channel 2, which is a pulse around 1 microsecond.
The website does not allow me to attach the video I took. Therefore I am attaching two frames from the video, so that you can tell they shift.

Dear Ece,

I am sorry for the issue you are dealing with.

Would you please share the AWG configuration on your end? We will reproduce on our end and get back to you.

Thank you very much for your understanding.

Best regards,
Hank

Hello of course,
Here is the code. Here I use a loop to sweep through the time between the two pulses. You can see the problem generated after the last iteration, where the pulses need to stay synchronized but they shift. I am sorry for the formatting the website does not allow me to upload files.

import matplotlib.pyplot as plt
import numpy as np
from moku.instruments import MultiInstrument
from moku.instruments import Oscilloscope, ArbitraryWaveformGenerator
import time
import math
from scipy.optimize import curve_fit

m = MultiInstrument(‘10.16.13.195’, platform_id=4, force_connect=True)

m = MultiInstrument(‘169.254.52.149’, platform_id=4, force_connect=True)

#All time variables are in nanoseconds
#The sequence is as follows: MW pulse → twait → Laser pulse

duration_laser = 500000
twait= 1100 #Time between the laser and MW pulse
t_padding = 5000 #this is a time variable I use to make sure tl is a multiple of tm.
t_integ = 0.1*duration_laser #the integration time

#The duration of the MW pulse is swept.
tMW_start = 20
tMW_end = 500
xdata = np.linspace(tMW_start,tMW_end, 51) #The data on the x-axis, the list for the MW pulse duration.

#We fix a length for the MW cycle. This might need to be changed if you want to send pulses that are longer than 2 microseconds.
#Step size is 10 ns. If you have a step size that is too big this might be a problem to create the short MW pulses.
tcyc_mw = 2000
step = 10
tm = np.arange(0, tcyc_mw+step, step) #The time vector for the MW pulse. One MW pulse will be created using this vector.

photolum = np.zeros(len(xdata)) #The photoluminescence data defined as an empty array at first. It should be the same size as xdata so it is measured for each MW pulse duration.
a_vec = np.zeros(len(photolum)) #This is the vector to integrate the first part of the pulse.
b_vec = np.zeros(len(photolum)) #This is the vector to integrate the final part of the pulse.

try:
awg = m.set_instrument(1, ArbitraryWaveformGenerator)
osc = m.set_instrument(2, Oscilloscope)

connections = [dict(source="Input1", destination="Slot1InA"),
               dict(source="Slot1OutA", destination="Slot2InA"),
               dict(source="Input1", destination="Slot2InB"),
               dict(source="Slot1OutB", destination="Slot2InC"),
               dict(source="Slot1OutC", destination="Slot2InD"),
               dict(source="Slot1OutA", destination="Output1"),
               dict(source="Slot1OutB", destination="Output2"),
               dict(source="Input2", destination="Slot1InB")]

#Multi-instrument parameters. The output and frontend settings are made to ensure get about 3-5 Volts for the pulse outputs (while having a wave amplitude of 0.8).
m.set_connections(connections=connections) 
m.set_output(1, '14dB')
m.set_output(2, '14dB')
m.set_frontend(1, impedance="50Ohm", attenuation="0dB", coupling="DC")
m.set_frontend(2, impedance="50Ohm", attenuation="0dB", coupling="DC")

#Oscillation paremeters. The timebase is converted to seconds. The trigger is set to the channel A to prepare the signal for the integration. 
osc.set_timebase(0, duration_laser*1e-9)
osc.set_trigger(source="ChannelA", level=0.30)
osc.set_acquisition_mode(mode="Precision")
osc.disable_input(4)

for n, duration_MW in enumerate(xdata):
    t_1cycle = duration_MW + duration_laser + twait #The total time it takes for one cycle of the laser pulse.
    tl = np.linspace(0, t_1cycle, 500)              #The time vector for the laser pulse. One laser pulse will be created using this vector.

    if tl[-1]%tm[-1] != 0:                          #If the total time for one cycle of the laser pulse is not an integer multiple of the MW pulse cycle time,
        remainder = int(tl[-1]%tm[-1])              #we extend the total time for the laser pulse to the next integer multiple of the MW pulse cycle time.
        div = math.ceil(tl[-1]/tm[-1])
        new_length = div*tm[-1]
        t_padding = new_length - t_1cycle           #"t_padding" will be the extra time we add to tl to make it an integer multiple of tm. This will be the time from the laser pulse to the MW pulse.
        print(t_padding)                          #One problem is that this time keeps changing, so the time between the laser pulse and the MW pulse is not constant between iterations. 
        # laser += (tm[-1]-remainder)*[0,]
        tl = np.linspace(0, new_length, 500)        #Update "tl".
        
    print(new_length) 
    #Create the MW pulse as a list using tm.
    mic_wave = list(np.heaviside(tm-0,0)-np.heaviside(tm-duration_MW,0))
    #Create the laser pulse as a list using tl.
    laser = list(np.heaviside(tl-duration_MW,0)-np.heaviside(tl-duration_MW-duration_laser,0))

    freq_MW = 1/(tm[-1]*1e-9)
    freq_laser = 1/(tl[-1]*1e-9)
    print(freq_MW/freq_laser) #This should be an integer. If it is not, the MW pulse will not be in the right place.
    
    awg.generate_waveform(channel=1, sample_rate='Auto', lut_data=laser, frequency=freq_laser, amplitude=0.8) 
    awg.generate_waveform(channel=2, sample_rate='625Ms', lut_data=mic_wave, frequency=freq_MW, amplitude=0.8) #Generate the MW pulse with higher sampling rate.
    awg.pulse_modulate(2, dead_cycles=int(tl[-1]/tm[-1])-1, dead_voltage=0)
    awg.sync_phase() #This is important to synchronize the two channels in the AWG.
    
    time.sleep(0.001)
    data = osc.get_data(wait_complete=True, wait_reacquire=True)
    time.sleep(0.001)

    #Give a delay to integrate the photoluminescence signal to avoid falling and rising edges. 
    delay_start = 1000
    delay_end = 10000
    
    inddelay = np.where(np.array(data['time'])*1e9>delay_start)[0][0]
    ind0 = np.where(np.array(data['time'])*1e9>t_integ+delay_start)[0][0]
    ind1 = np.where(np.array(data['time'])*1e9>duration_laser-t_integ-delay_end)[0][0]
    ind2 = np.where(np.array(data['time'])*1e9>duration_laser-delay_end)[0][0]
    
 
    
    a_vec[n] = np.mean(data['ch2'][inddelay:ind0])
    b_vec[n] = np.mean(data['ch2'][ind1:ind2])
    photolum[n]= a_vec[n]/b_vec[n]
    
    plt.figure(n)
    plt.plot(np.array(data['time'])[:], np.array(data['ch2'])[:], 'b-')
    plt.plot(np.array(data['time'])[inddelay:ind0], 
             np.array(data['ch2'])[inddelay:ind0], 'r-')
    plt.plot(np.array(data['time'])[ind1:ind2], 
             np.array(data['ch2'])[ind1:ind2], 'r-')
    plt.ylim([0,0.12])
    plt.show()
    
    
def decay_func(x, b, a0, a1, omega):
     return a0 * np.sinc(x / b) * np.exp(-omega * x) + a1

# Fit the data with decay_func
# initial_guess = [8e-3, 0.1, 1.0, 8e-3]  # b, a0, a1, omega
# popt, pcov = curve_fit(decay_func, xdata, photolum, p0=initial_guess)
# b_fit, a0_fit, a1_fit, omega_fit = popt
   
# # Generate a smooth fit curve
# x_fit = np.linspace(xdata[0], xdata[-1], 200)
# photolum_fit = decay_func(x_fit, b_fit, a0_fit, a1_fit, omega_fit)
   
   
plt.plot(xdata, photolum, 'b-', label='Data')
plt.ylim(0.8, 1.05)
# plt.plot(x_fit, photolum_fit, 'r-', label='Fitted curve')
plt.xlabel("Time (s)")
plt.ylabel("Photoluminescence (arb. units)")
plt.legend()
plt.show()

# awg.enable_output(1, enable=False)
# awg.enable_output(2, enable=False)

except Exception as e:
raise e
finally:
# Close the connection to the Moku device
# This ensures network resources are released correctly
m.relinquish_ownership()

Hello @ecekbb,
To prevent drift, the actual sample rate has to divide into the sample rate 1.25Gs/s. For example, f_micwave=500kHz * 201 pts = 100500000 pts/s and 1.25Gs/s / 100500000 = 12.438 so this setting would cause a bit of drift. You can adjust the settings of your AWG to accommodate this, or you can use the normal wave form generator with settings similar to this


I hope this was helpful and if you have any more questions please let me know!
(Forum post for reference: AWG frequency error/offset - #4 by nnosk55)

Hello,
I tried doing the adjustments you mentioned. So I chose f_microwave = 1/(2000*e-9) * 500 = 250 000 000 pts/s and 1.25Gs/s / 250 000 000 = 5. So it is an integer, but it is still causing drift. Should I make this adjustment also for the pulse in Channel 1? However that would be hard for me to implement since I am updating the cycle time at each iteration. Because I am sweeping through multiple durations for the MW, this condition is hard for me to ensure at each loop.
Thank you for your time and help.

Edit: I also tried another solution where i kept this integer for Channel 2, but I still observe this shifting problem.

Hello @ecekbb ,
Please reach out to support@liquidinstruments.com for further support. Thank you!