Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 19 additions & 1 deletion azure-quantum/azure/quantum/cirq/targets/generic.py
Original file line number Diff line number Diff line change
Expand Up @@ -215,8 +215,24 @@ def _to_cirq_job(self, azure_job, program: "cirq.Circuit" = None):
target=self,
)

# Some backends emit verbose strings for individual qubit outcomes instead of
# single-character values. Map the known ones explicitly so they don't corrupt
# the per-qubit register split.
_SHOT_STRING_MAP: Dict[str, str] = {
"Zero": "0",
"False": "0",
"One": "1",
"True": "1",
"Loss": "-",
}

@staticmethod
def _qir_display_to_bitstring(obj: Any) -> str:
if isinstance(obj, str):
mapped = AzureGenericQirCirqTarget._SHOT_STRING_MAP.get(obj)
if mapped is not None:
return mapped

if isinstance(obj, str) and not re.match(r"[\d\s]+$", obj):
try:
obj = ast.literal_eval(obj)
Expand All @@ -228,7 +244,9 @@ def _qir_display_to_bitstring(obj: Any) -> str:
AzureGenericQirCirqTarget._qir_display_to_bitstring(t) for t in obj
)
if isinstance(obj, list):
return "".join(str(bit) for bit in obj)
return "".join(
AzureGenericQirCirqTarget._qir_display_to_bitstring(bit) for bit in obj
)
return str(obj)

@staticmethod
Expand Down
78 changes: 78 additions & 0 deletions azure-quantum/tests/test_cirq.py
Original file line number Diff line number Diff line change
Expand Up @@ -233,6 +233,84 @@ def test_cirq_generic_to_cirq_result_drops_non_binary_shots_and_exposes_raw():
)


def test_cirq_generic_to_cirq_result_non_binary_shots_filtered_and_raw_preserved():
"""Non-binary shots are excluded from measurements[] but kept in raw_shots.

Known backend-specific outcome strings ("Loss", "True", "False", "One", "Zero")
are mapped to their single-character equivalents before register splitting.
Single-char non-binary markers like "." pass through unchanged.
"""
np = pytest.importorskip("numpy")
cirq = pytest.importorskip("cirq")

from azure.quantum.cirq.targets.generic import AzureGenericQirCirqTarget

measurement_dict = {"m": [0]}
# "Loss" -> mapped to "-"
# "." -> preserved as "."
# "1"/"0" -> binary, appear in measurements
shots = ["Loss", ".", "1", "0"]

result = AzureGenericQirCirqTarget._to_cirq_result(
result=shots,
param_resolver=cirq.ParamResolver({}),
measurement_dict=measurement_dict,
)

# Only binary outcomes in measurements.
np.testing.assert_array_equal(
result.measurements["m"],
np.asarray([[1], [0]], dtype=np.int8),
)

# Original shot objects kept verbatim in raw_shots.
assert result.raw_shots == shots

# raw_measurements: "Loss" -> "-", "." -> ".", binary values unchanged.
raw_meas = result.raw_measurements()
np.testing.assert_array_equal(
raw_meas["m"],
np.asarray([["-"], ["."], ["1"], ["0"]], dtype="<U1"),
)


def test_cirq_generic_qir_display_to_bitstring_unexpected_scalar_becomes_loss_marker():
"""Only the literal string 'Loss' gets special-cased to the loss marker.
All other strings follow normal procedure: digit strings bypass
ast.literal_eval; non-digit strings are evaled or returned verbatim.
"""
pytest.importorskip("cirq")

from azure.quantum.cirq.targets.generic import AzureGenericQirCirqTarget

fn = AzureGenericQirCirqTarget._qir_display_to_bitstring

# Digit strings bypass ast.literal_eval entirely.
assert fn("0") == "0"
assert fn("1") == "1"
assert fn("011") == "011"

# Non-digit strings that fail literal_eval are returned verbatim.
assert fn(".") == "."
assert fn("-") == "-"
assert fn("1.1") == "1.1" # evaluated to float 1.1, str() -> "1.1"
assert fn(".1.0.1") == ".1.0.1" # fails eval, returned as-is
assert fn("-10") == "-10" # evals to int -10, str() -> "-10"
assert fn("1-0-1") == "1-0-1" # fails eval, returned as-is

# Known multi-character per-qubit outcome strings are mapped explicitly.
assert fn("Zero") == "0"
assert fn("False") == "0"
assert fn("One") == "1"
assert fn("True") == "1"
assert fn("Loss") == "-"

# List elements are processed recursively, so special-case strings inside
# a list are also mapped correctly.
assert fn(["Zero", "Loss", "Loss"]) == "0--"
assert fn(["One", "Zero", "One"]) == "101"


def test_cirq_service_targets_excludes_non_qir_target():
"""Targets without a target_profile (e.g. Pasqal) must not be wrapped as
AzureGenericQirCirqTarget — they use pulse-level input formats incompatible
Expand Down
Loading