Skip to content

Merge QubitTable changes into main branch of default simulator #20

@rmshaffer

Description

@rmshaffer

Describe the feature you'd like
The QubitTable class in autoqasm.simulator.program_context is almost an exact duplicate of the one in the default simulator repo.

The AutoQASM version of this class is here:

class QubitTable(Table):
def __init__(self):
super().__init__("Qubits")
def _validate_qubit_in_range(self, qubit: int, register_name: str) -> None:
if qubit >= len(self[register_name]):
raise IndexError(
f"qubit register index `{qubit}` out of range for qubit register "
f"of length {len(self[register_name])} `{register_name}`."
)
@singledispatchmethod
def get_by_identifier(self, identifier: Union[Identifier, IndexedIdentifier]) -> tuple[int]:
"""Convenience method to get an element with a possibly indexed identifier.
Args:
identifier (Union[Identifier, IndexedIdentifier]): The identifier to retrieve.
Returns:
tuple[int]: The qubit indices associated with the given identifier.
"""
if identifier.name.startswith("$"):
return (int(identifier.name[1:]),)
return self[identifier.name]
@get_by_identifier.register
def _(self, identifier: IndexedIdentifier) -> tuple[int]: # noqa: C901
"""When identifier is an IndexedIdentifier, function returns a tuple
corresponding to the elements referenced by the indexed identifier.
Args:
identifier (IndexedIdentifier): The indexed identifier to retrieve.
Raises:
IndexError: Qubit register index out of range for specified register.
Returns:
tuple[int]: The qubit indices associated with the given identifier.
"""
name = identifier.name.name
indices = self.get_qubit_indices(identifier)
primary_index = indices[0]
if isinstance(primary_index, (IntegerLiteral, SymbolLiteral)):
if isinstance(primary_index, IntegerLiteral):
self._validate_qubit_in_range(primary_index.value, name)
target = (self[name][0] + primary_index.value,)
elif isinstance(primary_index, RangeDefinition):
target = tuple(np.array(self[name])[convert_range_def_to_slice(primary_index)])
# Discrete set
else:
index_list = convert_discrete_set_to_list(primary_index)
for index in index_list:
if isinstance(index, int):
self._validate_qubit_in_range(index, name)
target = tuple([self[name][0] + index for index in index_list])
if len(indices) == 2:
# used for gate calls on registers, index will be IntegerLiteral
secondary_index = indices[1].value
target = (target[secondary_index],)
# validate indices manually, since we use addition instead of indexing to
# accommodate symbolic indices
for q in target:
if isinstance(q, (int, Integer)) and (relative_index := q - self[name][0]) >= len(
self[name]
):
raise IndexError(
f"qubit register index `{relative_index}` out of range for qubit register "
f"of length {len(self[name])} `{name}`."
)
return target
@staticmethod
def get_qubit_indices(
identifier: IndexedIdentifier,
) -> list[IntegerLiteral | RangeDefinition | DiscreteSet]:
"""Gets the qubit indices from a given indexed identifier.
Args:
identifier (IndexedIdentifier): The identifier representing the
qubit indices.
Raises:
IndexError: Index consists of multiple dimensions.
Returns:
list[IntegerLiteral | RangeDefinition | DiscreteSet]: The qubit indices
corresponding to the given indexed identifier.
"""
primary_index = identifier.indices[0]
if isinstance(primary_index, list):
if len(primary_index) != 1:
raise IndexError("Cannot index multiple dimensions for qubits.")
primary_index = primary_index[0]
if len(identifier.indices) == 1:
return [primary_index]
elif len(identifier.indices) == 2:
# used for gate calls on registers, index will be IntegerLiteral
secondary_index = identifier.indices[1][0]
return [primary_index, secondary_index]
else:
raise IndexError("Cannot index multiple dimensions for qubits.")
def _get_indices_length(
self,
indices: Sequence[IntegerLiteral | SymbolLiteral | RangeDefinition | DiscreteSet],
) -> int:
last_index = indices[-1]
if isinstance(last_index, (IntegerLiteral, SymbolLiteral)):
return 1
elif isinstance(last_index, RangeDefinition):
buffer = np.sign(last_index.step.value) if last_index.step is not None else 1
start = last_index.start.value if last_index.start is not None else 0
stop = last_index.end.value + buffer
step = last_index.step.value if last_index.step is not None else 1
return (stop - start) // step
elif isinstance(last_index, DiscreteSet):
return len(last_index.values)
else:
raise TypeError(f"tuple indices must be integers or slices, not {type(last_index)}")
def get_qubit_size(self, identifier: Union[Identifier, IndexedIdentifier]) -> int:
"""Gets the number of qubit indices for the given identifier.
Args:
identifier (Union[Identifier, IndexedIdentifier]): The identifier representing
the qubit indices.
Returns:
int: The number of qubit indices contained in the given identifier.
"""
if isinstance(identifier, IndexedIdentifier):
indices = self.get_qubit_indices(identifier)
return self._get_indices_length(indices)
return len(self.get_by_identifier(identifier))

The default simulator implementation is here:
https://github.com/amazon-braket/amazon-braket-default-simulator-python/blob/a4d7f98cb123ae6a1092972e728d2dbb93cb27b5/src/braket/default_simulator/openqasm/program_context.py#L96-L150

We should merge the changes into the default simulator repo and delete the QubitTable class from this repo.

How would this feature be used? Please describe.
This will reduce code duplication and improve maintainability.

Describe alternatives you've considered
n/a

Additional context
n/a

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    Status

    Backlog

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions