"""A library to support generating qblox q1asm programs."""
import numpy as np
END_OF_LINE = "\n"
[docs]class Program:
"""A class to represent a sequencer q1asm assembly program.
A q1asm program is made of blocks of code (:class:`qibolab.instruments.qblox.qblox_q1asm.Block`).
Real time registers (variables) are necessary to implement sweeps in real time.
This class keeps track of the registers used and has a method to provide the next available register.
"""
MAX_REGISTERS = 64
[docs] def next_register(self):
"""Provides the number of the next available register."""
self._next_register_number += 1
if self._next_register_number >= Program.MAX_REGISTERS:
raise RuntimeError("There are no more registers available.")
return self._next_register_number
def __init__(self):
self._blocks: list = []
self._next_register_number: int = -1
[docs] def add_blocks(self, *blocks):
"""Adds a :class:`qibolab.instruments.qblox.qblox_q1asm.Block` of code
to the list of blocks."""
for block in blocks:
self._blocks.append(block)
def __repr__(self) -> str:
"""Returns the program."""
block_str: str = ""
for block in self._blocks:
block_str += repr(block) + END_OF_LINE
return block_str
[docs]class Block:
"""A class to represent a block of q1asm assembly code.
A block is comprised of code lines.
Attributes:
name (str): A name for the block of code.
lines (list(tupple)): A list of lines of code (code, comment, indentation level).
"""
# TODO: replace tupples used for code lines with a Line object
GLOBAL_INDENTATION_LEVEL = 3
SPACES_PER_LEVEL = 4
SPACES_BEFORE_COMMENT = 4
def __init__(self, name=""):
self.name = name
self.lines: list = []
self._indentation = 0
def _indentation_string(self, level):
return " " * Block.SPACES_PER_LEVEL * level
@property
def indentation(self):
return self._indentation
@indentation.setter
def indentation(self, value):
if not isinstance(value, int):
raise TypeError(
f"indentation type should be int, got {type(value).__name__}"
)
diff = value - self._indentation
self._indentation = value
self.lines = [
(line, comment, level + diff) for (line, comment, level) in self.lines
]
[docs] def append(self, line, comment="", level=0):
self.lines = self.lines + [(line, comment, self._indentation + level)]
[docs] def prepend(self, line, comment="", level=0):
self.lines = [(line, comment, self._indentation + level)] + self.lines
[docs] def append_spacer(self):
self.lines = self.lines + [("", "", self._indentation)]
def __repr__(self) -> str:
"""Returns a string with the block of code, taking care of the
indentation of instructions and comments."""
def comment_col(line, level):
col = Block.SPACES_PER_LEVEL * (level + Block.GLOBAL_INDENTATION_LEVEL)
col += len(line)
return col
max_col: int = 0
for line, comment, level in self.lines:
if comment:
max_col = max(max_col, comment_col(line, level))
block_str: str = ""
if self.name:
block_str += (
self._indentation_string(
self._indentation + Block.GLOBAL_INDENTATION_LEVEL
)
+ "# "
+ self.name
+ END_OF_LINE
)
for line, comment, level in self.lines:
block_str += (
self._indentation_string(level + Block.GLOBAL_INDENTATION_LEVEL) + line
)
if comment:
block_str += (
" "
* (max_col - comment_col(line, level) + Block.SPACES_BEFORE_COMMENT)
+ " # "
+ comment
)
block_str += END_OF_LINE
return block_str
def __add__(self, other):
if isinstance(other, Block):
block = Block()
for line, comment, level in self.lines:
block.append(line, comment, level)
for line, comment, level in other.lines:
block.append(line, comment, level)
else:
raise TypeError(f"Expected Block, got {type(other).__name__}")
return block
def __radd__(self, other):
if isinstance(other, Block):
block = Block()
for line, comment, level in other.lines:
block.append(line, comment, level)
for line, comment, level in self.lines:
block.append(line, comment, level)
else:
raise TypeError(f"Expected Block, got {type(other).__name__}")
return block
def __iadd__(self, other):
if isinstance(other, Block):
for line, comment, level in other.lines:
self.append(line, comment, level)
else:
raise TypeError(f"Expected Block, got {type(other).__name__}")
return self
[docs]class Register:
"""A class to represent a q1asm program register.
Registers are used as variables in real time FPGA code.
Attributes:
name (str): a name to identify the register
"""
def __init__(self, program: Program, name: str = ""):
self._number = program.next_register()
self._name = name
self._type = type
def __repr__(self) -> str:
return "R" + str(self._number)
@property
def name(self):
return self._name
@name.setter
def name(self, value):
self._name = value
[docs]def wait_block(
wait_time: int, register: Register, force_multiples_of_four: bool = False
):
"""Generates blocks of code to implement long delays.
Arguments:
wait_time (int): the total time to wait.
register (:class:`qibolab.instruments.qblox.qblox_q1asm.Register`): the register used to loop
force_multiples_of_four (bool): a flag that forces the delay to be a multiple of 4(ns)
"""
n_loops: int
loop_wait: int
wait: int
block = Block()
# constrains
# extra_wait and wait_loop_step need to be within (4,65535) (2**16 bits variable)
# extra_wait and wait_loop_step need to be multiples of 4ns *
if wait_time < 0:
raise ValueError("wait_time must be positive.")
elif wait_time == 0:
n_loops = 0
loop_wait = 0
wait = 0
elif wait_time > 0 and wait_time < 4:
# TODO: log("wait_time > 0 and wait_time < 4 is not supported by the instrument, wait_time changed to 4ns")
n_loops = 0
loop_wait = 0
wait = 4
elif wait_time >= 4 and wait_time < 2**16: # 65536ns
n_loops = 0
loop_wait = 0
wait = wait_time
elif wait_time >= 2**16 and wait_time < 2**32: # 4.29s
loop_wait = 2**16 - 4
n_loops = wait_time // loop_wait
wait = wait_time % loop_wait
while loop_wait >= 4 and (wait > 0 and wait < 4):
loop_wait -= 4
n_loops = wait_time // loop_wait
wait = wait_time % loop_wait
if loop_wait < 4 or (wait > 0 and wait < 4):
raise ValueError(
f"Unable to decompose {wait_time} into valid (n_loops * loop_wait + wait)"
)
else:
raise ValueError("wait_time > 65535**2 is not supported yet.")
if force_multiples_of_four:
wait = int(np.ceil(wait / 4)) * 4
wait_time = n_loops * loop_wait + wait
if n_loops > 0:
block.append(f"# wait {wait_time} ns")
block.append(f"move {n_loops}, {register}")
block.append(f"wait_loop_{register}:")
block.append(f"wait {loop_wait}", level=1)
block.append(f"loop {register}, @wait_loop_{register}", level=1)
if wait > 0:
block.append(f"wait {wait}")
return block
[docs]def loop_block(
start: int, stop: int, step: int, register: Register, block: Block
): # validate values
"""Generates blocks of code to implement loops.
Its behaviour is similar to range(): it includes the first value, but never the last.
Arguments:
start (int): the initial value.
stop (int): the final value (excluded).
step (int): the step in which the register is incremented.
register (:class:`qibolab.instruments.qblox.qblox_q1asm.Register`): the register modified within the loop.
block (:class:`qibolab.instruments.qblox.qblox_q1asm.Register`): the block of code to be iterated.
"""
if not (isinstance(start, int) and isinstance(stop, int) and isinstance(step, int)):
raise ValueError("begin, end and step must be int")
if stop != start and step == 0:
raise ValueError("step must not be 0")
if (stop > start and not step > 0) or (stop < start and not step < 0):
raise ValueError("invalid step")
if start < 0 or stop < 0:
raise ValueError("sweep possitive values only")
header_block = Block()
# same behaviour as range() includes the first but never the last
header_block.append(f"move {start}, {register}", comment=register.name + " loop")
header_block.append("nop")
header_block.append(f"loop_{register}:")
header_block.append_spacer()
body_block = Block()
body_block.indentation = 1
body_block += block
footer_block = Block()
footer_block.append_spacer()
if stop > start:
footer_block.append(f"add {register}, {step}, {register}")
footer_block.append("nop")
footer_block.append(
f"jlt {register}, {stop}, @loop_{register}", comment=register.name + " loop"
)
elif stop < start:
footer_block.append(f"sub {register}, {abs(step)}, {register}")
footer_block.append("nop")
footer_block.append(
f"jge {register}, {stop + 1}, @loop_{register}",
comment=register.name + " loop",
)
return header_block + body_block + footer_block
[docs]def convert_phase(phase_rad: float):
"""Converts phase values in radiants to the encoding used in qblox FPGAs.
The phase is divided into 1e9 steps between 0° and 360°, expressed
as an integer between 0 and 1e9 (e.g 45°=125e6).
https://qblox-qblox-instruments.readthedocs-hosted.com/en/master/api_reference/sequencer.html
"""
phase_deg = (phase_rad * 360 / (2 * np.pi)) % 360
return int(phase_deg * 1e9 / 360)
[docs]def convert_frequency(freq: float):
"""Converts frequency values to the encoding used in qblox FPGAs.
The frequency is divided into 4e9 steps between -500 and 500 MHz and
expressed as an integer between -2e9 and 2e9. (e.g. 1 MHz=4e6).
https://qblox-qblox-instruments.readthedocs-hosted.com/en/master/api_reference/sequencer.html
"""
if not (freq >= -500e6 and freq <= 500e6):
raise ValueError("frequency must be a float between -500e6 and 500e6 Hz")
return int(freq * 4) % 2**32 # two's complement of 18? TODO: confirm with qblox
[docs]def convert_gain(gain: float):
"""Converts gain values to the encoding used in qblox FPGAs.
Both gain values are divided in 2**sample path width steps. QCM DACs
resolution 16bits, QRM DACs and ADCs 12 bit
https://qblox-qblox-instruments.readthedocs-hosted.com/en/master/api_reference/sequencer.html
"""
if not (gain >= -1 and gain <= 1):
raise ValueError("gain must be a float between -1 and 1")
if gain == 1:
return 2**15 - 1
else:
return int(np.floor(gain * 2**15)) % 2**32 # two's complement 32 bit number
[docs]def convert_offset(offset: float):
"""Converts offset values to the encoding used in qblox FPGAs.
Both offset values are divided in 2**sample path width steps. QCM
DACs resolution 16bits, QRM DACs and ADCs 12 bit QCM 5Vpp, QRM 2Vpp
https://qblox-qblox-instruments.readthedocs-hosted.com/en/master/api_reference/sequencer.html
"""
scale_factor = 1.25 * np.sqrt(2)
normalised_offset = offset / scale_factor
if not (normalised_offset >= -1 and normalised_offset <= 1):
raise ValueError(
f"offset must be a float between {-scale_factor:.3f} and {scale_factor:.3f} V"
)
if normalised_offset == 1:
return 2**15 - 1
else:
return (
int(np.floor(normalised_offset * 2**15)) % 2**32
) # two's complement 32 bit number? or 12 or 24?