National Instruments Photon Counter DAQ Controlled - ACBJayichLab/NV_ABJ GitHub Wiki

Importing

     from NV_ABJ.hardware_interfaces.photon_counter.ni_daq_counters.ni_photon_counter_daq_controlled import *

Code Examples

To implement a counter class you simply need to supply the name of the device, the counter port, and the trigger port

device_name = "PXI1Slot4"
counter_pfi = "pfi0"
trigger_pfi = "pfi2"


photon_counter = NationalInstrumentsPhotonCounterDaqControlled(device_name=device_name,
                               counter_pfi=counter_pfi,
                               trigger_pfi=trigger_pfi)

List of all possible inputs

  • device_name (str): name of the national instruments device for example "PXI1Slot4"
  • counter_pfi (str): This is the counter signal that the photon counter is attached to traditionally we attach to "pfi0" on the device_name
  • trigger_pfi (str): This is used by the sequence synchronizer so that we can take data for a prescribed time this is usually "pfi#" implemented by using BNC into user# and on the terminal block connecting user# to pfi#
  • ctr (str, optional): This is a counter used by the daq. If you have multiple counters running simultaneously on the same device you may need to change this to an available "ctr#". Defaults to "ctr0".
  • port (str, optional): This is a digital internal port for counting cycles. If you have multiple counters running simultaneously on the same device you may need to change this to an available "port#". Defaults to "port0".
  • number_of_clock_cycles (int, optional): This is the number of clock cycles when sampling data. Defaults to 2.
  • timeout_waiting_for_data (float, optional): This is how long the daq will wait for a trigger in this case the trigger is internal and will be activated once loaded. Defaults to 60.

Counts for a Dwell Time Untriggered

To get the counts of a singular count for a dwell time you can call get_counts_raw function

counts = photon_counter.get_counts_raw(dwell_time_nano_seconds=150)

Counts per second for a Dwell Time Untriggered

To get per seconds just call get_counts_per_second

counts_per_sec = photon_counter.get_counts_per_second(dwell_time_nano_seconds=150)

Repeated Counts for a Dwell Time Triggered

If you are running a repeated triggered event such as with a sequence that's repeated like we traditionally get_counts_raw_when_triggered

counts = photon_counter.get_counts_raw_when_triggered(dwell_time_nano_seconds=150,number_of_data_taking_cycles=10)

Repeated Counts per second for a Dwell Time Triggered

If you want to return per second on the trigger

counts_per_sec = photon_counter.get_counts_per_second_when_triggered(dwell_time_nano_seconds=150,number_of_data_taking_cycles=10)

Photon Counter Physical Implementation

Base Resolution and Sample Counts

Because this relies on two functionalities there are two clocks that matter. There is the daq clock that will determine the max sample rate and the minimum sample time known as the maximum analog out of the daq, then there is the clock that will determine the step resolution also known as the maximum timebase of the daq. If you would like to find this out you can use the code below for a NI PXIe-6363 there is a base clock of 100MHz, and a sample clock of 20MHz. This corresponds to a sampling max of 20 mega count per second, minimum timing of 150ns, and a minimum clock resolution of 20ns.

import nidaqmx
import nidaqmx.system

device_name = "PXI1Slot4"
port = "port0"
device = nidaqmx.system.device.Device(device_name)

daq_clock = device.ci_max_timebase

with nidaqmx.Task() as samp_clk_task:
        samp_clk_task.di_channels.add_di_chan(f"{device_name}/{port}")
        sample_clock = samp_clk_task.timing.samp_clk_max_rate*2 # This converts it to Hz 

print(f"The DAQs max clock is {daq_clock} Hz")
print(f"The DAQs max sample rate is {sample_clock} Hz")
print(f"Minimum sample time {3*pow(10,9)/sample_clock} ns")
print(f"Timing resolution {2*pow(10,9)/daq_clock} ns")

The reason this is implementation is called DAQ controlled is because the DAQ clock cycles are utilized to set the total time that data is collected. This is in juxtaposition to triggering twice with the sequence generator and utilizing buffers to count the cycles. The method can be outlined in the graphs below. Looking first at the internal measurement with the graph below we can see that the clock cycles of the daq are higher than the sample rate. To be a valid sample time we must have at least three clock cycles of time this corresponds to a 2 cycle on off where the first one can't trigger it. daq_cycles_to_resolution

This also can show you why we can't have a resolution better than that of the clock on the daq. This is because we can't subdivide the clock speed to be less putting a hard physical limit on it. The buffered approach can average out the minimum resolution of the daq but it is a weighted average which you can post process too if you want to know the counts for a resolution between that of the daq. This method of implementation in the code simply makes it more apparent this is the case.

Triggered Output

When we want to trigger the counts we need to send in a pulse this is for synchronization and we can see how there will be an associated delay in the signal

signal_timing

In clock cycle 2 we see that the trigger is on and we should start taking the data at clock cycle 3 we begin our acquisition and we can count a total of two rising edges for the photons so the daq is able to report two counts of photons during the 6 cycles of acquisition. This should illustrate that timing is a source of error because the actual photon rate is not 1/3 the daq clock but 1/2 the daq clock speed. If we want to reduce this specific counting error we can take the same duration of data repeatedly and average out. This will however not remove the fundamental issue of counting on the daq cycles. If the photons come at the end of the counting we will consistently measure on photon less than we want to. This may average out to be near the expected value but will in fact always be less. To compensate for this we actually should add one additional cycle to the acquisition time allowing the acquisition time this will allow us to measure the trailing photon for a given dwell time making the center of the Gaussian of points collected no longer below the actual but on the actual number of photons counted. This effect is below where 3 photons are measured during the acquisition time.

signal_timing_with_compensated_cycle

This method of compensation will affect how short of a pulse we can measure but the benefits appear to outweigh any cons associated with this implementation method.

Fundamental Limitations

The national instruments' photon counter has a fundamental limit associated with the daq clock. This can be visualized below where we see a stepping function. The Clock that is input into the daq is a 20MHz signal from the SRS and we can see that the counts increase by one roughly every clock cycle.

ni_daq_dwell_time_20MHz

You may be tempted to think that a higher frequency will allow you to have a lower resolution timing. This is not the case unfortunately as we increase to 30MHz we can see the binning has stayed consistently with the National Instruments clock frequency.

ni_daq_dwell_time_30MHz

If you would like to make a similar plot to test your daq you can use a similar code snippet as below. It creates a photon counter and increments it every 5ns from 150ns to 600ns

device_name = "PXI1Slot4"
trigger_pfi = "pfi2"
counter_pfi = "pfi0"
ctr = "ctr0"
port = "port0"


photon_counter = NationalInstrumentsPhotonCounterDaqControlled(device_name=device_name,
                               counter_pfi=counter_pfi,
                               trigger_pfi=trigger_pfi)

nano_seconds_array = np.arange(200,600,5)
dwell_counts_averages = np.zeros(len(nano_seconds_array))


for index,dwell_time in enumerate(nano_seconds_array):
    counts_list = np.zeros(100)
    for i,v in enumerate(counts_list):
        counts = photon_counter.get_counts_raw(dwell_time_nano_seconds=dwell_time)
        counts_list[i] = counts

    dwell_counts_averages[index] = np.mean(counts_list)

import matplotlib.pyplot as plt
print(nano_seconds_array,dwell_counts_averages)
plt.scatter(nano_seconds_array,dwell_counts_averages)
plt.xlabel("Time Duration (ns)")
plt.ylabel("Average Counts")
plt.title("Average Counts per Dwell Time")
plt.show()

The other part we must be aware of is the fact that dwell time and measurable frequency are linked. The shorter the dwell time the lower the frequency you can accurately measure and we will start to see binning occur when there isn't averaging. Below we can see this affect for a 1-50MHz sweep using an SG384 and the measured counts per second using the National Instruments Daq. The ideal case is a 45 degree line where we see the same frequency as what we input. We also observe aliasing occur at the clock frequency of the DAQ which is 100 MHz. TO Avoid aliasing this is why National Instruments limits sampling to a 20 MHz clock.

ni_10ms_dwell_frequency_sweep ni_1ms_dwell_frequency_sweep ni_0_5_ms_dwell_frequency_sweep

The above plots were made using the code below

sg = SG380.make_gpib_connection("GPIB0::27::INSTR")
sg.turn_on_signal()

frequency_list = np.linspace(1_000_000,50_000_000,500)
counts_per_second  = np.zeros(len(frequency_list))

for index, frequency in enumerate(frequency_list):

    sg.set_frequency_hz(frequency)
    photon_counter = NationalInstrumentsPhotonCounterDaqControlled("PXI1Slot4","pfi0","pfi2")
    counts = photon_counter.get_counts_per_second(500)

    counts_per_second[index] = counts

plt.scatter(frequency_list/pow(10,6),counts_per_second/pow(10,6))
plt.xlabel("Signal Generator Frequency Input (MHz)")
plt.ylabel("NI Measured Mega Counts Per Second")
plt.title("Measuring Daq Counting Limit 0.5 ms Sample Time")
plt.show()

As we can see as the dwell time is lowered the binning is exaggerated with the amount of step functions. We can also see the solution because the points overlap. This means we can solve this by averaging down with the assumption that there is a Gaussian error centered at the actual value allowing for the actual value to emerge.

If we want to improve our resolution we can take an average using the code below we sweep the same frequency range but average at each frequency for 100 times

sg = SG380.make_gpib_connection("GPIB0::27::INSTR")
sg.turn_on_signal()

frequency_list = np.linspace(1_000_000,50_000_000,500)
counts_per_second  = np.zeros(len(frequency_list))

for index, frequency in enumerate(frequency_list):
    counts_ave = np.zeros(100)
    for i, v in enumerate(counts_ave):
        sg.set_frequency_hz(frequency)
        photon_counter = NationalInstrumentsPhotonCounterDaqControlled("PXI1Slot4","pfi0","pfi2")
        counts = photon_counter.get_counts_per_second(500)
        counts_ave[i] = counts

    counts_per_second[index] = np.mean(counts_ave)

plt.scatter(frequency_list/pow(10,6),counts_per_second/pow(10,6))
plt.xlabel("Signal Generator Frequency Input (MHz)")
plt.ylabel("NI Measured Mega Counts Per Second")
plt.title("Measuring Daq Counting Limit 0.5 ms Sample Time 100 Averages")
plt.show()

ni_0_5_ms_dwell_frequency_sweep_100_averages

Here, we can see that the same dwell time of 500 ns leads to a smoother count that is almost identical to a longer dwell time. This points to a fundamental limit in the hardware which may require a higher amount of average than initially anticipated when just looking at the physics of the NV. This also means that if we have a short dwell time required and a not-very bright source we will have to average for more runs, if we have an overly bright source (~20Mcount/s) we will run into aliasing when taking data and our counts can appear to be lower than expected. This will not be solved by averaging and can be seen in the non-linear aspects of the above plots. We can also see that in the first test to determine the minimum resolution with a fixed frequency by averaging over many counts we do not improve the bin resolution leaving the graph choppy instead of a smooth 45 degree line.