# --------------------------------------------------------------------------
# Description : TruthTable QCoDeS interface
# Git repository : https://gitlab.com/qblox/packages/software/qblox_instruments.git
# Copyright (C) Qblox BV (2025)
# --------------------------------------------------------------------------
from __future__ import annotations
import itertools
import warnings
from dataclasses import dataclass
from typing import Any
# Define valid options for measurement and level.
MEASUREMENT_OPTIONS = ("count", "digital")
LEVEL_OPTIONS = ("low", "mid", "high", "invalid")
[docs]
@dataclass(frozen=True)
class ChannelMeasurement:
"""
Represents a measurement on a specific channel.
Parameters
----------
channel : int
The channel number (must be in 0...7).
measurement : str
The measurement type (must be one of :class:`MEASUREMENT_OPTIONS`).
"""
channel: int
measurement: str
def __post_init__(self) -> None:
if not (0 <= self.channel <= 7):
raise ValueError(f"Invalid channel '{self.channel}'. Channel must be in range 0...7")
if self.measurement not in MEASUREMENT_OPTIONS:
raise ValueError(
f"Invalid measurement type '{self.measurement}'. "
f"Valid options: {MEASUREMENT_OPTIONS}"
)
[docs]
@dataclass(frozen=True)
class Condition:
"""
Represents a condition for a truth table entry, combining a channel measurement with a level.
Parameters
----------
channel_measurement : ChannelMeasurement
The channel measurement.
level : str
The level (must be one of :class:`LEVEL_OPTIONS`).
"""
channel_measurement: ChannelMeasurement
level: str
def __post_init__(self) -> None:
if self.level not in LEVEL_OPTIONS:
raise ValueError(f"Invalid level '{self.level}'. Valid options: {LEVEL_OPTIONS}")
def __str__(self) -> str:
return (
f"channel{self.channel_measurement.channel}_"
f"{self.channel_measurement.measurement}_"
f"{self.level}"
)
[docs]
@dataclass(frozen=True)
class TruthTableEntry:
"""
Represents an entry in the truth table.
Parameters
----------
conditions : list[Condition]
List of conditions that must be met.
trigger_address : int
The trigger address associated with these conditions.
"""
conditions: list[Condition]
trigger_address: int
[docs]
class TruthTable:
"""
Truth table manager class.
Attributes
----------
MAX_INPUTS : int
Maximum number of distinct input channels allowed.
MAX_TRIGGER_ADDRESSES : int
Maximum number of distinct trigger addresses allowed.
"""
MAX_INPUTS: int = 4
MAX_TRIGGER_ADDRESSES: int = 4
[docs]
def __init__(self, default_trigger_address: int = 0, overwrite_conflict: bool = False) -> None:
"""
Initialize the TruthTable.
Parameters
----------
default_trigger_address : int, optional
The default trigger address. Must be in range 0...15.
overwrite_conflict : bool, optional
Whether to overwrite existing LUT entries on conflict.
Raises
------
ValueError
If the default trigger address is not in range 0...15.
"""
if not (0 <= default_trigger_address <= 15):
raise ValueError(
f"Invalid default trigger address '{default_trigger_address}'. "
"Default trigger address must be in range 0...15"
)
self._default_trigger_address: int = default_trigger_address
self._overwrite_conflict: bool = overwrite_conflict
self._truth_table: list[TruthTableEntry] = []
# Stores the user-defined conditions and their corresponding trigger addresses.
self._all_entries: list[TruthTableEntry] = []
# Contains the expanded version of `self._truth_table`.
# It includes all possible input combinations that map to a specific trigger address,
# accounting for implicit conditions where certain inputs can take multiple valid levels.
self._truth_table_inputs: list[ChannelMeasurement] = []
self._trigger_addresses: set[int] = {default_trigger_address}
self._lut: dict[str, Any] = {}
def __str__(self) -> str:
"""
Returns a string representation of the entire `TruthTable`.
Returns
-------
str
For each combination of input levels, displays a line in the format:
IF (channel0_count_low && channel1_digital_high ... ) THEN set trigger address <value>
"""
msg = "Truth Table:\n"
for entry in self._all_entries:
msg += f" IF ({' && '.join(str(cond) for cond in entry.conditions)}) "
msg += f"THEN set trigger address {entry.trigger_address}\n"
msg += f"Default Trigger Address: {self._default_trigger_address}\n"
return msg
@property
def lut(self) -> dict[str, Any]:
"""
Returns the lookup table (LUT).
Returns
-------
dict
A dictionary with keys: `src` and `lut`,
`src` - list of input configurations,
`lut` - list of trigger addresses.
"""
return self._lut
def _encode(self) -> dict[str, Any]:
"""
Encodes the current truth table into a lookup table (LUT).
Returns
-------
dict
A dictionary with keys: `src` and `lut`,
`src` - list of input configurations,
`lut` - list of trigger addresses.
"""
# Create mapping from each ChannelMeasurement to its index.
input_mapping = {cm: idx for idx, cm in enumerate(self._truth_table_inputs)}
num_inputs_used = len(input_mapping)
num_levels = len(LEVEL_OPTIONS)
lut_size = num_levels**num_inputs_used
lut = [self._default_trigger_address] * lut_size
self._all_entries = []
for entry in self._truth_table:
conditions = entry.conditions
trigger_address = entry.trigger_address
# For each input, start with all possible level indices.
input_options = [tuple(range(num_levels)) for _ in range(num_inputs_used)]
for condition in conditions:
cm = condition.channel_measurement
if cm in input_mapping:
ch_index = input_mapping[cm]
try:
level_index = LEVEL_OPTIONS.index(condition.level)
except ValueError as e:
raise ValueError(
f"Invalid level '{condition.level}'. Valid options: {LEVEL_OPTIONS}"
) from e
# Restrict this channel to the specific level.
input_options[ch_index] = (level_index,)
# For every combination of input levels, update the LUT.
for input_values in itertools.product(*input_options):
index = sum(val * (num_levels**i) for i, val in enumerate(input_values))
combined_conditions = [
Condition(channel_measurement=cm, level=LEVEL_OPTIONS[level])
for cm, level in zip(self._truth_table_inputs, input_values)
]
self._all_entries.append(TruthTableEntry(combined_conditions, trigger_address))
if lut[index] != self._default_trigger_address and lut[index] != trigger_address:
conflict_msg = (
f"Conflict while setting LUT entry to trigger address {trigger_address}.\n"
f"Condition ({' && '.join(str(cond) for cond in combined_conditions)}) "
f"is already set to trigger address {lut[index]}."
)
if self._overwrite_conflict:
warnings.warn(
f"{conflict_msg}\nOverwriting with trigger address {trigger_address}."
)
else:
raise ValueError(conflict_msg)
lut[index] = trigger_address
src = [{"ch": cm.channel, "cfg": cm.measurement} for cm in self._truth_table_inputs]
return {"src": src, "lut": lut}
[docs]
def add_conditions(self, conditions: list[Condition], trigger_address: int) -> None:
"""
Adds a set of conditions with an associated trigger address to the truth table.
Parameters
----------
conditions : list[Condition]
List of conditions to add.
trigger_address : int
Trigger address to associate with these conditions. Must be in range 1...15.
Raises
------
ValueError
If trigger_address is not in range 1...15.
If number of distinct inputs exceeds `MAX_INPUTS`.
If adding the trigger_address would exceed `MAX_TRIGGER_ADDRESSES`.
If a conflict is detected in the LUT and overwriting is disabled.
"""
# Validate trigger address range.
if not (1 <= trigger_address <= 15):
raise ValueError(
f"Invalid trigger address '{trigger_address}'. "
"Trigger address must be in range 1...15"
)
for condition in conditions:
if condition.channel_measurement not in self._truth_table_inputs:
self._truth_table_inputs.append(condition.channel_measurement)
if len(self._truth_table_inputs) > self.MAX_INPUTS:
raise ValueError(
f"Only {self.MAX_INPUTS} distinct inputs are allowed. "
f"Current inputs: {self._truth_table_inputs}"
)
self._trigger_addresses.add(trigger_address)
if len(self._trigger_addresses) > self.MAX_TRIGGER_ADDRESSES:
raise ValueError(
f"Trigger address {trigger_address} cannot be added. "
"It exceeds the maximum number of trigger addresses "
f"({self.MAX_TRIGGER_ADDRESSES}). "
f"Current trigger addresses: {self._trigger_addresses}"
)
entry = TruthTableEntry(conditions=conditions, trigger_address=trigger_address)
self._truth_table.append(entry)
self._lut = self._encode()
[docs]
@classmethod
def from_config(
cls, entries: list, default_trigger_address: int = 0, overwrite_conflict: bool = False
) -> TruthTable:
"""
Builds a TruthTable from a list of dictionaries.
Parameters
----------
entries : list of dict
List of configuration dictionary entries.
default_trigger_address : int, optional
Default trigger address.
overwrite_conflict : bool, optional
Whether to overwrite conflicts in the LUT.
Returns
-------
TruthTable
The constructed truth table.
Notes
-----
Each dictionary is expected to have the format:
.. code-block:: python
{
"conditions": [
{"channel": int, "measurement": str, "level": str},
...
],
"trigger": int
}
"""
tt = cls(default_trigger_address, overwrite_conflict)
for entry in entries:
conditions = [
Condition(
channel_measurement=ChannelMeasurement(
channel=condition["channel"], measurement=condition["measurement"]
),
level=condition["level"],
)
for condition in entry["conditions"]
]
tt.add_conditions(conditions, entry["trigger"])
return tt