Skip to content

qubic.rfsoc.hwconfig

ElementConfig implementation for current QubiC gateware running on the RFSoC (ZCU216) platform. Responsible for converting phase/frequencies/amplitudes from natural units into words/buffers that can be loaded into FPGA memory. Instantiated during assembly based on provided channel config.

RFSoCElementCfg

Bases: ElementConfig

ElementConfig implementation for QubiC 2.0 on ZCU216.

Source code in qubic/rfsoc/hwconfig.py
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
class RFSoCElementCfg(ElementConfig):
    """
    ElementConfig implementation for QubiC 2.0 on ZCU216.
    """
    def __init__(self, samples_per_clk: int = 16, interp_ratio: int = 1, env_max_samples: int = 4095):
        self.env_n_bits = 16
        self.freq_n_bits = 32
        self.n_phase_bits = 17
        self.amp_n_bits = 16
        self.interp_ratio = interp_ratio
        self.env_max_samples = env_max_samples
        super().__init__(2.e-9, samples_per_clk)

    def get_freq_addr(self, freq_ind: int) -> int:
        return freq_ind

    def get_cfg_word(self, elem_ind: int, mode_bits: int = None) -> int:
        if mode_bits is not None: 
            raise Exception('mode not implemented')
        return elem_ind

    def get_freq_buffer(self, freqs: list | np.ndarray) -> np.ndarray:
        """
        Converts a list of frequencies (in Hz) to a buffer, where each frequency 
        has 16 elements:

          - `[0]` is a 32-bit freq word, encoding phase increment per clock cycle
          - `[1:15]` are 16 bit I MSB + 16 bit Q LSB, encoding 15 phase offsets for 
                each sample (except 0) within a clock cycle
        16-element arrays for each frequency are concatenated into a single 1D numpy array.

        Parameters
        ----------
        freqs: list | np.ndarray
            List of frequencies, in Hz

        Returns
        -------
        np.ndarray:
            Frequency buffer: array of concatenated 16-element frequency lists; shape
            is `(len(freqs)*16,)`

        """
        freq_buffer = np.empty(0)
        scale = 2**(self.freq_n_bits/2 - 1) - 1
        for freq in freqs:
            cur_freq_buffer = np.zeros(self.samples_per_clk)
            if freq is not None:
                cur_freq_buffer[0] = int(freq*2**self.freq_n_bits/self.fpga_clk_freq) & (2**self.freq_n_bits - 1)
                for i in range(1, self.samples_per_clk):
                    i_mult = int(round(np.cos(2*np.pi*freq*i*self.sample_period)*scale) % (2**(self.freq_n_bits/2)))
                    q_mult = int(round(np.sin(2*np.pi*freq*i*self.sample_period)*scale) % (2**(self.freq_n_bits/2)))
                    cur_freq_buffer[i] = (i_mult << (self.freq_n_bits//2)) + q_mult

            freq_buffer = np.append(freq_buffer, cur_freq_buffer)

        return freq_buffer

    def get_phase_word(self, phase: float) -> int:
        """
        Converts phase to 17 bit unsigned int, normalized to 2*pi
        (i.e. 2\*pi -> 2\*\*17)
        Parameters
        ----------
        phase: float
            input phase (in radians)

        Returns
        -------
        int:
            17-bit phase word
        """
        phase = int(((phase % (2*np.pi))/(2*np.pi)) * 2**17)
        return phase % 2**17

    def get_env_word(self, env_ind: int, length_nsamples: int) -> int:
        """
        Returns the envelope word stored in the pulse command, which encodes the
        starting address and length of the pulse envelope.

        Parameters
        ----------
        env_ind: int
            starting index of the envelope in the envelope buffer
        length_nsamples: int
            length of the envelope in samples (could be the same as the
            pulse length in samples, or lower if interpolating)
        Returns
        -------
        int:
            env_word
        """
        if env_ind + length_nsamples > self.env_max_samples:
            raise Exception('{} exceeds max env length of {}'.format(env_ind + length_nsamples, self.env_max_samples))
        return env_ind//int(self.samples_per_clk/self.interp_ratio) \
                + (int(np.ceil(self.interp_ratio*length_nsamples/self.samples_per_clk)) << 12)

    def get_cw_env_word(self, env_ind: int) -> int:
        """
        Returns the envelope word for a CW pulse. `env_ind` is required 
        since the CW pulse requires a single clock cycle of envelope
        data to be stored.

        Parameters
        ----------
        env_ind: int
            starting index of the envelope in the envelope buffer
        Returns
        -------
        int:
            env_word
        """
        if self.samples_per_clk//self.interp_ratio > self.env_max_samples:
            raise Exception('{} exceeds max env \
                            length of {}'.format(env_ind + self.samples_per_clk//self.interp_ratio, self.env_max_samples))
        return env_ind//int(self.samples_per_clk/self.interp_ratio)


    def get_env_buffer(self, env: np.ndarray | list | dict):
        """
        Converts env to a list of samples to write to the env buffer memory.

        Parameters
        ----------
        env : np.ndarray, list, or dict
            if np.ndarray or list this is interpreted as a list of samples. Samples
            should be normalized to 1.

            if dict, a function in the qubitconfig.envelope_pulse library is used to
            calculate the envelope samples. env['env_func'] should be the name of the function,
            and env['paradict'] is a dictionary of attributes to pass to env_func. The 
            set of attributes varies according to the function but should include the 
            pulse duration twidth

        Returns
        -------
        np.ndarray:
            buffer of envelope data

        """
        if isinstance(env, np.ndarray) or isinstance(env, list):
            env_samples = np.asarray(env)
        elif isinstance(env, dict):
            dt = self.interp_ratio * self.sample_period
            env_func = getattr(ep, env['env_func'])
            _, env_samples = env_func(dt=dt, **env['paradict'])
            # ipdb.set_trace()
        elif env == 'cw':
            env_samples = np.ones(self.samples_per_clk//self.interp_ratio)
        else:
            raise TypeError(f'env {env} must be dict or array')

        env_samples = np.pad(env_samples, (0, (self.samples_per_clk//self.interp_ratio - len(env_samples)
                % self.samples_per_clk//self.interp_ratio) % self.samples_per_clk//self.interp_ratio))

        return (cg.twos_complement(np.real(env_samples*(2**(self.env_n_bits-1)-1)).astype(int), nbits=self.env_n_bits) << self.env_n_bits) \
                    + cg.twos_complement(np.imag(env_samples*(2**(self.env_n_bits-1)-1)).astype(int), nbits=self.env_n_bits)

    def length_nclks(self, tlength: float) -> int:
        """
        Converts pulse length in seconds to integer number of clock cycles.

        Parameters
        ----------
        tlength: float
            time in seconds

        Returns
        -------
        int:
            time in clocks
        """
        return int(np.ceil(tlength/self.fpga_clk_period))

    def get_amp_word(self, amplitude: float) -> int:
        """
        Converts amplitude (normalized to 1) into command word for FPGA

        Parameters
        ----------
        amplitude: float

        Returns
        -------
        int:
            amplitude word

        """
        return int(cg.twos_complement(amplitude*(2**(self.amp_n_bits - 1) - 1), nbits=self.amp_n_bits))

get_amp_word(amplitude)

Converts amplitude (normalized to 1) into command word for FPGA

Parameters:

Name Type Description Default
amplitude float
required

Returns:

Name Type Description
int int

amplitude word

Source code in qubic/rfsoc/hwconfig.py
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
def get_amp_word(self, amplitude: float) -> int:
    """
    Converts amplitude (normalized to 1) into command word for FPGA

    Parameters
    ----------
    amplitude: float

    Returns
    -------
    int:
        amplitude word

    """
    return int(cg.twos_complement(amplitude*(2**(self.amp_n_bits - 1) - 1), nbits=self.amp_n_bits))

get_cw_env_word(env_ind)

Returns the envelope word for a CW pulse. env_ind is required since the CW pulse requires a single clock cycle of envelope data to be stored.

Parameters:

Name Type Description Default
env_ind int

starting index of the envelope in the envelope buffer

required

Returns:

Name Type Description
int int

env_word

Source code in qubic/rfsoc/hwconfig.py
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
def get_cw_env_word(self, env_ind: int) -> int:
    """
    Returns the envelope word for a CW pulse. `env_ind` is required 
    since the CW pulse requires a single clock cycle of envelope
    data to be stored.

    Parameters
    ----------
    env_ind: int
        starting index of the envelope in the envelope buffer
    Returns
    -------
    int:
        env_word
    """
    if self.samples_per_clk//self.interp_ratio > self.env_max_samples:
        raise Exception('{} exceeds max env \
                        length of {}'.format(env_ind + self.samples_per_clk//self.interp_ratio, self.env_max_samples))
    return env_ind//int(self.samples_per_clk/self.interp_ratio)

get_env_buffer(env)

Converts env to a list of samples to write to the env buffer memory.

Parameters:

Name Type Description Default
env np.ndarray, list, or dict

if np.ndarray or list this is interpreted as a list of samples. Samples should be normalized to 1.

if dict, a function in the qubitconfig.envelope_pulse library is used to calculate the envelope samples. env['env_func'] should be the name of the function, and env['paradict'] is a dictionary of attributes to pass to env_func. The set of attributes varies according to the function but should include the pulse duration twidth

required

Returns:

Type Description
np.ndarray:

buffer of envelope data

Source code in qubic/rfsoc/hwconfig.py
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
def get_env_buffer(self, env: np.ndarray | list | dict):
    """
    Converts env to a list of samples to write to the env buffer memory.

    Parameters
    ----------
    env : np.ndarray, list, or dict
        if np.ndarray or list this is interpreted as a list of samples. Samples
        should be normalized to 1.

        if dict, a function in the qubitconfig.envelope_pulse library is used to
        calculate the envelope samples. env['env_func'] should be the name of the function,
        and env['paradict'] is a dictionary of attributes to pass to env_func. The 
        set of attributes varies according to the function but should include the 
        pulse duration twidth

    Returns
    -------
    np.ndarray:
        buffer of envelope data

    """
    if isinstance(env, np.ndarray) or isinstance(env, list):
        env_samples = np.asarray(env)
    elif isinstance(env, dict):
        dt = self.interp_ratio * self.sample_period
        env_func = getattr(ep, env['env_func'])
        _, env_samples = env_func(dt=dt, **env['paradict'])
        # ipdb.set_trace()
    elif env == 'cw':
        env_samples = np.ones(self.samples_per_clk//self.interp_ratio)
    else:
        raise TypeError(f'env {env} must be dict or array')

    env_samples = np.pad(env_samples, (0, (self.samples_per_clk//self.interp_ratio - len(env_samples)
            % self.samples_per_clk//self.interp_ratio) % self.samples_per_clk//self.interp_ratio))

    return (cg.twos_complement(np.real(env_samples*(2**(self.env_n_bits-1)-1)).astype(int), nbits=self.env_n_bits) << self.env_n_bits) \
                + cg.twos_complement(np.imag(env_samples*(2**(self.env_n_bits-1)-1)).astype(int), nbits=self.env_n_bits)

get_env_word(env_ind, length_nsamples)

Returns the envelope word stored in the pulse command, which encodes the starting address and length of the pulse envelope.

Parameters:

Name Type Description Default
env_ind int

starting index of the envelope in the envelope buffer

required
length_nsamples int

length of the envelope in samples (could be the same as the pulse length in samples, or lower if interpolating)

required

Returns:

Name Type Description
int int

env_word

Source code in qubic/rfsoc/hwconfig.py
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
def get_env_word(self, env_ind: int, length_nsamples: int) -> int:
    """
    Returns the envelope word stored in the pulse command, which encodes the
    starting address and length of the pulse envelope.

    Parameters
    ----------
    env_ind: int
        starting index of the envelope in the envelope buffer
    length_nsamples: int
        length of the envelope in samples (could be the same as the
        pulse length in samples, or lower if interpolating)
    Returns
    -------
    int:
        env_word
    """
    if env_ind + length_nsamples > self.env_max_samples:
        raise Exception('{} exceeds max env length of {}'.format(env_ind + length_nsamples, self.env_max_samples))
    return env_ind//int(self.samples_per_clk/self.interp_ratio) \
            + (int(np.ceil(self.interp_ratio*length_nsamples/self.samples_per_clk)) << 12)

get_freq_buffer(freqs)

Converts a list of frequencies (in Hz) to a buffer, where each frequency has 16 elements:

  • [0] is a 32-bit freq word, encoding phase increment per clock cycle
  • [1:15] are 16 bit I MSB + 16 bit Q LSB, encoding 15 phase offsets for each sample (except 0) within a clock cycle 16-element arrays for each frequency are concatenated into a single 1D numpy array.

Parameters:

Name Type Description Default
freqs list | ndarray

List of frequencies, in Hz

required

Returns:

Type Description
np.ndarray:

Frequency buffer: array of concatenated 16-element frequency lists; shape is (len(freqs)*16,)

Source code in qubic/rfsoc/hwconfig.py
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
def get_freq_buffer(self, freqs: list | np.ndarray) -> np.ndarray:
    """
    Converts a list of frequencies (in Hz) to a buffer, where each frequency 
    has 16 elements:

      - `[0]` is a 32-bit freq word, encoding phase increment per clock cycle
      - `[1:15]` are 16 bit I MSB + 16 bit Q LSB, encoding 15 phase offsets for 
            each sample (except 0) within a clock cycle
    16-element arrays for each frequency are concatenated into a single 1D numpy array.

    Parameters
    ----------
    freqs: list | np.ndarray
        List of frequencies, in Hz

    Returns
    -------
    np.ndarray:
        Frequency buffer: array of concatenated 16-element frequency lists; shape
        is `(len(freqs)*16,)`

    """
    freq_buffer = np.empty(0)
    scale = 2**(self.freq_n_bits/2 - 1) - 1
    for freq in freqs:
        cur_freq_buffer = np.zeros(self.samples_per_clk)
        if freq is not None:
            cur_freq_buffer[0] = int(freq*2**self.freq_n_bits/self.fpga_clk_freq) & (2**self.freq_n_bits - 1)
            for i in range(1, self.samples_per_clk):
                i_mult = int(round(np.cos(2*np.pi*freq*i*self.sample_period)*scale) % (2**(self.freq_n_bits/2)))
                q_mult = int(round(np.sin(2*np.pi*freq*i*self.sample_period)*scale) % (2**(self.freq_n_bits/2)))
                cur_freq_buffer[i] = (i_mult << (self.freq_n_bits//2)) + q_mult

        freq_buffer = np.append(freq_buffer, cur_freq_buffer)

    return freq_buffer

get_phase_word(phase)

Converts phase to 17 bit unsigned int, normalized to 2*pi (i.e. 2*pi -> 2**17)

Parameters:

Name Type Description Default
phase float

input phase (in radians)

required

Returns:

Name Type Description
int int

17-bit phase word

Source code in qubic/rfsoc/hwconfig.py
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
def get_phase_word(self, phase: float) -> int:
    """
    Converts phase to 17 bit unsigned int, normalized to 2*pi
    (i.e. 2\*pi -> 2\*\*17)
    Parameters
    ----------
    phase: float
        input phase (in radians)

    Returns
    -------
    int:
        17-bit phase word
    """
    phase = int(((phase % (2*np.pi))/(2*np.pi)) * 2**17)
    return phase % 2**17

length_nclks(tlength)

Converts pulse length in seconds to integer number of clock cycles.

Parameters:

Name Type Description Default
tlength float

time in seconds

required

Returns:

Name Type Description
int int

time in clocks

Source code in qubic/rfsoc/hwconfig.py
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
def length_nclks(self, tlength: float) -> int:
    """
    Converts pulse length in seconds to integer number of clock cycles.

    Parameters
    ----------
    tlength: float
        time in seconds

    Returns
    -------
    int:
        time in clocks
    """
    return int(np.ceil(tlength/self.fpga_clk_period))