Source code for silq.tools.pulse_tools

import numpy as np
from typing import List, Union
import logging
from matplotlib import pyplot as plt


__all__ = ["pulse_to_waveform_sequence"]

logger = logging.getLogger(__name__)


[docs]def pulse_to_waveform_sequence( max_points: int, frequency: float, sampling_rate: float, frequency_threshold: float, total_duration=None, final_delay_threshold: float = None, min_points: int = 1, sample_points_multiple: int = 1, point_offsets: List[int] = [-1, 0, 1, 2], filters=[], plot=False, ) -> Union[dict, None]: """ This method can be used when generating a periodic signal with an AWG device. Given a frequency and duration of the desired signal, a general AWG can produce that signal by repeating one waveform (waveform_1) for a number of times (cycles) and ending with a second waveform (waveform_2). This is a practical way of generating periodic signals that are long in duration without using a lot of the available RAM of the AWG. Because of the finite sampling rate and restrictions on amount of sample points (which must generally be a multiple of a certain number), there will be an error in the period of the generated signal. This error goes down with the number of periods (n) that one cycle of the repeated waveform contains. This function calculates the minimum number n for which the error threshold is satisfied. Therefore minimizing the total amount of sample points that need to be stored by the AWG. Args: duration: duration of the signal in seconds frequency: frequency of the signal in Hz sampling_rate: the sampling rate of the waveform threshold: threshold in relative period error n_min: minimum number of signal periods that the waveform must contain n_max: maximum number of signal periods that a waveform can contain sample_points_multiple: the number of samples must be a multiple of Returns: Dict containing: 'optimum': Dict of optimal settings 'error': Relative frequency error of optimal settings 'final_delay': Remaining duration of pulse after waveform repetitions. Cannot be negative. 'periods': Periods within waveform (using modified frequency) 'repetitions': Repetitions of waveform to (almost) reach end of pulse 'duration': Duration of main waveform 'points': Number of waveform points 'idx': index of optimal result. first index corresponds to period index of periods between min_periods and max_periods. Second index is the point offset index. 'modified_frequency': Frequency close to target frequency whose period perfectly fits in the number of points 'filtered_results': Array of settings that satisfy filters 'repetitions_multiple': Repetition array for all settings 'final_delays': Array of final_delays for all settings 'errors': Array of relative frequency errors for all settings 'periods_range': Range of periods that have been considered If the minimum number of periods (set by min_points) exceeds the maximum number of periods (set by max_points), None is returned """ t_period = 1 / abs(frequency) max_periods = int(max_points / sampling_rate / t_period) # Bounded by max_points min_periods = int( np.ceil(min_points / sampling_rate / t_period) ) # Bounded by min_points if min_periods >= max_periods: return None periods = np.arange(min_periods, max_periods + 1) t_periods = periods * t_period # Always consider neighbouring points as well, as they may have more favourable settings point_offsets_multiple = np.array(point_offsets, dtype=int) * sample_points_multiple # Calculate frequency errors points_periods = ( t_periods * sampling_rate ) # Unrounded points for each periods value (1D) points_cutoff = ( sample_points_multiple * (points_periods // sample_points_multiple) ).astype( int ) # Rounded (floor) points for each periods value (1D) # Rounded (floor) points for each periods value with offsets (2D) points_cutoff_multiple = points_cutoff[:, np.newaxis] + point_offsets_multiple # Durations of rounded (floor) points for each periods value with offsets (2D) t_cutoff_multiple = points_cutoff_multiple / sampling_rate # Relative frequency error (2D) errors = np.abs(1 - t_cutoff_multiple / t_periods[:, np.newaxis]) # Calculate final delays final_delays = total_duration * np.ones(points_cutoff_multiple.shape) # We add a small value to avoid machine precision errors. # Note that we add instead of round since we do floor division repetitions_multiple = ((total_duration + 1e-13) // t_cutoff_multiple).astype(int) t_periods_cutoff_multiple = t_cutoff_multiple * repetitions_multiple final_delays -= t_periods_cutoff_multiple # Filter results filtered_results = np.ones(errors.shape, dtype=bool) # Ensure that all results have at least one repetition filtered_results[repetitions_multiple < 1] = False if np.all(~filtered_results): # TODO Don't raise an error, but return the original frequency raise RuntimeError('Pulse cannot be approximated because it is too short') filter_arrs = { "frequency": errors, "final_delay": final_delays, "points": points_cutoff_multiple, } # Prepend thresholds to filters if final_delay_threshold is not None: filters = [("final_delay", final_delay_threshold)] + filters filters = [("frequency", frequency_threshold)] + filters for k, (filter_name, threshold) in enumerate(filters): filter_arr = filter_arrs[filter_name] if np.any(filtered_results[filter_arr <= threshold]): logger.debug( f"Found {np.sum(filtered_results[filter_arr <= threshold])} " f"satisfying {filter_name} < {threshold}" ) filtered_results[filter_arr > threshold] = False else: min_val = filter_arr[filtered_results].min() remaining_results = filtered_results.copy() remaining_results[filter_arr != min_val] = 0 # min_idx is a tuple containing the first element that minimizes # according to the filter min_idx = np.unravel_index( remaining_results.argmax(), remaining_results.shape ) log_str = ( f"Could not find any sine waveform decomposition with " f"{filter_name} error < {threshold}, " ) # Print log message, either as warning or as debug message if k == 1 or k == 2 and final_delay_threshold is not None: logger.warning(log_str) else: logger.debug(log_str) break else: logger.debug("Satisfied all filters, choosing lowest frequency error") min_val = errors[filtered_results].min() remaining_results = filtered_results.copy() remaining_results[errors != min_val] = 0 min_idx = np.unravel_index(remaining_results.argmax(), remaining_results.shape) modified_frequency = 1 / ( points_cutoff_multiple[min_idx] / periods[min_idx[0]] / sampling_rate ) if frequency < 0: modified_frequency *= -1 optimum = { "error": errors[min_idx], "final_delay": final_delays[min_idx], "periods": min_periods + min_idx[0], "repetitions": repetitions_multiple[min_idx], "duration": points_cutoff_multiple[min_idx] / sampling_rate, "points": points_cutoff_multiple[min_idx], "idx": min_idx, "modified_frequency": modified_frequency, } if plot: fig, axes = plt.subplots(3, sharex=True) axes[0].semilogy(periods, repetitions_multiple) axes[1].semilogy(periods, errors) axes[2].semilogy(periods, final_delays) axes[0].set_ylabel("Repetitions") axes[1].set_ylabel("Rel. frequency error") axes[2].set_ylabel("Final delay (s)") axes[2].set_xlabel("Periods") handle = axes[0].vlines( optimum["periods"], *axes[0].get_ylim(), linestyle="--", lw=2 ) axes[1].vlines(optimum["periods"], *axes[1].get_ylim(), linestyle="--", lw=2) axes[2].vlines(optimum["periods"], *axes[2].get_ylim(), linestyle="--", lw=2) axes[0].legend([handle], ["optimum"]) plt.subplots_adjust(hspace=0) return { "errors": errors, "periods_range": periods, "final_delays": final_delays, "repetitions_multiple": repetitions_multiple, "points_per_period": points_cutoff_multiple, "filtered_results": filtered_results, "optimum": optimum, }