- Edited
The experiment below linearly maps a float between 0.0
and 1.0
to an unsigned 32-bit integer between 0
and 2**32-1
. The conversion happens on a Kasli SoC, completely without RPC calls.
Beware that you should run this experiment on a real hardware device. Running with artiq/examples/no_hardware/device_db.py
changes the results.
from artiq.experiment import EnvExperiment, kernel, rpc
from artiq.language.types import TInt32, TFloat
import numpy as np
@rpc
def print_binary(number, type_cast, nr_bits):
bits = ""
for i in range(nr_bits):
bits = str(i % 10) + bits
print("bits :", bits)
print("binary :", f"{type_cast(number):{int(nr_bits)}b}")
@rpc
def rpc_python_benchmark(amplitude):
r"""Linearly maps amplitude ∈ [0.0, 1.0] to an unsigned 32-bit integer {0,1,..., 2**32-1}.
Comparison function for `kernel_hack()` with python types."""
if amplitude < 0.0 or amplitude > 1.0:
raise ValueError("Invalid AD9910 fractional amplitude!")
if amplitude > 0.9999999998: # increasing the last digit from 8 to 9 fails
amplitude = 0.9999999998
x = round(amplitude * (1 << 32))
print("decimal :", x)
print_binary(x, np.uint32, 32)
@rpc
def rpc_numpy_benchmark(amplitude: TFloat):
r"""Linearly maps amplitude ∈ [0.0, 1.0] to an unsigned 32-bit integer {0,1,..., 2**32-1}.
Comparison function for `kernel_hack()` with numpy types."""
if amplitude < 0.0:
raise ValueError("Invalid AD9910 fractional amplitude!")
elif amplitude <= 0.4999999998: # increasing the last digit from 8 to 9 fails
x = np.int32(round(amplitude * (1 << 32)))
elif amplitude <= 0.9999999998: # increasing the last digit from 8 to 9 fails
x = np.int32(-round((1-amplitude) * (1 << 32))) # the minus sign is a hack
elif amplitude <= 1.0:
x = np.int32(-round((1-0.9999999998) * (1 << 32)))
else:
raise ValueError("Invalid AD9910 fractional amplitude!")
print("decimal :", x)
print_binary(x, np.uint32, 32)
@kernel
def kernel_hack(amplitude: TFloat) -> TInt32:
r"""Linearly maps amplitude ∈ [0.0, 1.0] to an unsigned 32-bit integer {0,1,..., 2**32-1}.
Hacking is necessary because the ARTIQ compiler does *not* know unsigned integers."""
if amplitude < 0.0:
raise ValueError("Invalid AD9910 fractional amplitude!")
elif amplitude <= 0.4999999998: # increasing the last digit from 8 to 9 fails
# no clue why minus sign is inverted compared to `rpc_numpy_benchmark`
return np.int32(-round(amplitude * 2 * (1 << 31))) # (1 << 32) produces gargabe
elif amplitude <= 0.9999999998: # increasing the last digit from 8 to 9 fails
# no clue why minus sign is inverted compared to `rpc_numpy_benchmark`
return np.int32(round((1-amplitude) * 2 * (1 << 31))) # (1 << 32) produces gargabe
elif amplitude <= 1.0:
return np.int32(round((1-0.9999999998) * 2 * (1 << 31))) # (1 << 32) produces gargabe
else:
raise ValueError("Invalid AD9910 fractional amplitude!")
return np.int32(0) # prevents compiler crash
class UnsignedIntegerTest(EnvExperiment):
"""
Do must run this with *real* hardware in `device_db.py`.
We have tested unsigned integer hacks with
`artiq/examples/no_hardware/device_db.py` before,
and the results were *different* from the real hardware.
"""
def build(self):
self.setattr_device("core")
@kernel
def run(self):
amplitude = 0.5
print("----------- RPC PYTHON TYPES -----------")
rpc_python_benchmark(amplitude)
print("----------- RPC NUMPY TYPES -----------")
rpc_numpy_benchmark(amplitude)
print("----------- KERNEL HACK -----------")
x = kernel_hack(amplitude)
print("decimal :", x)
print_binary(x, np.uint32, 32)
Why did we do all this?
The AD9910's digital ramp generator accepts unsigned 32-bit integers in these registers:
Digital Ramp Limit Register—Address 0x0B
Digital Ramp Step Size Register—Address 0x0C
See pages 28, 29 and 59 of the AD9910 datasheet.
Unfortunately, artiq.coredevice.AD9910.amplitude_to_asf
(see artiq/coredevice/ad9910.py#L747) does not handle and cannot be adapted to handle unsigned integers.