From c204415682946ef8c3cec16860e6fbaef20349dc Mon Sep 17 00:00:00 2001 From: Viraj Chaudhari Date: Tue, 2 Jun 2026 14:37:02 -0700 Subject: [PATCH 1/2] feat: add outputs field to support mid-circuit measurements --- .../task_result/gate_model_task_result_v1.py | 8 +- .../test_gate_model_task_result_v1.py | 130 ++++++++++++++++++ 2 files changed, 137 insertions(+), 1 deletion(-) diff --git a/src/braket/task_result/gate_model_task_result_v1.py b/src/braket/task_result/gate_model_task_result_v1.py index 501ecd1..ee50468 100644 --- a/src/braket/task_result/gate_model_task_result_v1.py +++ b/src/braket/task_result/gate_model_task_result_v1.py @@ -12,13 +12,18 @@ # language governing permissions and limitations under the License -from pydantic.v1 import BaseModel, Field, confloat, conint, conlist, constr +from typing import TypeAlias + +from pydantic.v1 import BaseModel, Field, StrictBool, confloat, conint, conlist, constr from braket.ir.jaqcd.program_v1 import Results from braket.schema_common import BraketSchemaBase, BraketSchemaHeader from braket.task_result.additional_metadata import AdditionalMetadata from braket.task_result.task_metadata_v1 import TaskMetadata +ScalarValue: TypeAlias = StrictBool | conint(strict=True) | confloat(ge=-float("inf"), strict=True) +OutputValue: TypeAlias = ScalarValue | list[ScalarValue] + class ResultTypeValue(BaseModel): """ @@ -70,5 +75,6 @@ class GateModelTaskResult(BraketSchemaBase): ) resultTypes: list[ResultTypeValue] | None measuredQubits: conlist(conint(ge=0), min_items=1) | None + outputs: conlist(dict[constr(min_length=1), OutputValue], min_items=1) | None taskMetadata: TaskMetadata additionalMetadata: AdditionalMetadata diff --git a/test/unit_tests/braket/task_result/test_gate_model_task_result_v1.py b/test/unit_tests/braket/task_result/test_gate_model_task_result_v1.py index fe7e540..7a569a2 100644 --- a/test/unit_tests/braket/task_result/test_gate_model_task_result_v1.py +++ b/test/unit_tests/braket/task_result/test_gate_model_task_result_v1.py @@ -182,3 +182,133 @@ def test_incorrect_result_type_attribute_type(): @pytest.mark.xfail(raises=ValidationError) def test_incorrect_result_type_attribute_value(): ResultTypeValue(type={"type": "unknown"}, value=1) + + +def test_outputs_only_two_shots_multiple_registers( + task_metadata, + additional_metadata_gate_model, + measured_qubits, +): + outputs = [ + {"c1": 1, "c2": [0, 1], "float_variable": 1.57}, + {"c1": 0, "c2": [0, 0], "float_variable": 1.57}, + ] + result = GateModelTaskResult( + outputs=outputs, + measuredQubits=measured_qubits, + taskMetadata=task_metadata, + additionalMetadata=additional_metadata_gate_model, + ) + assert result.outputs == outputs + assert result.measurements is None + assert result.measurementProbabilities is None + assert GateModelTaskResult.parse_raw(result.json()) == result + + +def test_outputs_loop_same_qubit_measured_repeatedly( + task_metadata, + additional_metadata_gate_model, + measured_qubits, +): + outputs = [{"mcm_abc": [1, 0, 0]}] + result = GateModelTaskResult( + outputs=outputs, + measuredQubits=measured_qubits, + taskMetadata=task_metadata, + additionalMetadata=additional_metadata_gate_model, + ) + assert result.outputs == outputs + + +def test_outputs_negative_integer_value( + task_metadata, + additional_metadata_gate_model, + measured_qubits, +): + outputs = [{"mcm": [-1, 1, 0]}] + result = GateModelTaskResult( + outputs=outputs, + measuredQubits=measured_qubits, + taskMetadata=task_metadata, + additionalMetadata=additional_metadata_gate_model, + ) + assert result.outputs == outputs + assert GateModelTaskResult.parse_raw(result.json()).outputs == outputs + + +def test_outputs_undefined_value_omitted_from_shot_dict( + task_metadata, + additional_metadata_gate_model, + measured_qubits, +): + outputs = [ + {"c1": 1, "c2": [0, 1]}, + {"c1": 0}, + ] + result = GateModelTaskResult( + outputs=outputs, + measuredQubits=measured_qubits, + taskMetadata=task_metadata, + additionalMetadata=additional_metadata_gate_model, + ) + assert result.outputs == outputs + + +def test_outputs_alongside_measurements( + task_metadata, + additional_metadata_gate_model, + measured_qubits, + measurements, +): + outputs = [{"c": [m_q0, m_q1]} for m_q0, m_q1 in measurements] + result = GateModelTaskResult( + measurements=measurements, + outputs=outputs, + measuredQubits=measured_qubits, + taskMetadata=task_metadata, + additionalMetadata=additional_metadata_gate_model, + ) + assert result.measurements == measurements + assert result.outputs == outputs + + +@pytest.mark.parametrize( + "outputs", + [ + [], # must contain at least one shot + [{"c1": "not-a-scalar"}], # strings not allowed + [{"c1": [1, "0"]}], # strings inside a list not allowed + [{"c1": [[0, 1]]}], # nested lists not allowed + [{"c1": {"nested": "dict"}}], # dict values not allowed + [{"c1": None}], # None not allowed (omit key instead) + [{"": 1}], # empty variable name not allowed + ], +) +@pytest.mark.xfail(raises=ValidationError) +def test_incorrect_outputs( + outputs, + measured_qubits, + task_metadata, + additional_metadata_gate_model, +): + GateModelTaskResult( + outputs=outputs, + measuredQubits=measured_qubits, + taskMetadata=task_metadata, + additionalMetadata=additional_metadata_gate_model, + ) + + +def test_outputs_default_is_none( + task_metadata, + additional_metadata_gate_model, + measured_qubits, + measurements, +): + result = GateModelTaskResult( + measurements=measurements, + measuredQubits=measured_qubits, + taskMetadata=task_metadata, + additionalMetadata=additional_metadata_gate_model, + ) + assert result.outputs is None From 2e3bd2d56e1b9acae504892dff287b235d4db856 Mon Sep 17 00:00:00 2001 From: Viraj Chaudhari Date: Fri, 5 Jun 2026 18:12:09 +0000 Subject: [PATCH 2/2] enforce homogenous output list, null for undefined and other feedback --- .../task_result/gate_model_task_result_v1.py | 37 +++++++++++++++++-- .../test_gate_model_task_result_v1.py | 23 ++++++++++-- 2 files changed, 54 insertions(+), 6 deletions(-) diff --git a/src/braket/task_result/gate_model_task_result_v1.py b/src/braket/task_result/gate_model_task_result_v1.py index ee50468..b682b9e 100644 --- a/src/braket/task_result/gate_model_task_result_v1.py +++ b/src/braket/task_result/gate_model_task_result_v1.py @@ -14,15 +14,22 @@ from typing import TypeAlias -from pydantic.v1 import BaseModel, Field, StrictBool, confloat, conint, conlist, constr +from pydantic.v1 import BaseModel, Field, StrictBool, confloat, conint, conlist, constr, validator from braket.ir.jaqcd.program_v1 import Results from braket.schema_common import BraketSchemaBase, BraketSchemaHeader from braket.task_result.additional_metadata import AdditionalMetadata from braket.task_result.task_metadata_v1 import TaskMetadata -ScalarValue: TypeAlias = StrictBool | conint(strict=True) | confloat(ge=-float("inf"), strict=True) -OutputValue: TypeAlias = ScalarValue | list[ScalarValue] +StrictInt = conint(strict=True) +StrictFloat = confloat(allow_inf_nan=False, strict=True) + +ScalarValue: TypeAlias = StrictBool | StrictInt | StrictFloat +ScalarOrNull: TypeAlias = ScalarValue | None + +OutputValue: TypeAlias = ( + ScalarOrNull | list[StrictBool | None] | list[StrictInt | None] | list[StrictFloat | None] +) class ResultTypeValue(BaseModel): @@ -56,6 +63,11 @@ class GateModelTaskResult(BraketSchemaBase): Indicates which qubits are in `measurements`. Default is `None`. resultTypes (list[ResultTypeValue]): Requested result types and their values. Default is `None`. + outputs (list[dict[str, OutputValue]]): Per-shot values of OpenQASM 3 + ``output``-declared variables. Each element is one shot, mapping each + output variable name to its value (a scalar, or a homogeneous list for + registers). An undefined value is represented by ``None``. Default is + `None`. taskMetadata (TaskMetadata): The task metadata additionalMetadata (AdditionalMetadata): Additional metadata of the task """ @@ -78,3 +90,22 @@ class GateModelTaskResult(BraketSchemaBase): outputs: conlist(dict[constr(min_length=1), OutputValue], min_items=1) | None taskMetadata: TaskMetadata additionalMetadata: AdditionalMetadata + + @validator("outputs", each_item=True) + def validate_non_empty_shot(cls, shot): + """ + Rejects empty per-shot dicts. Every declared output variable appears in + each shot (its value or None), so an empty shot signals malformed data. + + Args: + shot (dict): One per-shot output dict from the outputs list. + + Returns: + dict: The same shot, validated to be non-empty. + + Raises: + ValueError: If the shot dict is empty. + """ + if not shot: + raise ValueError("each output shot must contain at least one variable") + return shot diff --git a/test/unit_tests/braket/task_result/test_gate_model_task_result_v1.py b/test/unit_tests/braket/task_result/test_gate_model_task_result_v1.py index 7a569a2..51333df 100644 --- a/test/unit_tests/braket/task_result/test_gate_model_task_result_v1.py +++ b/test/unit_tests/braket/task_result/test_gate_model_task_result_v1.py @@ -236,14 +236,14 @@ def test_outputs_negative_integer_value( assert GateModelTaskResult.parse_raw(result.json()).outputs == outputs -def test_outputs_undefined_value_omitted_from_shot_dict( +def test_outputs_undefined_value_represented_as_none( task_metadata, additional_metadata_gate_model, measured_qubits, ): outputs = [ {"c1": 1, "c2": [0, 1]}, - {"c1": 0}, + {"c1": 0, "c2": [None, 1]}, ] result = GateModelTaskResult( outputs=outputs, @@ -254,6 +254,22 @@ def test_outputs_undefined_value_omitted_from_shot_dict( assert result.outputs == outputs +def test_outputs_bool_value( + task_metadata, + additional_metadata_gate_model, + measured_qubits, +): + outputs = [{"flag": True}, {"flag": False}] + result = GateModelTaskResult( + outputs=outputs, + measuredQubits=measured_qubits, + taskMetadata=task_metadata, + additionalMetadata=additional_metadata_gate_model, + ) + assert result.outputs == outputs + assert GateModelTaskResult.parse_raw(result.json()).outputs == outputs + + def test_outputs_alongside_measurements( task_metadata, additional_metadata_gate_model, @@ -276,11 +292,12 @@ def test_outputs_alongside_measurements( "outputs", [ [], # must contain at least one shot + [{}], # shot dict must not be empty [{"c1": "not-a-scalar"}], # strings not allowed [{"c1": [1, "0"]}], # strings inside a list not allowed + [{"c1": [1, 1.5]}], # mixed types within a list not allowed [{"c1": [[0, 1]]}], # nested lists not allowed [{"c1": {"nested": "dict"}}], # dict values not allowed - [{"c1": None}], # None not allowed (omit key instead) [{"": 1}], # empty variable name not allowed ], )