Direct and Mixed mode#

Presto’s inputs and outputs can operate in two modes: “direct” and “mixed”. In direct mode, the signals generated by the logic are output directly by the digital-to-analog converter (DAC), and similarly the signals acquired by the analog-to-digital converter (ADC) are directly available to the logic. With “logic” we mean the further signal processing and analysis provided by the different modes of operation of Presto, like the pulsed mode and lockin mode.

In mixed mode, instead, we use the digital upconversion and downconversion features of Presto. On the logic side, we are in the intermediate-frequency (IF) domain and all samples are complex valued, or equivalently all samples are pairs of I and Q values. For the outputs, these samples are fed to the I and Q ports of a digital IQ mixer that performs frequency upconversion to the radio-frequency (RF) domain using a numerically-controlled oscillator (NCO) as the carrier tone (often also called local oscillator, LO). The real-valued RF signal is then output by the DAC.

Similarly for the input side, the ADC acquires a real-valued RF signal that is then fed to a digital IQ mixer that performs frequency downconversion to the IF domain using an NCO. The complex-valued IF signal is then propagated to the rest of the logic.

Digital frequency conversion is conceptually identical to the traditional way of generating high-frequency RF signals using baseband outputs and analog, external IQ mixers and local oscillators. The difference is that with Presto all these analog external components are replaced with digital logic (i.e. math on the signals), cutting down on the number of components in an experimental setup and thus reducing cost as well as troubleshooting and calibration efforts. Moreover, all the imperfections of real-world analog components (nonlinearities, amplitude and phase imbalance, LO leakage, …) are just not applicable anymore.

Programming direct and mixed mode#

Starting from version 2.12.0, the Presto API has an autoconfiguration feature: all you need to do is set the arguments adc_mode and dac_mode when you instantiate the Pulsed or Lockin classes. For example:

from presto import pulsed
from presto.hardware import AdcMode, DacMode

with pulsed.Pulsed(
    address="192.168.20.42",
    adc_mode=AdcMode.Direct,
    dac_mode=DacMode.Direct,
) as pls:
    ...

will initialize Presto in pulsed mode and direct mode. Similarly:

from presto import lockin
from presto.hardware import AdcMode, DacMode

with lockin.Lockin(
    address="192.168.20.42",
    adc_mode=AdcMode.Mixed,
    dac_mode=DacMode.Mixed,
) as lck:
    ...
    lck.hardware.configure_mixer(3.5e9, in_ports=1, out_ports=[1, 3])
    ...

will initialize Presto in lockin mode and mixed mode. It will also configure a carrier of 3.5 GHz for digital upconversion on output ports 1 and 3 and digital downconversion on input port 1. See Hardware.configure_mixer() for more details on programming the digital IQ mixers.

Finer control#

With the above snippets of code, the Presto API will choose automatically the optimal sampling rate on each input and output port based on the provided carrier frequencies. If you want finer control on the chosen settings, or for the rare cases in which the automatic algorithm fails, you can provide explicitly the sampling rates. See Recommended DAC configuration and Advanced tile configuration for more information.

Migrating from version 2.11.0 and prior#

If your measurement scripts were written using Presto API version 2.11.0 or earlier, they should continue to work just like before also on version 2.12.0 and later. If you find that’s not the case, please contact our support and we’ll look into it: you may have found a bug!

Nevertheless, you might still want to update your code to make use of the new “autoconfiguration” feature. That will make your code easier to read, more resilient to changes in parameters, and maybe even improve performance if you happened to use a suboptimal configuration. See below for some common coding patterns in earlier versions of the Presto API, and how they can be simplified with the autoconfiguration feature.

We’ll use the diff syntax below to make which lines of code should be added, removed or left unchanged. Like this:

  this line should remain unchanged
- this line should be removed
+ this line should be added

Class instantiation#

When instantiating the Lockin or Pulsed class, it is as simple as removing all settings related to adc_fsample and dac_fsample. Also, for dac_mode use either DacMode.Direct or DacMode.Mixed, don’t use the low-level DacMode.Mixed02, DacMode.Mixed04 and DacMode.Mixed42.

Here’s some examples:

  with lockin.Lockin(
      address="192.168.20.42",
      adc_mode=AdcMode.Direct,
-     adc_fsample=AdcFSample.G4,
      dac_mode=DacMode.Direct,
-     dac_fsample=DacFSample.G6,
  ) as lck:
      ...
  with pulsed.Pulsed(
      address="192.168.20.42",
      adc_mode=AdcMode.Mixed,
-     adc_fsample=AdcFSample.G2,
+     dac_mode=DacMode.Mixed,
-     dac_mode=DacMode.Mixed42,
-     dac_fsample=DacFSample.G10,
  ) as pls:
      ...
  with pulsed.Pulsed(
      address="192.168.20.42",
      adc_mode=AdcMode.Mixed,
-     adc_fsample=AdcFSample.G4,
+     dac_mode=DacMode.Mixed,
-     dac_mode=[DacMode.Mixed42, DacMode.Mixed02, DacMode.Mixed02, DacMode.Mixed02],
-     dac_fsample=[DacFSample.G10, DacFSample.G6, DacFSample.G6, DacFSample.G6],
  ) as pls:
      ...

Configure mixer in lockin mode#

Prior to version 2.12.0, Hardware.configure_mixer() used to have effect (almost) immediately in lockin mode. A common pattern was to sleep for a small time after calling the method to wait for the settings to propagate to the hardware before continuing with the experiment.

When using the autoconfiguration feature, this is no longer the case and a call to apply_settings() is required after configure_mixer() for the settings to take effect. On the other hand, no sleep is necessary anymore.

  for freq in some_frequency_array:
      lck.hardware.configure_mixer(freq, in_ports=1, out_ports=1)
-     lck.hardware.sleep(1e-3, False)
+     lck.apply_settings()
  
      data = lck.get_pixels(10_000)
      ...

You don’t need to call apply_settings() every time you change something, you can combine multiple changes and then apply settings only once:

og = lck.add_output_group(1, 1)
og.set_frequency(240e6)  # requires apply_settings
for amp in some_amplitude_array:
    for freq in some_frequency_array:
        lck.hardware.configure_mixer(freq, in_ports=1, out_ports=1)  # requires apply_settings
        og.set_amplitude(amp)  # requires apply_settings
        lck.apply_settings()  # HERE!

        data = lck.get_pixels(10_000)
        ...

Synchronize multiple mixers#

Prior to version 2.12.0, each call to Hardware.configure_mixer() was handled independently. That meant that a little extra care was required when using different mixer frequencies to achieve a reproducible phase relationship. One would pass the sync=False optional parameter to the first call to configure_mixer(), and sync=True to the second so that the two mixers would start their carriers simultaneously.

Using the autoconfiguration feature in version 2.12.0, this is not necessary anymore: all calls to configure_mixer() are handled together later on, when calling the methods Pulsed.run() and Lockin.apply_settings() in pulsed and lockin mode, respectively.

So, just drop the sync parameters:

  with pulsed.Pulsed(...) as pls:
      ...
-     pls.hardware.configure_mixer(freq=4e9, in_ports=2, out_ports=2, sync=False)
-     pls.hardware.configure_mixer(freq=8e9, out_ports=3, sync=True)
+     pls.hardware.configure_mixer(freq=4e9, in_ports=2, out_ports=2)
+     pls.hardware.configure_mixer(freq=8e9, out_ports=3)
      ...
      pls.run(T, 1, 10_000)
  with lockin.Lockin(...) as lck:
      ...
-     lck.hardware.configure_mixer(freq=4e9, in_ports=2, out_ports=2, sync=False)
-     lck.hardware.configure_mixer(freq=8e9, out_ports=3, sync=True)
+     lck.hardware.configure_mixer(freq=4e9, in_ports=2, out_ports=2)
+     lck.hardware.configure_mixer(freq=8e9, out_ports=3)
+     lck.apply_settings()