From 59f71d26bec64d4f89d0f70a757b0c272f97a809 Mon Sep 17 00:00:00 2001 From: Jean-Christophe Jaskula Date: Fri, 8 Sep 2023 11:29:18 -0400 Subject: [PATCH 01/26] add duration type to FreeParameterExpression --- setup.py | 2 +- src/braket/pulse/ast/approximation_parser.py | 2 +- src/braket/pulse/ast/free_parameters.py | 41 +++++++------- src/braket/pulse/pulse_sequence.py | 23 ++++---- src/braket/pulse/waveforms.py | 3 +- .../braket/circuits/test_circuit.py | 34 ++++++------ .../braket/circuits/test_gate_calibration.py | 6 +-- test/unit_tests/braket/circuits/test_gates.py | 2 +- .../braket/pulse/test_pulse_sequence.py | 54 ++++++++----------- .../unit_tests/braket/pulse/test_waveforms.py | 33 +++++------- 10 files changed, 90 insertions(+), 110 deletions(-) diff --git a/setup.py b/setup.py index 25fca8252..cb4bb778e 100644 --- a/setup.py +++ b/setup.py @@ -29,7 +29,7 @@ install_requires=[ "amazon-braket-schemas>=1.19.1", "amazon-braket-default-simulator>=1.19.1", - "oqpy~=0.2.1", + "oqpy~=0.3.1", "setuptools", "backoff", "boltons", diff --git a/src/braket/pulse/ast/approximation_parser.py b/src/braket/pulse/ast/approximation_parser.py index 69e9fafa5..d8096bf83 100644 --- a/src/braket/pulse/ast/approximation_parser.py +++ b/src/braket/pulse/ast/approximation_parser.py @@ -56,7 +56,7 @@ def __init__(self, program: Program, frames: Dict[str, Frame]): self.amplitudes = defaultdict(TimeSeries) self.frequencies = defaultdict(TimeSeries) self.phases = defaultdict(TimeSeries) - context = _ParseState(variables=dict(), frame_data=_init_frame_data(frames)) + context = _ParseState(variables={"pi": np.pi}, frame_data=_init_frame_data(frames)) self._qubit_frames_mapping: Dict[str, List[str]] = _init_qubit_frame_mapping(frames) self.visit(program.to_ast(include_externs=False), context) diff --git a/src/braket/pulse/ast/free_parameters.py b/src/braket/pulse/ast/free_parameters.py index 6cfc36d03..6bcd719f6 100644 --- a/src/braket/pulse/ast/free_parameters.py +++ b/src/braket/pulse/ast/free_parameters.py @@ -13,8 +13,9 @@ from typing import Dict, Union from openpulse import ast -from openqasm3.ast import DurationLiteral from openqasm3.visitor import QASMTransformer +from oqpy.program import Program +from oqpy.timing import OQDurationLiteral from braket.parametric.free_parameter_expression import FreeParameterExpression @@ -22,48 +23,46 @@ class _FreeParameterExpressionIdentifier(ast.Identifier): """Dummy AST node with FreeParameterExpression instance attached""" - def __init__(self, expression: FreeParameterExpression): + def __init__( + self, expression: FreeParameterExpression, type: ast.ClassicalType = ast.FloatType() + ): super().__init__(name=f"FreeParameterExpression({expression})") self._expression = expression + self.type = type @property def expression(self) -> FreeParameterExpression: return self._expression + def to_ast(self) -> ast.Identifier: + return self + class _FreeParameterTransformer(QASMTransformer): """Walk the AST and evaluate FreeParameterExpressions.""" - def __init__(self, param_values: Dict[str, float]): + def __init__(self, param_values: Dict[str, float], program: Program): self.param_values = param_values + self.program = program super().__init__() def visit__FreeParameterExpressionIdentifier( - self, identifier: ast.Identifier + self, identifier: _FreeParameterExpressionIdentifier ) -> Union[_FreeParameterExpressionIdentifier, ast.FloatLiteral]: """Visit a FreeParameterExpressionIdentifier. Args: - identifier (Identifier): The identifier. + identifier (_FreeParameterExpressionIdentifier): The identifier. Returns: Union[_FreeParameterExpressionIdentifier, FloatLiteral]: The transformed expression. """ new_value = identifier.expression.subs(self.param_values) if isinstance(new_value, FreeParameterExpression): - return _FreeParameterExpressionIdentifier(new_value) + return _FreeParameterExpressionIdentifier(new_value, identifier.type) else: - return ast.FloatLiteral(new_value) - - def visit_DurationLiteral(self, duration_literal: DurationLiteral) -> DurationLiteral: - """Visit Duration Literal. - node.value, node.unit (node.unit.name, node.unit.value) - 1 - Args: - duration_literal (DurationLiteral): The duration literal. - Returns: - DurationLiteral: The transformed duration literal. - """ - duration = duration_literal.value - if not isinstance(duration, FreeParameterExpression): - return duration_literal - return DurationLiteral(duration.subs(self.param_values), duration_literal.unit) + if isinstance(identifier.type, ast.FloatType): + return ast.FloatLiteral(new_value) + elif isinstance(identifier.type, ast.DurationType): + return OQDurationLiteral(new_value).to_ast(self.program) + else: + raise NotImplementedError(f"{identifier.type} is not a supported type.") diff --git a/src/braket/pulse/pulse_sequence.py b/src/braket/pulse/pulse_sequence.py index 3606bbd11..08d792681 100644 --- a/src/braket/pulse/pulse_sequence.py +++ b/src/braket/pulse/pulse_sequence.py @@ -46,7 +46,7 @@ class PulseSequence: def __init__(self): self._capture_v0_count = 0 - self._program = Program() + self._program = Program(simplify_constants=False) self._frames = {} self._waveforms = {} self._free_parameters = set() @@ -183,10 +183,7 @@ def delay( Returns: PulseSequence: self, with the instruction added. """ - if isinstance(duration, FreeParameterExpression): - for p in duration.expression.free_symbols: - self._free_parameters.add(FreeParameter(p.name)) - duration = OQDurationLiteral(duration) + duration = self._format_parameter_ast(duration, type_=ast.DurationType()) if not isinstance(qubits_or_frames, QubitSet): if not isinstance(qubits_or_frames, list): qubits_or_frames = [qubits_or_frames] @@ -276,9 +273,9 @@ def make_bound_pulse_sequence(self, param_values: Dict[str, float]) -> PulseSequ """ program = deepcopy(self._program) tree: ast.Program = program.to_ast(include_externs=False, ignore_needs_declaration=True) - new_tree: ast.Program = _FreeParameterTransformer(param_values).visit(tree) + new_tree: ast.Program = _FreeParameterTransformer(param_values, program).visit(tree) - new_program = Program() + new_program = Program(simplify_constants=False) new_program.declared_vars = program.declared_vars new_program.undeclared_vars = program.undeclared_vars for x in new_tree.statements: @@ -325,13 +322,19 @@ def to_ir(self) -> str: return ast_to_qasm(tree) def _format_parameter_ast( - self, parameter: Union[float, FreeParameterExpression] + self, + parameter: Union[float, FreeParameterExpression], + type_: ast.ClassicalType = ast.FloatType(), ) -> Union[float, _FreeParameterExpressionIdentifier]: if isinstance(parameter, FreeParameterExpression): for p in parameter.expression.free_symbols: self._free_parameters.add(FreeParameter(p.name)) - return _FreeParameterExpressionIdentifier(parameter) - return parameter + return _FreeParameterExpressionIdentifier(parameter, type_) + else: + if isinstance(type_, ast.FloatType): + return parameter + elif isinstance(type_, ast.DurationType): + return OQDurationLiteral(parameter) def _parse_arg_from_calibration_schema( self, argument: Dict, waveforms: Dict[Waveform], frames: Dict[Frame] diff --git a/src/braket/pulse/waveforms.py b/src/braket/pulse/waveforms.py index f298dd3e5..48da8904b 100644 --- a/src/braket/pulse/waveforms.py +++ b/src/braket/pulse/waveforms.py @@ -21,7 +21,6 @@ import numpy as np from oqpy import WaveformVar, bool_, complex128, declare_waveform_generator, duration, float64 from oqpy.base import OQPyExpression -from oqpy.timing import OQDurationLiteral from braket.parametric.free_parameter import FreeParameter from braket.parametric.free_parameter_expression import ( @@ -457,7 +456,7 @@ def _map_to_oqpy_type( ) -> Union[_FreeParameterExpressionIdentifier, OQPyExpression]: if isinstance(parameter, FreeParameterExpression): return ( - OQDurationLiteral(parameter) + _FreeParameterExpressionIdentifier(parameter, duration) if is_duration_type else _FreeParameterExpressionIdentifier(parameter) ) diff --git a/test/unit_tests/braket/circuits/test_circuit.py b/test/unit_tests/braket/circuits/test_circuit.py index 21f972b89..c49b83bfc 100644 --- a/test/unit_tests/braket/circuits/test_circuit.py +++ b/test/unit_tests/braket/circuits/test_circuit.py @@ -740,7 +740,7 @@ def test_ir_non_empty_instructions_result_types_basis_rotation_instructions(): "qubit[2] __qubits__;", "cal {", " waveform drag_gauss_wf = drag_gaussian" - + "(3000000.0ns, 400000000.0ns, 0.2, 1, false);", + + "(3.0ms, 400.0ms, 0.2, 1, false);", "}", "defcal z $0, $1 {", " set_frequency(predefined_frame_1, 6000000.0);", @@ -769,7 +769,7 @@ def test_ir_non_empty_instructions_result_types_basis_rotation_instructions(): "bit[2] __bits__;", "cal {", " waveform drag_gauss_wf = drag_gaussian" - + "(3000000.0ns, 400000000.0ns, 0.2, 1, false);", + + "(3.0ms, 400.0ms, 0.2, 1, false);", "}", "defcal z $0, $1 {", " set_frequency(predefined_frame_1, 6000000.0);", @@ -800,7 +800,7 @@ def test_ir_non_empty_instructions_result_types_basis_rotation_instructions(): "OPENQASM 3.0;", "cal {", " waveform drag_gauss_wf = drag_gaussian" - + "(3000000.0ns, 400000000.0ns, 0.2, 1, false);", + + "(3.0ms, 400.0ms, 0.2, 1, false);", "}", "defcal z $0, $1 {", " set_frequency(predefined_frame_1, 6000000.0);", @@ -835,7 +835,7 @@ def test_ir_non_empty_instructions_result_types_basis_rotation_instructions(): "qubit[5] __qubits__;", "cal {", " waveform drag_gauss_wf = drag_gaussian" - + "(3000000.0ns, 400000000.0ns, 0.2, 1, false);", + + "(3.0ms, 400.0ms, 0.2, 1, false);", "}", "defcal z $0, $1 {", " set_frequency(predefined_frame_1, 6000000.0);", @@ -866,7 +866,7 @@ def test_ir_non_empty_instructions_result_types_basis_rotation_instructions(): "qubit[2] __qubits__;", "cal {", " waveform drag_gauss_wf = drag_gaussian" - + "(3000000.0ns, 400000000.0ns, 0.2, 1, false);", + + "(3.0ms, 400.0ms, 0.2, 1, false);", "}", "defcal z $0, $1 {", " set_frequency(predefined_frame_1, 6000000.0);", @@ -899,7 +899,7 @@ def test_ir_non_empty_instructions_result_types_basis_rotation_instructions(): "qubit[5] __qubits__;", "cal {", " waveform drag_gauss_wf = drag_gaussian" - + "(3000000.0ns, 400000000.0ns, 0.2, 1, false);", + + "(3.0ms, 400.0ms, 0.2, 1, false);", "}", "defcal z $0, $1 {", " set_frequency(predefined_frame_1, 6000000.0);", @@ -933,7 +933,7 @@ def test_ir_non_empty_instructions_result_types_basis_rotation_instructions(): "qubit[7] __qubits__;", "cal {", " waveform drag_gauss_wf = drag_gaussian" - + "(3000000.0ns, 400000000.0ns, 0.2, 1, false);", + + "(3.0ms, 400.0ms, 0.2, 1, false);", "}", "defcal z $0, $1 {", " set_frequency(predefined_frame_1, 6000000.0);", @@ -965,7 +965,7 @@ def test_ir_non_empty_instructions_result_types_basis_rotation_instructions(): "qubit[2] __qubits__;", "cal {", " waveform drag_gauss_wf = drag_gaussian" - + "(3000000.0ns, 400000000.0ns, 0.2, 1, false);", + + "(3.0ms, 400.0ms, 0.2, 1, false);", "}", "defcal z $0, $1 {", " set_frequency(predefined_frame_1, 6000000.0);", @@ -1033,8 +1033,7 @@ def test_parametric_circuit_with_fixed_argument_defcal(pulse_sequence): "bit[1] __bits__;", "qubit[1] __qubits__;", "cal {", - " waveform drag_gauss_wf = drag_gaussian" - + "(3000000.0ns, 400000000.0ns, 0.2, 1, false);", + " waveform drag_gauss_wf = drag_gaussian" + "(3.0ms, 400.0ms, 0.2, 1, false);", "}", "defcal z $0, $1 {", " set_frequency(predefined_frame_1, 6000000.0);", @@ -1131,8 +1130,7 @@ def foo( "bit[1] __bits__;", "qubit[1] __qubits__;", "cal {", - " waveform drag_gauss_wf = drag_gaussian" - + "(3000000.0ns, 400000000.0ns, 0.2, 1, false);", + " waveform drag_gauss_wf = drag_gaussian" + "(3.0ms, 400.0ms, 0.2, 1, false);", "}", "defcal foo(-0.2) $0 {", " shift_phase(predefined_frame_1, -0.1);", @@ -3360,11 +3358,9 @@ def test_pulse_circuit_to_openqasm(predefined_frame_1, user_defined_frame): "bit[2] __bits__;", "cal {", " frame user_defined_frame_0 = newframe(device_port_x0, 10000000.0, 3.14);", - " waveform gauss_wf = gaussian(1000000.0ns, 700000000.0ns, 1, false);", - " waveform drag_gauss_wf = drag_gaussian(3000000.0ns, 400000000.0ns, 0.2, 1," - " false);", - " waveform drag_gauss_wf_2 = drag_gaussian(3000000.0ns, 400000000.0ns, " - "0.2, 1, false);", + " waveform gauss_wf = gaussian(1.0ms, 700.0ms, 1, false);", + " waveform drag_gauss_wf = drag_gaussian(3.0ms, 400.0ms, 0.2, 1," " false);", + " waveform drag_gauss_wf_2 = drag_gaussian(3.0ms, 400.0ms, " "0.2, 1, false);", "}", "h $0;", "cal {", @@ -3477,7 +3473,7 @@ def test_parametrized_pulse_circuit(user_defined_frame): "bit[2] __bits__;", "cal {", " frame user_defined_frame_0 = newframe(device_port_x0, 10000000.0, 3.14);", - " waveform gauss_wf = gaussian(10000.0ns, 700000000.0ns, 1, false);", + " waveform gauss_wf = gaussian(10.0us, 700.0ms, 1, false);", "}", "rx(0.5) $0;", "cal {", @@ -3502,7 +3498,7 @@ def test_parametrized_pulse_circuit(user_defined_frame): "bit[2] __bits__;", "cal {", " frame user_defined_frame_0 = newframe(device_port_x0, 10000000.0, 3.14);", - " waveform gauss_wf = gaussian(10000.0ns, 700000000.0ns, 1, false);", + " waveform gauss_wf = gaussian(10.0us, 700.0ms, 1, false);", "}", "rx(0.5) $0;", "cal {", diff --git a/test/unit_tests/braket/circuits/test_gate_calibration.py b/test/unit_tests/braket/circuits/test_gate_calibration.py index 31c2384db..4037f3faf 100644 --- a/test/unit_tests/braket/circuits/test_gate_calibration.py +++ b/test/unit_tests/braket/circuits/test_gate_calibration.py @@ -80,7 +80,7 @@ def test_to_ir(pulse_sequence): "OPENQASM 3.0;", "defcal rx(1.0) $0, $1 {", " barrier test_frame_rf;", - " delay[1000000000000.0ns] test_frame_rf;", + " delay[1000s] test_frame_rf;", "}", ] ) @@ -100,7 +100,7 @@ def test_to_ir_with_bad_key(pulse_sequence): "OPENQASM 3.0;", "defcal z $0, $1 {", " barrier test_frame_rf;", - " delay[1000000000000.0ns] test_frame_rf;", + " delay[1000s] test_frame_rf;", "}", ] ) @@ -118,7 +118,7 @@ def test_to_ir_with_key(pulse_sequence): "OPENQASM 3.0;", "defcal z $0, $1 {", " barrier test_frame_rf;", - " delay[1000000000000.0ns] test_frame_rf;", + " delay[1000s] test_frame_rf;", "}", ] ) diff --git a/test/unit_tests/braket/circuits/test_gates.py b/test/unit_tests/braket/circuits/test_gates.py index 9d5dd5580..51e73b83e 100644 --- a/test/unit_tests/braket/circuits/test_gates.py +++ b/test/unit_tests/braket/circuits/test_gates.py @@ -962,7 +962,7 @@ def to_ir(pulse_gate): [ "cal {", " set_frequency(user_frame, b + 3);", - " delay[(1000000000.0*c)ns] user_frame;", + " delay[c] user_frame;", "}", ] ) diff --git a/test/unit_tests/braket/pulse/test_pulse_sequence.py b/test/unit_tests/braket/pulse/test_pulse_sequence.py index 57ba20fbd..411074727 100644 --- a/test/unit_tests/braket/pulse/test_pulse_sequence.py +++ b/test/unit_tests/braket/pulse/test_pulse_sequence.py @@ -125,11 +125,9 @@ def test_pulse_sequence_make_bound_pulse_sequence(predefined_frame_1, predefined [ "OPENQASM 3.0;", "cal {", - " waveform gauss_wf = gaussian((1000000000.0*length_g)ns, (1000000000.0*sigma_g)ns, " - "1, false);", - " waveform drag_gauss_wf = " - "drag_gaussian((1000000000.0*length_dg)ns, (1000000000.0*sigma_dg)ns, 0.2, 1, false);", - " waveform constant_wf = constant((1000000000.0*length_c)ns, 2.0 + 0.3im);", + " waveform gauss_wf = gaussian(length_g, sigma_g, 1, false);", + " waveform drag_gauss_wf = drag_gaussian(length_dg, sigma_dg, 0.2, 1, false);", + " waveform constant_wf = constant(length_c, 2.0 + 0.3im);", " waveform arb_wf = {1.0 + 0.4im, 0, 0.3, 0.1 + 0.2im};", " bit[2] psb;", " set_frequency(predefined_frame_1, a + 2*b);", @@ -138,12 +136,9 @@ def test_pulse_sequence_make_bound_pulse_sequence(predefined_frame_1, predefined " shift_phase(predefined_frame_1, a + 2*b);", " set_scale(predefined_frame_1, a + 2*b);", " psb[0] = capture_v0(predefined_frame_1);", - ( - " delay[(1000000000.0*a + 2000000000.0*b)ns]" - " predefined_frame_1, predefined_frame_2;" - ), - " delay[(1000000000.0*a + 2000000000.0*b)ns] predefined_frame_1;", - " delay[1000000.0ns] predefined_frame_1;", + " delay[a + 2*b] predefined_frame_1, predefined_frame_2;", + " delay[a + 2*b] predefined_frame_1;", + " delay[1.0ms] predefined_frame_1;", " barrier predefined_frame_1, predefined_frame_2;", " play(predefined_frame_1, gauss_wf);", " play(predefined_frame_2, drag_gauss_wf);", @@ -173,10 +168,9 @@ def test_pulse_sequence_make_bound_pulse_sequence(predefined_frame_1, predefined [ "OPENQASM 3.0;", "cal {", - " waveform gauss_wf = gaussian(1000000.0ns, (1000000000.0*sigma_g)ns, 1, false);", - " waveform drag_gauss_wf = drag_gaussian(3000000.0ns, 400000000.0ns, 0.2, 1," - " false);", - " waveform constant_wf = constant(4000000.0ns, 2.0 + 0.3im);", + " waveform gauss_wf = gaussian(1.0ms, sigma_g, 1, false);", + " waveform drag_gauss_wf = drag_gaussian(3.0ms, 400.0ms, 0.2, 1, false);", + " waveform constant_wf = constant(4.0ms, 2.0 + 0.3im);", " waveform arb_wf = {1.0 + 0.4im, 0, 0.3, 0.1 + 0.2im};", " bit[2] psb;", " set_frequency(predefined_frame_1, a + 4);", @@ -185,9 +179,9 @@ def test_pulse_sequence_make_bound_pulse_sequence(predefined_frame_1, predefined " shift_phase(predefined_frame_1, a + 4);", " set_scale(predefined_frame_1, a + 4);", " psb[0] = capture_v0(predefined_frame_1);", - " delay[(1000000000.0*a + 4000000000.0)ns] predefined_frame_1, predefined_frame_2;", - " delay[(1000000000.0*a + 4000000000.0)ns] predefined_frame_1;", - " delay[1000000.0ns] predefined_frame_1;", + " delay[a + 4] predefined_frame_1, predefined_frame_2;", + " delay[a + 4] predefined_frame_1;", + " delay[1.0ms] predefined_frame_1;", " barrier predefined_frame_1, predefined_frame_2;", " play(predefined_frame_1, gauss_wf);", " play(predefined_frame_2, drag_gauss_wf);", @@ -206,10 +200,9 @@ def test_pulse_sequence_make_bound_pulse_sequence(predefined_frame_1, predefined [ "OPENQASM 3.0;", "cal {", - " waveform gauss_wf = gaussian(1000000.0ns, 700000000.0ns, 1, false);", - " waveform drag_gauss_wf = drag_gaussian(3000000.0ns, 400000000.0ns, 0.2, 1," - " false);", - " waveform constant_wf = constant(4000000.0ns, 2.0 + 0.3im);", + " waveform gauss_wf = gaussian(1.0ms, 700.0ms, 1, false);", + " waveform drag_gauss_wf = drag_gaussian(3.0ms, 400.0ms, 0.2, 1, false);", + " waveform constant_wf = constant(4.0ms, 2.0 + 0.3im);", " waveform arb_wf = {1.0 + 0.4im, 0, 0.3, 0.1 + 0.2im};", " bit[2] psb;", " set_frequency(predefined_frame_1, 5);", @@ -218,9 +211,9 @@ def test_pulse_sequence_make_bound_pulse_sequence(predefined_frame_1, predefined " shift_phase(predefined_frame_1, 5);", " set_scale(predefined_frame_1, 5);", " psb[0] = capture_v0(predefined_frame_1);", - " delay[5000000000.00000ns] predefined_frame_1, predefined_frame_2;", - " delay[5000000000.00000ns] predefined_frame_1;", - " delay[1000000.0ns] predefined_frame_1;", + " delay[5s] predefined_frame_1, predefined_frame_2;", + " delay[5s] predefined_frame_1;", + " delay[1.0ms] predefined_frame_1;", " barrier predefined_frame_1, predefined_frame_2;", " play(predefined_frame_1, gauss_wf);", " play(predefined_frame_2, drag_gauss_wf);", @@ -311,10 +304,9 @@ def test_pulse_sequence_to_ir(predefined_frame_1, predefined_frame_2): [ "OPENQASM 3.0;", "cal {", - " waveform gauss_wf = gaussian(1000000.0ns, 700000000.0ns, 1, false);", - " waveform drag_gauss_wf = drag_gaussian(3000000.0ns, 400000000.0ns, 0.2, 1," - " false);", - " waveform constant_wf = constant(4000000.0ns, 2.0 + 0.3im);", + " waveform gauss_wf = gaussian(1.0ms, 700.0ms, 1, false);", + " waveform drag_gauss_wf = drag_gaussian(3.0ms, 400.0ms, 0.2, 1, false);", + " waveform constant_wf = constant(4.0ms, 2.0 + 0.3im);", " waveform arb_wf = {1.0 + 0.4im, 0, 0.3, 0.1 + 0.2im};", " bit[2] psb;", " set_frequency(predefined_frame_1, 3000000000.0);", @@ -324,8 +316,8 @@ def test_pulse_sequence_to_ir(predefined_frame_1, predefined_frame_2): " set_scale(predefined_frame_1, 0.25);", " psb[0] = capture_v0(predefined_frame_1);", " delay[2.0ns] predefined_frame_1, predefined_frame_2;", - " delay[1000.0ns] predefined_frame_1;", - " delay[1000000.0ns] $0;", + " delay[1.0us] predefined_frame_1;", + " delay[1.0ms] $0;", " barrier $0, $1;", " barrier predefined_frame_1, predefined_frame_2;", " play(predefined_frame_1, gauss_wf);", diff --git a/test/unit_tests/braket/pulse/test_waveforms.py b/test/unit_tests/braket/pulse/test_waveforms.py index b42eacc0b..86f8253c1 100644 --- a/test/unit_tests/braket/pulse/test_waveforms.py +++ b/test/unit_tests/braket/pulse/test_waveforms.py @@ -74,7 +74,7 @@ def test_constant_waveform(): assert wf.iq == iq assert wf.id == id - _assert_wf_qasm(wf, "waveform const_wf_x = constant(4000000.0ns, 4);") + _assert_wf_qasm(wf, "waveform const_wf_x = constant(4.0ms, 4);") def test_constant_waveform_default_params(): @@ -101,14 +101,13 @@ def test_constant_wf_free_params(): assert wf.parameters == [FreeParameter("length_v") + FreeParameter("length_w")] _assert_wf_qasm( wf, - "waveform const_wf = " - "constant((1000000000.0*length_v + 1000000000.0*length_w)ns, 2.0 - 3.0im);", + "waveform const_wf = " "constant(length_v + length_w, 2.0 - 3.0im);", ) wf_2 = wf.bind_values(length_v=2e-6, length_w=4e-6) assert len(wf_2.parameters) == 1 assert math.isclose(wf_2.parameters[0], 6e-6) - _assert_wf_qasm(wf_2, "waveform const_wf = constant(6000.0ns, 2.0 - 3.0im);") + _assert_wf_qasm(wf_2, "waveform const_wf = constant(6.0us, 2.0 - 3.0im);") def test_drag_gaussian_waveform(): @@ -126,9 +125,7 @@ def test_drag_gaussian_waveform(): assert wf.sigma == sigma assert wf.length == length - _assert_wf_qasm( - wf, "waveform drag_gauss_wf = drag_gaussian(4.0ns, 300000000.0ns, 0.6, 0.4, false);" - ) + _assert_wf_qasm(wf, "waveform drag_gauss_wf = drag_gaussian(4.0ns, 300.0ms, 0.6, 0.4, false);") def test_drag_gaussian_waveform_default_params(): @@ -167,7 +164,7 @@ def test_gaussian_waveform(): assert wf.sigma == sigma assert wf.length == length - _assert_wf_qasm(wf, "waveform gauss_wf = gaussian(4.0ns, 300000000.0ns, 0.4, false);") + _assert_wf_qasm(wf, "waveform gauss_wf = gaussian(4.0ns, 300.0ms, 0.4, false);") def test_drag_gaussian_wf_free_params(): @@ -187,8 +184,8 @@ def test_drag_gaussian_wf_free_params(): _assert_wf_qasm( wf, "waveform d_gauss_wf = " - "drag_gaussian((1000000000.0*length_v)ns, (1000000000.0*sigma_a + " - "1000000000.0*sigma_b)ns, beta_y, amp_z, false);", + "drag_gaussian(length_v, sigma_a + " + "sigma_b, beta_y, amp_z, false);", ) wf_2 = wf.bind_values(length_v=0.6, sigma_a=0.4) @@ -200,15 +197,12 @@ def test_drag_gaussian_wf_free_params(): ] _assert_wf_qasm( wf_2, - "waveform d_gauss_wf = drag_gaussian(600000000.0ns, (1000000000.0*sigma_b " - "+ 400000000.0)ns, beta_y, amp_z, false);", + "waveform d_gauss_wf = drag_gaussian(600.0ms, sigma_b + 0.4, beta_y, amp_z, false);", ) wf_3 = wf.bind_values(length_v=0.6, sigma_a=0.3, sigma_b=0.1, beta_y=0.2, amp_z=0.1) assert wf_3.parameters == [0.6, 0.4, 0.2, 0.1] - _assert_wf_qasm( - wf_3, "waveform d_gauss_wf = drag_gaussian(600000000.0ns, 400000000.0ns, 0.2, 0.1, false);" - ) + _assert_wf_qasm(wf_3, "waveform d_gauss_wf = drag_gaussian(600.0ms, 400.0ms, 0.2, 0.1, false);") def test_gaussian_waveform_default_params(): @@ -243,19 +237,16 @@ def test_gaussian_wf_free_params(): ] _assert_wf_qasm( wf, - "waveform gauss_wf = gaussian((1000000000.0*length_v)ns, (1000000000.0*sigma_x)ns, " - "amp_z, false);", + "waveform gauss_wf = gaussian(length_v, sigma_x, " "amp_z, false);", ) wf_2 = wf.bind_values(length_v=0.6, sigma_x=0.4) assert wf_2.parameters == [0.6, 0.4, FreeParameter("amp_z")] - _assert_wf_qasm( - wf_2, "waveform gauss_wf = gaussian(600000000.0ns, 400000000.0ns, amp_z, false);" - ) + _assert_wf_qasm(wf_2, "waveform gauss_wf = gaussian(600.0ms, 400.0ms, amp_z, false);") wf_3 = wf.bind_values(length_v=0.6, sigma_x=0.3, amp_z=0.1) assert wf_3.parameters == [0.6, 0.3, 0.1] - _assert_wf_qasm(wf_3, "waveform gauss_wf = gaussian(600000000.0ns, 300000000.0ns, 0.1, false);") + _assert_wf_qasm(wf_3, "waveform gauss_wf = gaussian(600.0ms, 300.0ms, 0.1, false);") def _assert_wf_qasm(waveform, expected_qasm): From ed3adcec2991eae8a49a9d1a633cbc0ece91180b Mon Sep 17 00:00:00 2001 From: Jean-Christophe Jaskula Date: Fri, 8 Sep 2023 18:45:17 -0400 Subject: [PATCH 02/26] consider FreeParameter as float --- src/braket/pulse/ast/free_parameters.py | 44 +++++++++++++------ src/braket/pulse/ast/qasm_parser.py | 5 ++- src/braket/pulse/pulse_sequence.py | 5 +-- test/unit_tests/braket/circuits/test_gates.py | 2 +- .../braket/pulse/test_pulse_sequence.py | 16 +++---- .../unit_tests/braket/pulse/test_waveforms.py | 10 ++--- 6 files changed, 50 insertions(+), 32 deletions(-) diff --git a/src/braket/pulse/ast/free_parameters.py b/src/braket/pulse/ast/free_parameters.py index 6bcd719f6..16acb5134 100644 --- a/src/braket/pulse/ast/free_parameters.py +++ b/src/braket/pulse/ast/free_parameters.py @@ -20,21 +20,26 @@ from braket.parametric.free_parameter_expression import FreeParameterExpression -class _FreeParameterExpressionIdentifier(ast.Identifier): +class _FreeParameterExpressionIdentifier(ast.QASMNode): """Dummy AST node with FreeParameterExpression instance attached""" def __init__( - self, expression: FreeParameterExpression, type: ast.ClassicalType = ast.FloatType() + self, expression: FreeParameterExpression, type_: ast.ClassicalType = ast.FloatType() ): - super().__init__(name=f"FreeParameterExpression({expression})") + self.name = f"FreeParameterExpression({expression})" self._expression = expression - self.type = type + self.type_ = type_ @property def expression(self) -> FreeParameterExpression: return self._expression - def to_ast(self) -> ast.Identifier: + def __repr__(self) -> str: + return f"_FreeParameterExpressionIdentifier(name={self.name})" + + def to_ast(self, program: Program) -> ast.Expression: + if isinstance(self.type_, ast.DurationType): + return ast.DurationLiteral(self, ast.TimeUnit.s) return self @@ -58,11 +63,24 @@ def visit__FreeParameterExpressionIdentifier( """ new_value = identifier.expression.subs(self.param_values) if isinstance(new_value, FreeParameterExpression): - return _FreeParameterExpressionIdentifier(new_value, identifier.type) - else: - if isinstance(identifier.type, ast.FloatType): - return ast.FloatLiteral(new_value) - elif isinstance(identifier.type, ast.DurationType): - return OQDurationLiteral(new_value).to_ast(self.program) - else: - raise NotImplementedError(f"{identifier.type} is not a supported type.") + return _FreeParameterExpressionIdentifier(new_value, identifier.type_) + return ast.FloatLiteral(new_value) + + def visit_DurationLiteral(self, duration_literal: ast.DurationLiteral) -> ast.DurationLiteral: + """Visit Duration Literal. + node.value, node.unit (node.unit.name, node.unit.value) + 1 + Args: + duration_literal (DurationLiteral): The duration literal. + Returns: + DurationLiteral: The transformed duration literal. + """ + duration = duration_literal.value + if not isinstance(duration, _FreeParameterExpressionIdentifier): + return duration_literal + new_duration = duration.expression.subs(self.param_values) + if isinstance(new_duration, FreeParameterExpression): + return _FreeParameterExpressionIdentifier(new_duration, duration.type_).to_ast( + self.program + ) + return OQDurationLiteral(new_duration).to_ast(self.program) diff --git a/src/braket/pulse/ast/qasm_parser.py b/src/braket/pulse/ast/qasm_parser.py index 8e6f94ded..9f3d82f36 100644 --- a/src/braket/pulse/ast/qasm_parser.py +++ b/src/braket/pulse/ast/qasm_parser.py @@ -18,6 +18,7 @@ from openqasm3.printer import PrinterState from braket.parametric.free_parameter_expression import FreeParameterExpression +from braket.pulse.ast.free_parameters import _FreeParameterExpressionIdentifier class _PulsePrinter(Printer): @@ -45,8 +46,8 @@ def visit_DurationLiteral(self, node: DurationLiteral, context: PrinterState) -> context (PrinterState): The printer state context. """ duration = node.value - if isinstance(duration, FreeParameterExpression): - self.stream.write(f"({duration.expression}){node.unit.name}") + if isinstance(duration, _FreeParameterExpressionIdentifier): + self.stream.write(f"({duration.expression}) * 1{node.unit.name}") else: super().visit_DurationLiteral(node, context) diff --git a/src/braket/pulse/pulse_sequence.py b/src/braket/pulse/pulse_sequence.py index 08d792681..b797756d4 100644 --- a/src/braket/pulse/pulse_sequence.py +++ b/src/braket/pulse/pulse_sequence.py @@ -331,10 +331,9 @@ def _format_parameter_ast( self._free_parameters.add(FreeParameter(p.name)) return _FreeParameterExpressionIdentifier(parameter, type_) else: - if isinstance(type_, ast.FloatType): - return parameter - elif isinstance(type_, ast.DurationType): + if isinstance(type_, ast.DurationType): return OQDurationLiteral(parameter) + return parameter def _parse_arg_from_calibration_schema( self, argument: Dict, waveforms: Dict[Waveform], frames: Dict[Frame] diff --git a/test/unit_tests/braket/circuits/test_gates.py b/test/unit_tests/braket/circuits/test_gates.py index 51e73b83e..61f890f84 100644 --- a/test/unit_tests/braket/circuits/test_gates.py +++ b/test/unit_tests/braket/circuits/test_gates.py @@ -962,7 +962,7 @@ def to_ir(pulse_gate): [ "cal {", " set_frequency(user_frame, b + 3);", - " delay[c] user_frame;", + " delay[(c) * 1s] user_frame;", "}", ] ) diff --git a/test/unit_tests/braket/pulse/test_pulse_sequence.py b/test/unit_tests/braket/pulse/test_pulse_sequence.py index 411074727..0f6128174 100644 --- a/test/unit_tests/braket/pulse/test_pulse_sequence.py +++ b/test/unit_tests/braket/pulse/test_pulse_sequence.py @@ -125,9 +125,9 @@ def test_pulse_sequence_make_bound_pulse_sequence(predefined_frame_1, predefined [ "OPENQASM 3.0;", "cal {", - " waveform gauss_wf = gaussian(length_g, sigma_g, 1, false);", - " waveform drag_gauss_wf = drag_gaussian(length_dg, sigma_dg, 0.2, 1, false);", - " waveform constant_wf = constant(length_c, 2.0 + 0.3im);", + " waveform gauss_wf = gaussian((length_g) * 1s, (sigma_g) * 1s, 1, false);", + " waveform drag_gauss_wf = drag_gaussian((length_dg) * 1s, (sigma_dg) * 1s, 0.2, 1, false);", + " waveform constant_wf = constant((length_c) * 1s, 2.0 + 0.3im);", " waveform arb_wf = {1.0 + 0.4im, 0, 0.3, 0.1 + 0.2im};", " bit[2] psb;", " set_frequency(predefined_frame_1, a + 2*b);", @@ -136,8 +136,8 @@ def test_pulse_sequence_make_bound_pulse_sequence(predefined_frame_1, predefined " shift_phase(predefined_frame_1, a + 2*b);", " set_scale(predefined_frame_1, a + 2*b);", " psb[0] = capture_v0(predefined_frame_1);", - " delay[a + 2*b] predefined_frame_1, predefined_frame_2;", - " delay[a + 2*b] predefined_frame_1;", + " delay[(a + 2*b) * 1s] predefined_frame_1, predefined_frame_2;", + " delay[(a + 2*b) * 1s] predefined_frame_1;", " delay[1.0ms] predefined_frame_1;", " barrier predefined_frame_1, predefined_frame_2;", " play(predefined_frame_1, gauss_wf);", @@ -168,7 +168,7 @@ def test_pulse_sequence_make_bound_pulse_sequence(predefined_frame_1, predefined [ "OPENQASM 3.0;", "cal {", - " waveform gauss_wf = gaussian(1.0ms, sigma_g, 1, false);", + " waveform gauss_wf = gaussian(1.0ms, (sigma_g) * 1s, 1, false);", " waveform drag_gauss_wf = drag_gaussian(3.0ms, 400.0ms, 0.2, 1, false);", " waveform constant_wf = constant(4.0ms, 2.0 + 0.3im);", " waveform arb_wf = {1.0 + 0.4im, 0, 0.3, 0.1 + 0.2im};", @@ -179,8 +179,8 @@ def test_pulse_sequence_make_bound_pulse_sequence(predefined_frame_1, predefined " shift_phase(predefined_frame_1, a + 4);", " set_scale(predefined_frame_1, a + 4);", " psb[0] = capture_v0(predefined_frame_1);", - " delay[a + 4] predefined_frame_1, predefined_frame_2;", - " delay[a + 4] predefined_frame_1;", + " delay[(a + 4) * 1s] predefined_frame_1, predefined_frame_2;", + " delay[(a + 4) * 1s] predefined_frame_1;", " delay[1.0ms] predefined_frame_1;", " barrier predefined_frame_1, predefined_frame_2;", " play(predefined_frame_1, gauss_wf);", diff --git a/test/unit_tests/braket/pulse/test_waveforms.py b/test/unit_tests/braket/pulse/test_waveforms.py index 86f8253c1..5e8e78111 100644 --- a/test/unit_tests/braket/pulse/test_waveforms.py +++ b/test/unit_tests/braket/pulse/test_waveforms.py @@ -101,7 +101,7 @@ def test_constant_wf_free_params(): assert wf.parameters == [FreeParameter("length_v") + FreeParameter("length_w")] _assert_wf_qasm( wf, - "waveform const_wf = " "constant(length_v + length_w, 2.0 - 3.0im);", + "waveform const_wf = " "constant((length_v + length_w) * 1s, 2.0 - 3.0im);", ) wf_2 = wf.bind_values(length_v=2e-6, length_w=4e-6) @@ -184,8 +184,8 @@ def test_drag_gaussian_wf_free_params(): _assert_wf_qasm( wf, "waveform d_gauss_wf = " - "drag_gaussian(length_v, sigma_a + " - "sigma_b, beta_y, amp_z, false);", + "drag_gaussian((length_v) * 1s, (sigma_a + " + "sigma_b) * 1s, beta_y, amp_z, false);", ) wf_2 = wf.bind_values(length_v=0.6, sigma_a=0.4) @@ -197,7 +197,7 @@ def test_drag_gaussian_wf_free_params(): ] _assert_wf_qasm( wf_2, - "waveform d_gauss_wf = drag_gaussian(600.0ms, sigma_b + 0.4, beta_y, amp_z, false);", + "waveform d_gauss_wf = drag_gaussian(600.0ms, (sigma_b + 0.4) * 1s, beta_y, amp_z, false);", ) wf_3 = wf.bind_values(length_v=0.6, sigma_a=0.3, sigma_b=0.1, beta_y=0.2, amp_z=0.1) @@ -237,7 +237,7 @@ def test_gaussian_wf_free_params(): ] _assert_wf_qasm( wf, - "waveform gauss_wf = gaussian(length_v, sigma_x, " "amp_z, false);", + "waveform gauss_wf = gaussian((length_v) * 1s, (sigma_x) * 1s, " "amp_z, false);", ) wf_2 = wf.bind_values(length_v=0.6, sigma_x=0.4) From 9c4293cdb3e779414c18ea67ae89648faab9a7f6 Mon Sep 17 00:00:00 2001 From: Jean-Christophe Jaskula Date: Fri, 8 Sep 2023 20:15:06 -0400 Subject: [PATCH 03/26] move to_ast to FreeParameterExprsesion --- .../parametric/free_parameter_expression.py | 51 +++++++++++++++++-- src/braket/pulse/ast/free_parameters.py | 38 ++++---------- src/braket/pulse/ast/qasm_parser.py | 1 - src/braket/pulse/pulse_sequence.py | 4 +- src/braket/pulse/waveforms.py | 11 ++-- .../braket/pulse/test_pulse_sequence.py | 3 +- 6 files changed, 66 insertions(+), 42 deletions(-) diff --git a/src/braket/parametric/free_parameter_expression.py b/src/braket/parametric/free_parameter_expression.py index 1b64e2ddb..1da2469e4 100644 --- a/src/braket/parametric/free_parameter_expression.py +++ b/src/braket/parametric/free_parameter_expression.py @@ -15,8 +15,17 @@ import ast from numbers import Number -from typing import Any, Dict, Union - +from typing import Any, Dict, Optional, Union + +from openpulse.ast import ( + ClassicalType, + DurationLiteral, + DurationType, + FloatType, + QASMNode, + TimeUnit, +) +from oqpy import Program from sympy import Expr, Float, Symbol, sympify @@ -30,7 +39,11 @@ class FreeParameterExpression: present will NOT run. Values must be substituted prior to execution. """ - def __init__(self, expression: Union[FreeParameterExpression, Number, Expr, str]): + def __init__( + self, + expression: Union[FreeParameterExpression, Number, Expr, str], + _type: Optional[ClassicalType] = None, + ): """ Initializes a FreeParameterExpression. Best practice is to initialize using FreeParameters and Numbers. Not meant to be initialized directly. @@ -39,6 +52,7 @@ def __init__(self, expression: Union[FreeParameterExpression, Number, Expr, str] Args: expression (Union[FreeParameterExpression, Number, Expr, str]): The expression to use. + _type (Optional[ClassicalType]): type of the expression Examples: >>> expression_1 = FreeParameter("theta") * FreeParameter("alpha") @@ -51,8 +65,11 @@ def __init__(self, expression: Union[FreeParameterExpression, Number, Expr, str] ast.Pow: self.__pow__, ast.USub: self.__neg__, } + self._type = _type if _type is not None else FloatType() if isinstance(expression, FreeParameterExpression): self._expression = expression.expression + if _type is None: + self._type = expression._type elif isinstance(expression, (Number, Expr)): self._expression = expression elif isinstance(expression, str): @@ -170,6 +187,34 @@ def __repr__(self) -> str: """ return repr(self.expression) + def to_ast(self, program: Program) -> QASMNode: + """Creates an AST node for the :class:'FreeParameterExpression'. + + Args: + program (Program): Unused. + + Returns: + QASMNode: The AST node. + """ + if isinstance(self._type, DurationType): + return DurationLiteral(_FreeParameterExpressionIdentifier(self), TimeUnit.s) + return _FreeParameterExpressionIdentifier(self) + + +class _FreeParameterExpressionIdentifier(QASMNode): + """Dummy AST node with FreeParameterExpression instance attached""" + + def __init__(self, expression: FreeParameterExpression): + self.name = f"FreeParameterExpression({expression})" + self._expression = expression + + @property + def expression(self) -> FreeParameterExpression: + return self._expression + + def __repr__(self) -> str: + return f"_FreeParameterExpressionIdentifier(name={self.name})" + def subs_if_free_parameter(parameter: Any, **kwargs) -> Any: """Substitute a free parameter with the given kwargs, if any. diff --git a/src/braket/pulse/ast/free_parameters.py b/src/braket/pulse/ast/free_parameters.py index 16acb5134..56bedc755 100644 --- a/src/braket/pulse/ast/free_parameters.py +++ b/src/braket/pulse/ast/free_parameters.py @@ -17,30 +17,10 @@ from oqpy.program import Program from oqpy.timing import OQDurationLiteral -from braket.parametric.free_parameter_expression import FreeParameterExpression - - -class _FreeParameterExpressionIdentifier(ast.QASMNode): - """Dummy AST node with FreeParameterExpression instance attached""" - - def __init__( - self, expression: FreeParameterExpression, type_: ast.ClassicalType = ast.FloatType() - ): - self.name = f"FreeParameterExpression({expression})" - self._expression = expression - self.type_ = type_ - - @property - def expression(self) -> FreeParameterExpression: - return self._expression - - def __repr__(self) -> str: - return f"_FreeParameterExpressionIdentifier(name={self.name})" - - def to_ast(self, program: Program) -> ast.Expression: - if isinstance(self.type_, ast.DurationType): - return ast.DurationLiteral(self, ast.TimeUnit.s) - return self +from braket.parametric.free_parameter_expression import ( + FreeParameterExpression, + _FreeParameterExpressionIdentifier, +) class _FreeParameterTransformer(QASMTransformer): @@ -63,8 +43,9 @@ def visit__FreeParameterExpressionIdentifier( """ new_value = identifier.expression.subs(self.param_values) if isinstance(new_value, FreeParameterExpression): - return _FreeParameterExpressionIdentifier(new_value, identifier.type_) - return ast.FloatLiteral(new_value) + return _FreeParameterExpressionIdentifier(new_value) + else: + return ast.FloatLiteral(new_value) def visit_DurationLiteral(self, duration_literal: ast.DurationLiteral) -> ast.DurationLiteral: """Visit Duration Literal. @@ -80,7 +61,8 @@ def visit_DurationLiteral(self, duration_literal: ast.DurationLiteral) -> ast.Du return duration_literal new_duration = duration.expression.subs(self.param_values) if isinstance(new_duration, FreeParameterExpression): - return _FreeParameterExpressionIdentifier(new_duration, duration.type_).to_ast( - self.program + return ast.DurationLiteral( + _FreeParameterExpressionIdentifier(new_duration), duration_literal.unit ) + # return super().visit(duration_literal) return OQDurationLiteral(new_duration).to_ast(self.program) diff --git a/src/braket/pulse/ast/qasm_parser.py b/src/braket/pulse/ast/qasm_parser.py index 9f3d82f36..f43fb7e7c 100644 --- a/src/braket/pulse/ast/qasm_parser.py +++ b/src/braket/pulse/ast/qasm_parser.py @@ -17,7 +17,6 @@ from openqasm3.ast import DurationLiteral from openqasm3.printer import PrinterState -from braket.parametric.free_parameter_expression import FreeParameterExpression from braket.pulse.ast.free_parameters import _FreeParameterExpressionIdentifier diff --git a/src/braket/pulse/pulse_sequence.py b/src/braket/pulse/pulse_sequence.py index b797756d4..b55e94f5b 100644 --- a/src/braket/pulse/pulse_sequence.py +++ b/src/braket/pulse/pulse_sequence.py @@ -329,7 +329,9 @@ def _format_parameter_ast( if isinstance(parameter, FreeParameterExpression): for p in parameter.expression.free_symbols: self._free_parameters.add(FreeParameter(p.name)) - return _FreeParameterExpressionIdentifier(parameter, type_) + if isinstance(type_, ast.DurationType): + return FreeParameterExpression(parameter, type_) + return parameter else: if isinstance(type_, ast.DurationType): return OQDurationLiteral(parameter) diff --git a/src/braket/pulse/waveforms.py b/src/braket/pulse/waveforms.py index 48da8904b..16f454b03 100644 --- a/src/braket/pulse/waveforms.py +++ b/src/braket/pulse/waveforms.py @@ -28,7 +28,6 @@ subs_if_free_parameter, ) from braket.parametric.parameterizable import Parameterizable -from braket.pulse.ast.free_parameters import _FreeParameterExpressionIdentifier class Waveform(ABC): @@ -453,13 +452,9 @@ def _make_identifier_name() -> str: def _map_to_oqpy_type( parameter: Union[FreeParameterExpression, float], is_duration_type: bool = False -) -> Union[_FreeParameterExpressionIdentifier, OQPyExpression]: - if isinstance(parameter, FreeParameterExpression): - return ( - _FreeParameterExpressionIdentifier(parameter, duration) - if is_duration_type - else _FreeParameterExpressionIdentifier(parameter) - ) +) -> Union[FreeParameterExpression, OQPyExpression]: + if isinstance(parameter, FreeParameterExpression) and is_duration_type: + return FreeParameterExpression(parameter, duration) return parameter diff --git a/test/unit_tests/braket/pulse/test_pulse_sequence.py b/test/unit_tests/braket/pulse/test_pulse_sequence.py index 0f6128174..4890a4b36 100644 --- a/test/unit_tests/braket/pulse/test_pulse_sequence.py +++ b/test/unit_tests/braket/pulse/test_pulse_sequence.py @@ -126,7 +126,8 @@ def test_pulse_sequence_make_bound_pulse_sequence(predefined_frame_1, predefined "OPENQASM 3.0;", "cal {", " waveform gauss_wf = gaussian((length_g) * 1s, (sigma_g) * 1s, 1, false);", - " waveform drag_gauss_wf = drag_gaussian((length_dg) * 1s, (sigma_dg) * 1s, 0.2, 1, false);", + " waveform drag_gauss_wf = drag_gaussian((length_dg) * 1s," + " (sigma_dg) * 1s, 0.2, 1, false);", " waveform constant_wf = constant((length_c) * 1s, 2.0 + 0.3im);", " waveform arb_wf = {1.0 + 0.4im, 0, 0.3, 0.1 + 0.2im};", " bit[2] psb;", From fc6aa3dddecca46ebe412d0c7a5af1b3661b764d Mon Sep 17 00:00:00 2001 From: Jean-Christophe Jaskula Date: Mon, 11 Sep 2023 13:02:53 -0400 Subject: [PATCH 04/26] change back FPEIdentifier's parent to Identifier --- src/braket/parametric/free_parameter_expression.py | 14 ++++++-------- src/braket/pulse/ast/free_parameters.py | 1 - 2 files changed, 6 insertions(+), 9 deletions(-) diff --git a/src/braket/parametric/free_parameter_expression.py b/src/braket/parametric/free_parameter_expression.py index 1da2469e4..9eada787f 100644 --- a/src/braket/parametric/free_parameter_expression.py +++ b/src/braket/parametric/free_parameter_expression.py @@ -21,8 +21,9 @@ ClassicalType, DurationLiteral, DurationType, + Expression, FloatType, - QASMNode, + Identifier, TimeUnit, ) from oqpy import Program @@ -187,34 +188,31 @@ def __repr__(self) -> str: """ return repr(self.expression) - def to_ast(self, program: Program) -> QASMNode: + def to_ast(self, program: Program) -> Expression: """Creates an AST node for the :class:'FreeParameterExpression'. Args: program (Program): Unused. Returns: - QASMNode: The AST node. + Expression: The AST node. """ if isinstance(self._type, DurationType): return DurationLiteral(_FreeParameterExpressionIdentifier(self), TimeUnit.s) return _FreeParameterExpressionIdentifier(self) -class _FreeParameterExpressionIdentifier(QASMNode): +class _FreeParameterExpressionIdentifier(Identifier): """Dummy AST node with FreeParameterExpression instance attached""" def __init__(self, expression: FreeParameterExpression): - self.name = f"FreeParameterExpression({expression})" + super().__init__(name=f"FreeParameterExpression({expression})") self._expression = expression @property def expression(self) -> FreeParameterExpression: return self._expression - def __repr__(self) -> str: - return f"_FreeParameterExpressionIdentifier(name={self.name})" - def subs_if_free_parameter(parameter: Any, **kwargs) -> Any: """Substitute a free parameter with the given kwargs, if any. diff --git a/src/braket/pulse/ast/free_parameters.py b/src/braket/pulse/ast/free_parameters.py index 56bedc755..1d8545933 100644 --- a/src/braket/pulse/ast/free_parameters.py +++ b/src/braket/pulse/ast/free_parameters.py @@ -64,5 +64,4 @@ def visit_DurationLiteral(self, duration_literal: ast.DurationLiteral) -> ast.Du return ast.DurationLiteral( _FreeParameterExpressionIdentifier(new_duration), duration_literal.unit ) - # return super().visit(duration_literal) return OQDurationLiteral(new_duration).to_ast(self.program) From f7da1155d463747fd6e087707a3e4d522260d2ca Mon Sep 17 00:00:00 2001 From: Jean-Christophe Jaskula Date: Mon, 11 Sep 2023 13:53:05 -0400 Subject: [PATCH 05/26] clean up syntax --- src/braket/pulse/pulse_sequence.py | 17 ++++++++--------- src/braket/pulse/waveforms.py | 8 +++++--- 2 files changed, 13 insertions(+), 12 deletions(-) diff --git a/src/braket/pulse/pulse_sequence.py b/src/braket/pulse/pulse_sequence.py index b55e94f5b..c7583b82d 100644 --- a/src/braket/pulse/pulse_sequence.py +++ b/src/braket/pulse/pulse_sequence.py @@ -183,7 +183,7 @@ def delay( Returns: PulseSequence: self, with the instruction added. """ - duration = self._format_parameter_ast(duration, type_=ast.DurationType()) + duration = self._format_parameter_ast(duration, _type=ast.DurationType()) if not isinstance(qubits_or_frames, QubitSet): if not isinstance(qubits_or_frames, list): qubits_or_frames = [qubits_or_frames] @@ -324,18 +324,17 @@ def to_ir(self) -> str: def _format_parameter_ast( self, parameter: Union[float, FreeParameterExpression], - type_: ast.ClassicalType = ast.FloatType(), + _type: ast.ClassicalType = ast.FloatType(), ) -> Union[float, _FreeParameterExpressionIdentifier]: if isinstance(parameter, FreeParameterExpression): for p in parameter.expression.free_symbols: self._free_parameters.add(FreeParameter(p.name)) - if isinstance(type_, ast.DurationType): - return FreeParameterExpression(parameter, type_) - return parameter - else: - if isinstance(type_, ast.DurationType): - return OQDurationLiteral(parameter) - return parameter + return ( + FreeParameterExpression(parameter, _type) + if isinstance(_type, ast.DurationType) + else parameter + ) + return OQDurationLiteral(parameter) if isinstance(_type, ast.DurationType) else parameter def _parse_arg_from_calibration_schema( self, argument: Dict, waveforms: Dict[Waveform], frames: Dict[Frame] diff --git a/src/braket/pulse/waveforms.py b/src/braket/pulse/waveforms.py index 16f454b03..72d2120bc 100644 --- a/src/braket/pulse/waveforms.py +++ b/src/braket/pulse/waveforms.py @@ -453,9 +453,11 @@ def _make_identifier_name() -> str: def _map_to_oqpy_type( parameter: Union[FreeParameterExpression, float], is_duration_type: bool = False ) -> Union[FreeParameterExpression, OQPyExpression]: - if isinstance(parameter, FreeParameterExpression) and is_duration_type: - return FreeParameterExpression(parameter, duration) - return parameter + return ( + FreeParameterExpression(parameter, duration) + if isinstance(parameter, FreeParameterExpression) and is_duration_type + else parameter + ) def _parse_waveform_from_calibration_schema(waveform: Dict) -> Waveform: From 21bdc0357a27c4824f4b2bff22ddb0fe49f13677 Mon Sep 17 00:00:00 2001 From: Jean-Christophe Jaskula Date: Mon, 11 Sep 2023 19:04:20 -0400 Subject: [PATCH 06/26] Merge branch 'jcjaskula-aws/draw_pulse_sequence_of_circuits' into jcjaskula-aws/draw_pulse_sequence_of_circuits_final_ngc_launch --- src/braket/circuits/circuit.py | 39 ++++- src/braket/circuits/circuit_pulse_sequence.py | 144 ++++++++++++++++++ src/braket/circuits/observable.py | 17 +++ src/braket/circuits/observables.py | 9 ++ src/braket/circuits/result_type.py | 16 ++ src/braket/circuits/result_types.py | 8 + src/braket/pulse/pulse_sequence.py | 31 ++++ 7 files changed, 263 insertions(+), 1 deletion(-) create mode 100644 src/braket/circuits/circuit_pulse_sequence.py diff --git a/src/braket/circuits/circuit.py b/src/braket/circuits/circuit.py index d3bf2d0e6..f8bcf1848 100644 --- a/src/braket/circuits/circuit.py +++ b/src/braket/circuits/circuit.py @@ -15,13 +15,30 @@ import warnings from numbers import Number -from typing import Any, Callable, Dict, Iterable, List, Optional, Set, Tuple, Type, TypeVar, Union +from typing import ( + TYPE_CHECKING, + Any, + Callable, + Dict, + Iterable, + List, + Optional, + Set, + Tuple, + Type, + TypeVar, + Union, +) + +if TYPE_CHECKING: + from braket.aws.aws_device import AwsDevice import numpy as np import oqpy from braket.circuits import compiler_directives from braket.circuits.ascii_circuit_diagram import AsciiCircuitDiagram +from braket.circuits.circuit_pulse_sequence import CircuitPulseSequenceBuilder from braket.circuits.free_parameter import FreeParameter from braket.circuits.free_parameter_expression import FreeParameterExpression from braket.circuits.gate import Gate @@ -1094,6 +1111,26 @@ def diagram(self, circuit_diagram_class: Type = AsciiCircuitDiagram) -> str: """ return circuit_diagram_class.build_diagram(self) + def pulse_sequence( + self, + device: AwsDevice, + gate_definitions: Optional[Dict[Tuple[Gate, QubitSet], PulseSequence]], + pulse_sequence_builder_class: Type = CircuitPulseSequenceBuilder, + ) -> PulseSequence: + """ + Get the associated pulse sequence for the current circuit. + + Args: + gate_definitions (Optional[Dict[Tuple[Gate, QubitSet], PulseSequence]]): + Additional gate definitions + pulse_sequence_builder_class (Type): A `CircuitPulseSequenceBuilder` class that builds + the pulse sequence for this circuit. Default = `CircuitPulseSequenceBuilder`. + + Returns: + PulseSequence: A PulseSequence corresponding to the full circuit. + """ + return pulse_sequence_builder_class(device, gate_definitions).build_pulse_sequence(self) + def to_ir( self, ir_type: IRType = IRType.JAQCD, diff --git a/src/braket/circuits/circuit_pulse_sequence.py b/src/braket/circuits/circuit_pulse_sequence.py new file mode 100644 index 000000000..ae3c87493 --- /dev/null +++ b/src/braket/circuits/circuit_pulse_sequence.py @@ -0,0 +1,144 @@ +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"). You +# may not use this file except in compliance with the License. A copy of +# the License is located at +# +# http://aws.amazon.com/apache2.0/ +# +# or in the "license" file accompanying this file. This file is +# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF +# ANY KIND, either express or implied. See the License for the specific +# language governing permissions and limitations under the License. + +from __future__ import annotations + +from typing import TYPE_CHECKING, Dict, List, Optional, Tuple + +if TYPE_CHECKING: + from braket.aws.aws_device import AwsDevice + +import braket.circuits.circuit as cir +from braket.circuits.gate_calibrations import GateCalibrations +from braket.circuits.gates import Gate, PulseGate +from braket.circuits.qubit_set import QubitSet +from braket.circuits.result_type import ResultType +from braket.circuits.serialization import IRType +from braket.pulse.pulse_sequence import PulseSequence + + +class CircuitPulseSequenceBuilder: + """Builds ASCII string circuit diagrams.""" + + def __init__( + self, + device: AwsDevice, + gate_definitions: Optional[Dict[Tuple[Gate, QubitSet], PulseSequence]] = None, + ) -> None: + self._device = device + self._gate_calibrations = GateCalibrations( + gate_definitions if gate_definitions is not None else {} + ) + + def build_pulse_sequence(self, circuit: cir.Circuit) -> PulseSequence: + """ + Build a PulseSequence corresponding to the full circuit. + + Args: + circuit (Circuit): Circuit for which to build a diagram. + + Returns: + PulseSequence: a pulse sequence created from all gates. + """ + + pulse_sequence = PulseSequence() + if not circuit.instructions: + return pulse_sequence + + # A list of parameters in the circuit to the currently assigned values. + if circuit.parameters: + raise ValueError("All parameters must be assigned to draw the pulse sequence.") + + # circuit needs to be verbatim + # if circuit not verbatim + # raise ValueError("Circuit must be encapsulated in a verbatim box.") + + for instruction in circuit.instructions: + gate = instruction.operator + qubit = instruction.target + if isinstance(gate, PulseGate): + gate_pulse_sequence = gate.pulse_sequence + elif ( + gate_pulse_sequence := self._gate_calibrations.pulse_sequences[(gate, qubit)] + ) is None: + raise ValueError( + f"No pulse sequence for {gate.name} was provided in the gate" + " calibration set." + ) + + # FIXME: this creates a single cal block, we should have defcals because barrier; + # would be applied to everything + pulse_sequence += gate_pulse_sequence + + if circuit.result_types: + for result_type in circuit.result_types: + pragma_str = result_type.to_ir(IRType.OPENQASM) + if pragma_str[:8] == "#pragma ": + pulse_sequence._program.pragma(pragma_str[8:]) + else: + raise ValueError("Result type cannot be used with pulse sequences.") + else: + for qubit in circuit.qubits: + pulse_sequence.capture_v0(self._readout_frame(qubit)) + # if ( + # measure_pulse_sequence := self._gate_calibrations.pulse_sequences[ + # ("MEASURE", qubit) + # ] + # ) is None: + # raise ValueError( + # "No pulse sequence for the measurement instruction was provided" + # " in the gate calibration set." + # ) + # pulse_sequence += measure_pulse_sequence + + # Result type columns + ( + additional_result_types, + _, + ) = CircuitPulseSequenceBuilder._categorize_result_types(circuit.result_types) + + # Additional result types line on bottom + if additional_result_types: + print(f"\nAdditional result types: {', '.join(additional_result_types)}") + + return pulse_sequence + + def _readout_frame(self, qubit: QubitSet): + if self._device.name == "Aspen-M-3": + return self._device.frames[f"q{int(qubit)}_ro_rx_frame"] + elif self._device.name == "Lucy": + return self._device.frames[f"r{int(qubit)}_measure"] + + @staticmethod + def _categorize_result_types( + result_types: List[ResultType], + ) -> Tuple[List[str], List[ResultType]]: + """ + Categorize result types into result types with target and those without. + + Args: + result_types (List[ResultType]): list of result types + + Returns: + Tuple[List[str], List[ResultType]]: first element is a list of result types + without `target` attribute; second element is a list of result types with + `target` attribute + """ + additional_result_types = [] + target_result_types = [] + for result_type in result_types: + if hasattr(result_type, "target"): + target_result_types.append(result_type) + else: + additional_result_types.extend(result_type.ascii_symbols) + return additional_result_types, target_result_types diff --git a/src/braket/circuits/observable.py b/src/braket/circuits/observable.py index c63304c91..3ac5477fd 100644 --- a/src/braket/circuits/observable.py +++ b/src/braket/circuits/observable.py @@ -27,6 +27,7 @@ OpenQASMSerializationProperties, SerializationProperties, ) +from braket.pulse.pulse_sequence import PulseSequence class Observable(QuantumOperator): @@ -104,6 +105,22 @@ def _to_openqasm( """ raise NotImplementedError("to_openqasm has not been implemented yet.") + def _to_pulse_sequence( + self, serialization_properties: OpenQASMSerializationProperties, target: QubitSet = None + ) -> PulseSequence: + """ + Returns the openqasm string representation of the result type. + + Args: + serialization_properties (OpenQASMSerializationProperties): The serialization properties + to use while serializing the object to the IR representation. + target (QubitSet): target qubit(s). Defaults to None. + + Returns: + str: Representing the openqasm representation of the result type. + """ + raise NotImplementedError("to_pulse_sequence has not been implemented yet.") + @property def coefficient(self) -> int: """ diff --git a/src/braket/circuits/observables.py b/src/braket/circuits/observables.py index b27170230..1472fe104 100644 --- a/src/braket/circuits/observables.py +++ b/src/braket/circuits/observables.py @@ -31,6 +31,7 @@ ) from braket.circuits.qubit_set import QubitSet from braket.circuits.serialization import IRType, OpenQASMSerializationProperties +from braket.pulse.pulse_sequence import PulseSequence class H(StandardObservable): @@ -152,6 +153,14 @@ def _to_openqasm( else: return f"{coef_prefix}x all" + def _to_pulse_sequence( + self, serialization_properties: OpenQASMSerializationProperties, target: QubitSet = None + ) -> PulseSequence: + pulse_sequence = PulseSequence() + for gate in self.basis_rotation_gates: + pulse_sequence += gate.pulse_sequence + return pulse_sequence + def to_matrix(self) -> np.ndarray: return self.coefficient * np.array([[0.0, 1.0], [1.0, 0.0]], dtype=complex) diff --git a/src/braket/circuits/result_type.py b/src/braket/circuits/result_type.py index d711e273b..d66eb1f7b 100644 --- a/src/braket/circuits/result_type.py +++ b/src/braket/circuits/result_type.py @@ -25,6 +25,7 @@ OpenQASMSerializationProperties, SerializationProperties, ) +from braket.pulse.pulse_sequence import PulseSequence class ResultType: @@ -117,6 +118,21 @@ def _to_openqasm(self, serialization_properties: OpenQASMSerializationProperties """ raise NotImplementedError("to_openqasm has not been implemented yet.") + def to_pulse_sequence( + self, serialization_properties: OpenQASMSerializationProperties + ) -> PulseSequence: + """ + Returns the openqasm string representation of the result type. + + Args: + serialization_properties (OpenQASMSerializationProperties): The serialization properties + to use while serializing the object to the IR representation. + + Returns: + str: Representing the openqasm representation of the result type. + """ + raise NotImplementedError("to_pulse_sequence has not been implemented yet.") + def copy( self, target_mapping: Dict[QubitInput, QubitInput] = None, target: QubitSetInput = None ) -> ResultType: diff --git a/src/braket/circuits/result_types.py b/src/braket/circuits/result_types.py index d73c7fbda..31ff76323 100644 --- a/src/braket/circuits/result_types.py +++ b/src/braket/circuits/result_types.py @@ -491,6 +491,14 @@ def _to_openqasm(self, serialization_properties: OpenQASMSerializationProperties ) return f"#pragma braket result expectation {observable_ir}" + def to_pulse_sequence(self, serialization_properties: OpenQASMSerializationProperties) -> str: + observable_pulse_sequence = self.observable.to_pulse_sequence( + target=self.target, + ir_type=IRType.OPENQASM, + serialization_properties=serialization_properties, + ) + return observable_pulse_sequence + @staticmethod @circuit.subroutine(register=True) def expectation(observable: Observable, target: QubitSetInput = None) -> ResultType: diff --git a/src/braket/pulse/pulse_sequence.py b/src/braket/pulse/pulse_sequence.py index c7583b82d..3f63bb4df 100644 --- a/src/braket/pulse/pulse_sequence.py +++ b/src/braket/pulse/pulse_sequence.py @@ -421,6 +421,37 @@ def __call__(self, arg: Any = None, **kwargs) -> PulseSequence: param_values[str(key)] = val return self.make_bound_pulse_sequence(param_values) + def __iadd__(self, other: PulseSequence) -> PulseSequence: + """In-place concatenation of pulse sequence.""" + if self._capture_v0_count: + raise ValueError( + "Cannot add a pulse sequence to another one that has already measurement" + " instructions." + ) + for frameId, frame in other._frames.items(): + _validate_uniqueness(self._frames, frame) + self._frames[frameId] = frame + for waveformId, waveform in other._waveforms.items(): + _validate_uniqueness(self._waveforms, waveform) + self._waveforms[waveformId] = waveform + for fp in other._free_parameters: + if fp in self._free_parameters: + raise ValueError( + f"A free parameter with the name {fp.name} already exists in the pulse" + " sequence. Please rename this free parameter." + ) + self._free_parameters.add(fp) + + self._program += other._program + return self + + def __add__(self, other: PulseSequence) -> PulseSequence: + """Return concatenation of two programs.""" + assert isinstance(other, PulseSequence) + self_copy = deepcopy(self) + self_copy += other + return self_copy + def __eq__(self, other): return ( isinstance(other, PulseSequence) From 5e6e9c3ece86836b1b1763d0d7993ed82b4d372d Mon Sep 17 00:00:00 2001 From: Jean-Christophe Jaskula Date: Sun, 3 Dec 2023 23:28:32 +0100 Subject: [PATCH 07/26] Merge branch 'main' into jcjaskula-aws/draw_pulse_sequence_of_circuits --- .git-template/hooks/pre-commit | 5 + .github/pull_request_template.md | 6 +- .github/workflows/check-format.yml | 9 +- .github/workflows/code-freeze.yml | 39 + .github/workflows/dependent-tests.yml | 6 +- .github/workflows/publish-to-pypi.yml | 6 +- .github/workflows/python-package.yml | 6 +- .github/workflows/twine-check.yml | 6 +- .pre-commit-config.yaml | 24 +- .readthedocs.yml | 2 +- CHANGELOG.md | 161 +++ CODEOWNERS | 2 +- CONTRIBUTING.md | 12 +- README.md | 22 +- doc/examples-adv-circuits-algorithms.rst | 24 +- doc/examples-braket-features.rst | 30 +- doc/examples-getting-started.rst | 30 +- doc/examples-hybrid-jobs.rst | 24 +- doc/examples-hybrid-quantum.rst | 18 +- doc/examples-ml-pennylane.rst | 24 +- doc/examples.rst | 4 +- doc/index.rst | 2 +- doc/requirements.txt | 5 +- examples/hybrid_job.py | 49 + examples/hybrid_job_script.py | 56 ++ examples/job.py | 45 - setup.cfg | 1 + setup.py | 10 +- src/braket/_sdk/_version.py | 2 +- .../ahs/analog_hamiltonian_simulation.py | 7 +- src/braket/ahs/atom_arrangement.py | 13 +- src/braket/ahs/driving_field.py | 14 +- src/braket/ahs/hamiltonian.py | 8 +- src/braket/ahs/pattern.py | 9 +- src/braket/ahs/shifting_field.py | 12 +- src/braket/annealing/problem.py | 8 +- src/braket/aws/aws_device.py | 167 +++- src/braket/aws/aws_quantum_job.py | 171 ++-- src/braket/aws/aws_quantum_task.py | 113 ++- src/braket/aws/aws_quantum_task_batch.py | 48 +- src/braket/aws/aws_session.py | 124 ++- src/braket/aws/queue_information.py | 85 ++ src/braket/circuits/angled_gate.py | 27 +- src/braket/circuits/ascii_circuit_diagram.py | 28 +- src/braket/circuits/basis_state.py | 4 +- src/braket/circuits/braket_program_context.py | 26 +- src/braket/circuits/circuit.py | 237 ++--- src/braket/circuits/circuit_diagram.py | 1 + src/braket/circuits/compiler_directive.py | 21 +- src/braket/circuits/gate.py | 26 +- src/braket/circuits/gate_calibrations.py | 28 +- src/braket/circuits/gates.py | 778 +++++++++++++-- src/braket/circuits/instruction.py | 45 +- src/braket/circuits/moments.py | 32 +- src/braket/circuits/noise.py | 47 +- src/braket/circuits/noise_helpers.py | 33 +- .../circuit_instruction_criteria.py | 8 +- src/braket/circuits/noise_model/criteria.py | 11 +- .../noise_model/criteria_input_parsing.py | 13 +- .../circuits/noise_model/gate_criteria.py | 9 +- .../noise_model/initialization_criteria.py | 2 +- .../circuits/noise_model/noise_model.py | 44 +- .../noise_model/observable_criteria.py | 9 +- .../qubit_initialization_criteria.py | 9 +- .../noise_model/unitary_gate_criteria.py | 9 +- src/braket/circuits/noises.py | 21 +- src/braket/circuits/observable.py | 43 +- src/braket/circuits/observables.py | 72 +- src/braket/circuits/quantum_operator.py | 7 +- .../circuits/quantum_operator_helpers.py | 2 +- src/braket/circuits/qubit.py | 56 +- src/braket/circuits/qubit_set.py | 81 +- src/braket/circuits/result_type.py | 56 +- src/braket/circuits/result_types.py | 80 +- src/braket/circuits/serialization.py | 4 +- src/braket/circuits/unitary_calculation.py | 75 +- src/braket/devices/device.py | 14 +- src/braket/devices/local_simulator.py | 24 +- src/braket/error_mitigation/debias.py | 4 +- .../error_mitigation/error_mitigation.py | 6 +- src/braket/jobs/__init__.py | 10 + src/braket/jobs/_entry_point_template.py | 79 ++ src/braket/jobs/config.py | 17 +- src/braket/jobs/data_persistence.py | 117 ++- src/braket/jobs/environment_variables.py | 85 ++ src/braket/jobs/hybrid_job.py | 415 ++++++++ src/braket/jobs/image_uris.py | 8 +- src/braket/jobs/local/local_job.py | 88 +- src/braket/jobs/local/local_job_container.py | 6 +- .../cwl_insights_metrics_fetcher.py | 12 +- src/braket/jobs/quantum_job.py | 6 +- src/braket/jobs/quantum_job_creation.py | 165 +-- src/braket/parametric/free_parameter.py | 22 +- .../parametric/free_parameter_expression.py | 6 +- src/braket/parametric/parameterizable.py | 6 +- src/braket/pulse/ast/approximation_parser.py | 22 +- src/braket/pulse/ast/free_parameters.py | 5 +- src/braket/pulse/ast/qasm_parser.py | 1 + src/braket/pulse/ast/qasm_transformer.py | 5 +- src/braket/pulse/frame.py | 6 +- src/braket/pulse/port.py | 6 +- src/braket/pulse/pulse_sequence.py | 34 +- src/braket/pulse/pulse_sequence_trace.py | 7 +- src/braket/pulse/waveforms.py | 26 +- .../quantum_information/pauli_string.py | 14 +- src/braket/registers/__init__.py | 15 + src/braket/registers/qubit.py | 68 ++ src/braket/registers/qubit_set.py | 95 ++ ...iltonian_simulation_quantum_task_result.py | 9 +- .../tasks/gate_model_quantum_task_result.py | 74 +- src/braket/tasks/local_quantum_task_batch.py | 6 +- src/braket/tasks/quantum_task.py | 6 +- src/braket/tasks/quantum_task_batch.py | 6 +- src/braket/timings/time_series.py | 29 +- src/braket/tracking/pricing.py | 5 +- src/braket/tracking/tracker.py | 10 +- .../job_test_submodule_file.py | 3 + .../job_test_submodule/requirements.txt | 1 + test/integ_tests/job_test_script.py | 23 +- test/integ_tests/requirements.txt | 1 + test/integ_tests/test_adjoint_gradient.py | 32 +- test/integ_tests/test_create_quantum_job.py | 151 ++- test/integ_tests/test_device_creation.py | 2 +- test/integ_tests/test_pulse.py | 6 + test/integ_tests/test_queue_information.py | 84 ++ test/unit_tests/braket/aws/test_aws_device.py | 88 +- .../braket/aws/test_aws_quantum_job.py | 32 +- .../braket/aws/test_aws_quantum_task.py | 55 +- .../unit_tests/braket/aws/test_aws_session.py | 84 +- .../braket/circuits/test_circuit.py | 943 +++++------------- test/unit_tests/braket/circuits/test_gates.py | 126 ++- .../braket/circuits/test_noise_helpers.py | 2 +- .../unit_tests/braket/circuits/test_noises.py | 22 +- .../braket/circuits/test_observables.py | 20 +- .../braket/circuits/test_result_types.py | 24 +- .../braket/devices/test_local_simulator.py | 18 +- test/unit_tests/braket/jobs/job_module.py | 2 + .../braket/jobs/test_data_persistence.py | 72 +- .../braket/jobs/test_environment_variables.py | 66 ++ .../unit_tests/braket/jobs/test_hybrid_job.py | 549 ++++++++++ .../braket/jobs/test_quantum_job_creation.py | 7 +- .../braket/parametric/test_free_parameter.py | 33 - .../pulse/ast/test_approximation_parser.py | 2 +- .../{circuits => registers}/test_qubit.py | 2 +- .../{circuits => registers}/test_qubit_set.py | 2 +- .../test_gate_model_quantum_task_result.py | 112 +++ tox.ini | 39 +- 147 files changed, 4997 insertions(+), 2498 deletions(-) create mode 100644 .git-template/hooks/pre-commit create mode 100644 .github/workflows/code-freeze.yml create mode 100644 examples/hybrid_job.py create mode 100644 examples/hybrid_job_script.py delete mode 100644 examples/job.py create mode 100644 src/braket/aws/queue_information.py create mode 100644 src/braket/jobs/_entry_point_template.py create mode 100644 src/braket/jobs/environment_variables.py create mode 100644 src/braket/jobs/hybrid_job.py create mode 100644 src/braket/registers/__init__.py create mode 100644 src/braket/registers/qubit.py create mode 100644 src/braket/registers/qubit_set.py create mode 100644 test/integ_tests/job_test_module/job_test_submodule/job_test_submodule_file.py create mode 100644 test/integ_tests/job_test_module/job_test_submodule/requirements.txt create mode 100644 test/integ_tests/requirements.txt create mode 100644 test/integ_tests/test_queue_information.py create mode 100644 test/unit_tests/braket/jobs/job_module.py create mode 100644 test/unit_tests/braket/jobs/test_environment_variables.py create mode 100644 test/unit_tests/braket/jobs/test_hybrid_job.py rename test/unit_tests/braket/{circuits => registers}/test_qubit.py (97%) rename test/unit_tests/braket/{circuits => registers}/test_qubit_set.py (97%) diff --git a/.git-template/hooks/pre-commit b/.git-template/hooks/pre-commit new file mode 100644 index 000000000..6d8d24a1d --- /dev/null +++ b/.git-template/hooks/pre-commit @@ -0,0 +1,5 @@ +#!/usr/bin/env bash +if [ -f .pre-commit-config.yaml ]; then + echo 'pre-commit configuration detected, but `pre-commit install` was never run' 1>&2 + exit 1 +fi diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 819c43010..87dda8a1f 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -10,9 +10,9 @@ _Put an `x` in the boxes that apply. You can also fill these out after creating #### General -- [ ] I have read the [CONTRIBUTING](https://github.com/aws/amazon-braket-sdk-python/blob/main/CONTRIBUTING.md) doc -- [ ] I used the commit message format described in [CONTRIBUTING](https://github.com/aws/amazon-braket-sdk-python/blob/main/CONTRIBUTING.md#commit-your-change) -- [ ] I have updated any necessary documentation, including [READMEs](https://github.com/aws/amazon-braket-sdk-python/blob/main/README.md) and [API docs](https://github.com/aws/amazon-braket-sdk-python/blob/main/CONTRIBUTING.md#documentation-guidelines) (if appropriate) +- [ ] I have read the [CONTRIBUTING](https://github.com/amazon-braket/amazon-braket-sdk-python/blob/main/CONTRIBUTING.md) doc +- [ ] I used the commit message format described in [CONTRIBUTING](https://github.com/amazon-braket/amazon-braket-sdk-python/blob/main/CONTRIBUTING.md#commit-your-change) +- [ ] I have updated any necessary documentation, including [READMEs](https://github.com/amazon-braket/amazon-braket-sdk-python/blob/main/README.md) and [API docs](https://github.com/amazon-braket/amazon-braket-sdk-python/blob/main/CONTRIBUTING.md#documentation-guidelines) (if appropriate) #### Tests diff --git a/.github/workflows/check-format.yml b/.github/workflows/check-format.yml index 160e8e000..dbe9599c1 100644 --- a/.github/workflows/check-format.yml +++ b/.github/workflows/check-format.yml @@ -16,16 +16,15 @@ jobs: check-code-format: runs-on: ubuntu-latest steps: - - uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac # v4.0.0 + - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 - name: Set up Python - uses: actions/setup-python@61a6322f88396a6271a6ee3565807d608ecaddd1 # v4.7.0 + uses: actions/setup-python@65d7f2d534ac1bc67fcd62888c5f4f3d2cb2b236 # v4.7.1 with: - python-version: '3.x' + python-version: '3.9' - name: Install dependencies run: | pip install --upgrade pip pip install -e .[test] - name: Run code format checks run: | - black --check . - flake8 --enable-extensions=BCS src + tox -e linters_check diff --git a/.github/workflows/code-freeze.yml b/.github/workflows/code-freeze.yml new file mode 100644 index 000000000..5aff0abc7 --- /dev/null +++ b/.github/workflows/code-freeze.yml @@ -0,0 +1,39 @@ +name: Code Freeze + +on: + pull_request: + branches: + - main + workflow_dispatch: + +permissions: + contents: read + +env: + FROZEN: ${{ vars.FROZEN }} + UNFROZEN_PREFIX: ${{ vars.UNFROZEN_PREFIX }} + +jobs: + check-pr-frozen-status: + runs-on: ubuntu-latest + steps: + - name: Fetch PR data and check if merge allowed + if: env.FROZEN == 'true' + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + PR_DATA=$(curl -s \ + -H "Authorization: Bearer $GITHUB_TOKEN" \ + -H "Accept: application/vnd.github.v3+json" \ + https://api.github.com/repos/${{ github.repository }}/pulls/${{ github.event.pull_request.number }}) + BRANCH_NAME=$(echo $PR_DATA | jq .head.ref -r) + PR_TITLE=$(echo $PR_DATA | jq .title -r) + + echo $BRANCH_NAME + echo $PR_TITLE + + if [[ "$BRANCH_NAME" != $UNFROZEN_PREFIX* ]] && + [[ "$PR_TITLE" != fix:* && "$PR_TITLE" != *"[critical]"* ]]; then + echo "Error: You can only merge from branches that start with '$UNFROZEN_PREFIX', or PRs titled with 'fix: ' and containing '[critical]'." + exit 1 + fi diff --git a/.github/workflows/dependent-tests.yml b/.github/workflows/dependent-tests.yml index 732600ba2..cfd395241 100644 --- a/.github/workflows/dependent-tests.yml +++ b/.github/workflows/dependent-tests.yml @@ -16,14 +16,14 @@ jobs: strategy: matrix: os: [ubuntu-latest, macos-latest, windows-latest] - python-version: ["3.8", "3.9", "3.10", "3.11"] + python-version: ["3.9", "3.10", "3.11"] dependent: - amazon-braket-pennylane-plugin-python steps: - - uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac # v4.0.0 + - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@61a6322f88396a6271a6ee3565807d608ecaddd1 # v4.7.0 + uses: actions/setup-python@65d7f2d534ac1bc67fcd62888c5f4f3d2cb2b236 # v4.7.1 with: python-version: ${{ matrix.python-version }} - name: Install dependencies diff --git a/.github/workflows/publish-to-pypi.yml b/.github/workflows/publish-to-pypi.yml index a26168c11..10aef9c29 100644 --- a/.github/workflows/publish-to-pypi.yml +++ b/.github/workflows/publish-to-pypi.yml @@ -12,15 +12,17 @@ jobs: name: Build and publish distribution to PyPi runs-on: ubuntu-latest steps: - - uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac # v4.0.0 + - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 - name: Set up Python - uses: actions/setup-python@61a6322f88396a6271a6ee3565807d608ecaddd1 # v4.7.0 + uses: actions/setup-python@65d7f2d534ac1bc67fcd62888c5f4f3d2cb2b236 # v4.7.1 with: python-version: '3.x' - name: Install wheel run: python -m pip install --user --upgrade wheel - name: Install twine run: python -m pip install --user --upgrade twine + - name: Install setuptools + run: python -m pip install --user --upgrade setuptools - name: Build a binary wheel and a source tarball run: python setup.py sdist bdist_wheel - name: Publish distribution to PyPI diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml index 591ffed15..0c02a7561 100644 --- a/.github/workflows/python-package.yml +++ b/.github/workflows/python-package.yml @@ -21,12 +21,12 @@ jobs: strategy: matrix: os: [ubuntu-latest, macos-latest, windows-latest] - python-version: ["3.8", "3.9", "3.10", "3.11"] + python-version: ["3.9", "3.10", "3.11"] steps: - - uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac # v4.0.0 + - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@61a6322f88396a6271a6ee3565807d608ecaddd1 # v4.7.0 + uses: actions/setup-python@65d7f2d534ac1bc67fcd62888c5f4f3d2cb2b236 # v4.7.1 with: python-version: ${{ matrix.python-version }} - name: Install dependencies diff --git a/.github/workflows/twine-check.yml b/.github/workflows/twine-check.yml index 5a5966763..f8e01fd72 100644 --- a/.github/workflows/twine-check.yml +++ b/.github/workflows/twine-check.yml @@ -14,15 +14,17 @@ jobs: name: Check long description runs-on: ubuntu-latest steps: - - uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac # v4.0.0 + - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 - name: Set up Python - uses: actions/setup-python@61a6322f88396a6271a6ee3565807d608ecaddd1 # v4.7.0 + uses: actions/setup-python@65d7f2d534ac1bc67fcd62888c5f4f3d2cb2b236 # v4.7.1 with: python-version: '3.x' - name: Install wheel run: python -m pip install --user --upgrade wheel - name: Install twine run: python -m pip install --user --upgrade twine + - name: Install setuptools + run: python -m pip install --user --upgrade setuptools - name: Build a binary wheel and a source tarball run: python setup.py sdist bdist_wheel - name: Check that long description will render correctly on PyPI. diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index d03e4d3d2..e9eea7581 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,18 +1,8 @@ repos: -- repo: local - hooks: - - id: tox - name: tox - stages: [push] - language: system - entry: tox - types: [python] - pass_filenames: false - - - id: tox_integ_tests - name: tox integ tests - stages: [push] - language: system - entry: tox -e integ-tests - types: [python] - pass_filenames: false + - repo: local + hooks: + - id: tox_linters + name: linters + entry: tox -e linters_check + language: system + types: [python] diff --git a/.readthedocs.yml b/.readthedocs.yml index 59d0f1631..e824a6afc 100644 --- a/.readthedocs.yml +++ b/.readthedocs.yml @@ -17,7 +17,7 @@ formats: build: os: ubuntu-22.04 tools: - python: "3.8" + python: "3.9" # Optionally set the version of Python and requirements required to build your docs python: diff --git a/CHANGELOG.md b/CHANGELOG.md index e83803bab..876595e2d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,166 @@ # Changelog +## v1.62.1 (2023-11-17) + +### Bug Fixes and Other Changes + + * Fix broken link to example notebook + * update: default no longer returning RETIRED devices from get_devices + +### Documentation Changes + + * Add matrix expressions to docstrings + +## v1.62.0 (2023-11-09) + +### Features + + * Add get_compiled_circuit convenience method + +## v1.61.0.post0 (2023-11-07) + +### Documentation Changes + + * Improve docstring for make_bound_circuit + +## v1.61.0 (2023-11-06) + +### Features + + * simplify entry point wrapper + +### Bug Fixes and Other Changes + + * fixing some type hints for optional params + +## v1.60.2 (2023-11-01) + +### Bug Fixes and Other Changes + + * drop task count for batch task tests to 3 + +## v1.60.1 (2023-11-01) + +### Bug Fixes and Other Changes + + * set python container version explicitly + * set decorator job working directory inside of function + * s3 config support for decorator jobs + +## v1.60.0 (2023-10-31) + +### Features + + * support dependency list for decorator hybrid jobs + +### Bug Fixes and Other Changes + + * Don't run pulse tests when QPU offline + +### Documentation Changes + + * Fix some nits in the decorator doc string + * update intended audience to include education and research + +## v1.59.2 (2023-10-25) + +### Bug Fixes and Other Changes + + * remove deprecated as_unitary method + +## v1.59.1.post0 (2023-10-24) + +### Documentation Changes + + * add the amazon braket tag in the stack exchange URL + +## v1.59.1 (2023-10-18) + +### Bug Fixes and Other Changes + + * doc fixes + +## v1.59.0 (2023-10-17) + +### Features + + * use region property + +## v1.58.1 (2023-10-16) + +### Bug Fixes and Other Changes + + * use separate aws session for python validation + +## v1.58.0 (2023-10-16) + +### Features + + * job decorator + +### Bug Fixes and Other Changes + + * update integ test for non-py310 + +## v1.57.2 (2023-10-11) + +### Bug Fixes and Other Changes + + * Use builtins for type hints + +## v1.57.1 (2023-10-05) + +### Bug Fixes and Other Changes + + * docs: fix helper docstring + +## v1.57.0 (2023-10-04) + +### Features + + * wrap non-dict results and update results on subsequent calls + * job helper functions + +### Bug Fixes and Other Changes + + * revert integ test changes + +## v1.56.2 (2023-10-03) + +### Bug Fixes and Other Changes + + * Refactor Qubit and QubitSet to a separate module + +## v1.56.1 (2023-09-27) + +### Bug Fixes and Other Changes + + * fixing search device when don't have access to a region. + +## v1.56.0 (2023-09-26) + +### Features + + * add queue visibility information + +## v1.55.1.post0 (2023-09-18) + +### Documentation Changes + + * Remove trailing backquotes + * add code contributors to the readme + * change the sphinx requirement to be greater than 7.0.0 + +## v1.55.1 (2023-09-14) + +### Bug Fixes and Other Changes + + * Revert "update: restricting parameter names to not collide with ones we use for OpenQASM generation. (#675)" + +### Documentation Changes + + * Replace aws org with amazon-braket + ## v1.55.0 (2023-09-09) ### Features diff --git a/CODEOWNERS b/CODEOWNERS index 2e2d8f104..df13e062e 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -3,4 +3,4 @@ # These owners will be the default owners for everything in # the repo. Unless a later match takes precedence, these accounts # will be requested for review when someone opens a pull request. -* @aws/amazon-braket-maintainers +* @amazon-braket/braket-maintainers diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 1e659a5b8..7362a17b7 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -30,7 +30,7 @@ information to effectively respond to your bug report or contribution. We welcome you to use the GitHub issue tracker to report bugs or suggest features. -When filing an issue, please check [existing open](https://github.com/aws/amazon-braket-sdk-python/issues) and [recently closed](https://github.com/aws/amazon-braket-sdk-python/issues?utf8=%E2%9C%93&q=is%3Aissue%20is%3Aclosed%20) issues to make sure somebody else hasn't already +When filing an issue, please check [existing open](https://github.com/amazon-braket/amazon-braket-sdk-python/issues) and [recently closed](https://github.com/amazon-braket/amazon-braket-sdk-python/issues?utf8=%E2%9C%93&q=is%3Aissue%20is%3Aclosed%20) issues to make sure somebody else hasn't already reported the issue. Please try to include as much information as you can. Details like these are incredibly useful: * A reproducible test case or series of steps. @@ -199,7 +199,7 @@ If a parameter of a function has a default value, please note what the default i If that default value is `None`, it can also be helpful to explain what happens when the parameter is `None`. If `**kwargs` is part of the function signature, link to the parent class(es) or method(s) so that the reader knows where to find the available parameters. -For an example file with docstrings, see [the `circuit` module](https://github.com/aws/amazon-braket-sdk-python/blob/main/src/braket/circuits/circuit.py). +For an example file with docstrings, see [the `circuit` module](https://github.com/amazon-braket/amazon-braket-sdk-python/blob/main/src/braket/circuits/circuit.py). ### Build and Test Documentation @@ -215,12 +215,12 @@ You can then find the generated HTML files in `build/documentation/html`. ## Find Contributions to Work On -Looking at the existing issues is a great way to find something to contribute on. As our projects, by default, use the default GitHub issue labels ((enhancement/bug/duplicate/help wanted/invalid/question/wontfix), looking at any ['help wanted'](https://github.com/aws/amazon-braket-sdk-python/labels/help%20wanted) issues is a great place to start. +Looking at the existing issues is a great way to find something to contribute on. As our projects, by default, use the default GitHub issue labels ((enhancement/bug/duplicate/help wanted/invalid/question/wontfix), looking at any ['help wanted'](https://github.com/amazon-braket/amazon-braket-sdk-python/labels/help%20wanted) issues is a great place to start. ## Building Integrations -The Amazon Braket SDK supports integrations with popular quantum computing frameworks such as [PennyLane](https://github.com/aws/amazon-braket-pennylane-plugin-python), [Strawberryfields](https://github.com/aws/amazon-braket-strawberryfields-plugin-python) and [DWave's Ocean library](https://github.com/aws/amazon-braket-ocean-plugin-python). These serve as a good reference for a new integration you wish to develop. +The Amazon Braket SDK supports integrations with popular quantum computing frameworks such as [PennyLane](https://github.com/amazon-braket/amazon-braket-pennylane-plugin-python), [Strawberryfields](https://github.com/amazon-braket/amazon-braket-strawberryfields-plugin-python) and [DWave's Ocean library](https://github.com/amazon-braket/amazon-braket-ocean-plugin-python). These serve as a good reference for a new integration you wish to develop. -When developing a new integration with the Amazon Braket SDK, please remember to update the [user agent header](https://datatracker.ietf.org/doc/html/rfc7231#section-5.5.3) to include version information for your integration. An example can be found [here](https://github.com/aws/amazon-braket-pennylane-plugin-python/commit/ccee35604afc2b04d83ee9103eccb2821a4256cb). +When developing a new integration with the Amazon Braket SDK, please remember to update the [user agent header](https://datatracker.ietf.org/doc/html/rfc7231#section-5.5.3) to include version information for your integration. An example can be found [here](https://github.com/amazon-braket/amazon-braket-pennylane-plugin-python/commit/ccee35604afc2b04d83ee9103eccb2821a4256cb). ## Code of Conduct @@ -236,6 +236,6 @@ If you discover a potential security issue in this project we ask that you notif ## Licensing -See the [LICENSE](https://github.com/aws/amazon-braket-sdk-python/blob/main/LICENSE) file for our project's licensing. We will ask you to confirm the licensing of your contribution. +See the [LICENSE](https://github.com/amazon-braket/amazon-braket-sdk-python/blob/main/LICENSE) file for our project's licensing. We will ask you to confirm the licensing of your contribution. We may ask you to sign a [Contributor License Agreement (CLA)](http://en.wikipedia.org/wiki/Contributor_License_Agreement) for larger changes. diff --git a/README.md b/README.md index 1f7fcb460..1c79e8034 100644 --- a/README.md +++ b/README.md @@ -2,8 +2,8 @@ [![Latest Version](https://img.shields.io/pypi/v/amazon-braket-sdk.svg)](https://pypi.python.org/pypi/amazon-braket-sdk) [![Supported Python Versions](https://img.shields.io/pypi/pyversions/amazon-braket-sdk.svg)](https://pypi.python.org/pypi/amazon-braket-sdk) -[![Build status](https://github.com/aws/amazon-braket-sdk-python/actions/workflows/python-package.yml/badge.svg?branch=main)](https://github.com/aws/amazon-braket-sdk-python/actions/workflows/python-package.yml) -[![codecov](https://codecov.io/gh/aws/amazon-braket-sdk-python/branch/main/graph/badge.svg?token=1lsqkZL3Ll)](https://codecov.io/gh/aws/amazon-braket-sdk-python) +[![Build status](https://github.com/amazon-braket/amazon-braket-sdk-python/actions/workflows/python-package.yml/badge.svg?branch=main)](https://github.com/amazon-braket/amazon-braket-sdk-python/actions/workflows/python-package.yml) +[![codecov](https://codecov.io/gh/amazon-braket/amazon-braket-sdk-python/branch/main/graph/badge.svg?token=1lsqkZL3Ll)](https://codecov.io/gh/amazon-braket/amazon-braket-sdk-python) [![Documentation Status](https://img.shields.io/readthedocs/amazon-braket-sdk-python.svg?logo=read-the-docs)](https://amazon-braket-sdk-python.readthedocs.io/en/latest/?badge=latest) The Amazon Braket Python SDK is an open source library that provides a framework that you can use to interact with quantum computing hardware devices through Amazon Braket. @@ -11,8 +11,8 @@ The Amazon Braket Python SDK is an open source library that provides a framework ## Prerequisites Before you begin working with the Amazon Braket SDK, make sure that you've installed or configured the following prerequisites. -### Python 3.8 or greater -Download and install Python 3.8 or greater from [Python.org](https://www.python.org/downloads/). +### Python 3.9 or greater +Download and install Python 3.9 or greater from [Python.org](https://www.python.org/downloads/). ### Git Install Git from https://git-scm.com/downloads. Installation instructions are provided on the download page. @@ -44,7 +44,7 @@ pip install amazon-braket-sdk You can also install from source by cloning this repository and running a pip install command in the root directory of the repository: ```bash -git clone https://github.com/aws/amazon-braket-sdk-python.git +git clone https://github.com/amazon-braket/amazon-braket-sdk-python.git cd amazon-braket-sdk-python pip install . ``` @@ -155,7 +155,7 @@ To select a quantum hardware device, specify its ARN as the value of the `device **Important** Quantum tasks may not run immediately on the QPU. The QPUs only execute quantum tasks during execution windows. To find their execution windows, please refer to the [AWS console](https://console.aws.amazon.com/braket/home) in the "Devices" tab. ## Sample Notebooks -Sample Jupyter notebooks can be found in the [amazon-braket-examples](https://github.com/aws/amazon-braket-examples/) repo. +Sample Jupyter notebooks can be found in the [amazon-braket-examples](https://github.com/amazon-braket/amazon-braket-examples/) repo. ## Braket Python SDK API Reference Documentation @@ -233,14 +233,18 @@ tox -e integ-tests -- your-arguments ### Issues and Bug Reports If you encounter bugs or face issues while using the SDK, please let us know by posting -the issue on our [Github issue tracker](https://github.com/aws/amazon-braket-sdk-python/issues/). -For other issues or general questions, please ask on the [Quantum Computing Stack Exchange](https://quantumcomputing.stackexchange.com/questions/ask) and add the tag amazon-braket. +the issue on our [Github issue tracker](https://github.com/amazon-braket/amazon-braket-sdk-python/issues/). +For other issues or general questions, please ask on the [Quantum Computing Stack Exchange](https://quantumcomputing.stackexchange.com/questions/ask?Tags=amazon-braket). ### Feedback and Feature Requests If you have feedback or features that you would like to see on Amazon Braket, we would love to hear from you! -[Github issues](https://github.com/aws/amazon-braket-sdk-python/issues/) is our preferred mechanism for collecting feedback and feature requests, allowing other users +[Github issues](https://github.com/amazon-braket/amazon-braket-sdk-python/issues/) is our preferred mechanism for collecting feedback and feature requests, allowing other users to engage in the conversation, and +1 issues to help drive priority. +### Code contributors + +[![Contributors](https://contrib.rocks/image?repo=amazon-braket/amazon-braket-sdk-python)](https://github.com/amazon-braket/amazon-braket-sdk-python/graphs/contributors) + ## License This project is licensed under the Apache-2.0 License. diff --git a/doc/examples-adv-circuits-algorithms.rst b/doc/examples-adv-circuits-algorithms.rst index ea99a475b..b37e87e34 100644 --- a/doc/examples-adv-circuits-algorithms.rst +++ b/doc/examples-adv-circuits-algorithms.rst @@ -7,9 +7,9 @@ Learn more about working with advanced circuits and algoritms. .. toctree:: :maxdepth: 2 -************************************************************************************************************************************************ -`Grover's search algorithm `_ -************************************************************************************************************************************************ +********************************************************************************************************************************************************** +`Grover's search algorithm `_ +********************************************************************************************************************************************************** This tutorial provides a step-by-step walkthrough of Grover's quantum algorithm. You learn how to build the corresponding quantum circuit with simple modular building @@ -17,9 +17,9 @@ blocks using the Amazon Braket SDK. You will learn how to build custom gates that are not part of the basic gate set provided by the SDK. A custom gate can used as a core quantum gate by registering it as a subroutine. -******************************************************************************************************************************************************************************************************** -`Quantum amplitude amplification `_ -******************************************************************************************************************************************************************************************************** +****************************************************************************************************************************************************************************************************************** +`Quantum amplitude amplification `_ +****************************************************************************************************************************************************************************************************************** This tutorial provides a detailed discussion and implementation of the Quantum Amplitude Amplification (QAA) algorithm using the Amazon Braket SDK. QAA is a routine in quantum computing which generalizes the idea behind @@ -29,18 +29,18 @@ target states in a given search space. In a quantum computer, QAA can be used to quadratic speedup over several classical algorithms. -************************************************************************************************************************************************************************************** -`Quantum Fourier transform `_ -************************************************************************************************************************************************************************************** +************************************************************************************************************************************************************************************************ +`Quantum Fourier transform `_ +************************************************************************************************************************************************************************************************ This tutorial provides a detailed implementation of the Quantum Fourier Transform (QFT) and its inverse using Amazon Braket's SDK. The QFT is an important subroutine to many quantum algorithms, most famously Shor's algorithm for factoring and the quantum phase estimation (QPE) algorithm for estimating the eigenvalues of a unitary operator. -*********************************************************************************************************************************************************************************** -`Quantum phase estimation `_ -*********************************************************************************************************************************************************************************** +********************************************************************************************************************************************************************************************* +`Quantum phase estimation `_ +********************************************************************************************************************************************************************************************* This tutorial provides a detailed implementation of the Quantum Phase Estimation (QPE) algorithm using the Amazon Braket SDK. The QPE algorithm is designed to estimate the diff --git a/doc/examples-braket-features.rst b/doc/examples-braket-features.rst index 1bfc9c0d7..75361f172 100644 --- a/doc/examples-braket-features.rst +++ b/doc/examples-braket-features.rst @@ -7,40 +7,40 @@ Learn more about the indivudal features of Amazon Braket. .. toctree:: :maxdepth: 2 -******************************************************************************************************************************************************************************************************************************* -`Getting notifications when a quantum task completes `_ -******************************************************************************************************************************************************************************************************************************* +***************************************************************************************************************************************************************************************************************************************************************** +`Getting notifications when a quantum task completes `_ +***************************************************************************************************************************************************************************************************************************************************************** This tutorial illustrates how Amazon Braket integrates with Amazon EventBridge for event-based processing. In the tutorial, you will learn how to configure Amazon Braket and Amazon Eventbridge to receive text notification about quantum task completions on your phone. -************************************************************************************************************************************************************* -`Allocating Qubits on QPU Devices `_ -************************************************************************************************************************************************************* +*********************************************************************************************************************************************************************** +`Allocating Qubits on QPU Devices `_ +*********************************************************************************************************************************************************************** This tutorial explains how you can use the Amazon Braket SDK to allocate the qubit selection for your circuits manually, when running on QPUs. -***************************************************************************************************************************************************************************************** -`Getting Devices and Checking Device Properties `_ -***************************************************************************************************************************************************************************************** +*************************************************************************************************************************************************************************************************** +`Getting Devices and Checking Device Properties `_ +*************************************************************************************************************************************************************************************************** This example shows how to interact with the Amazon Braket GetDevice API to retrieve Amazon Braket devices (such as simulators and QPUs) programatically, and how to gain access to their properties. -************************************************************************************************************************************************************************* -`Using the tensor network simulator TN1 `_ -************************************************************************************************************************************************************************* +*********************************************************************************************************************************************************************************** +`Using the tensor network simulator TN1 `_ +*********************************************************************************************************************************************************************************** This notebook introduces the Amazon Braket managed tensor network simulator, TN1. You will learn about how TN1 works, how to use it, and which problems are best suited to run on TN1. -*************************************************************************************************************************************************************** -`Simulating noise on Amazon Braket `_ -*************************************************************************************************************************************************************** +************************************************************************************************************************************************************************* +`Simulating noise on Amazon Braket `_ +************************************************************************************************************************************************************************* This notebook provides a detailed overview of noise simulation on Amazon Braket. You will learn how to define noise channels, apply noise to new or existing circuits, and run those circuits diff --git a/doc/examples-getting-started.rst b/doc/examples-getting-started.rst index 9523320c1..8c9eb90f5 100644 --- a/doc/examples-getting-started.rst +++ b/doc/examples-getting-started.rst @@ -7,15 +7,15 @@ Get started on Amazon Braket with some introductory examples. .. toctree:: :maxdepth: 2 -*********************************************************************************************************************************************** -`Getting started `_ -*********************************************************************************************************************************************** +********************************************************************************************************************************************************* +`Getting started `_ +********************************************************************************************************************************************************* A hello-world tutorial that shows you how to build a simple circuit and run it on a local simulator. -******************************************************************************************************************************************************************************************************************** -`Running quantum circuits on simulators `_ -******************************************************************************************************************************************************************************************************************** +****************************************************************************************************************************************************************************************************************************** +`Running quantum circuits on simulators `_ +****************************************************************************************************************************************************************************************************************************** This tutorial prepares a paradigmatic example for a multi-qubit entangled state, the so-called GHZ state (named after the three physicists Greenberger, Horne, and Zeilinger). @@ -26,9 +26,9 @@ and quantum metrology. **Note:** When a circuit is ran using a simulator, customers are required to use contiguous qubits/indices. -*********************************************************************************************************************************************************************************************************************** -`Running quantum circuits on QPU devices `_ -*********************************************************************************************************************************************************************************************************************** +********************************************************************************************************************************************************************************************************************************* +`Running quantum circuits on QPU devices `_ +********************************************************************************************************************************************************************************************************************************* This tutorial prepares a maximally-entangled Bell state between two qubits, for classical simulators and for QPUs. For classical devices, we can run the circuit on a @@ -36,9 +36,9 @@ local simulator or a cloud-based managed simulator. For the quantum devices, we run the circuit on the superconducting machine from Rigetti, and on the ion-trap machine provided by IonQ. -******************************************************************************************************************************************************************************************************************************************** -`Deep Dive into the anatomy of quantum circuits `_ -******************************************************************************************************************************************************************************************************************************************** +****************************************************************************************************************************************************************************************************************************************************** +`Deep Dive into the anatomy of quantum circuits `_ +****************************************************************************************************************************************************************************************************************************************************** This tutorial discusses in detail the anatomy of quantum circuits in the Amazon Braket SDK. You will learn how to build (parameterized) circuits and display them @@ -47,9 +47,9 @@ more about circuit depth and circuit size. Finally you will learn how to execute the circuit on a device of our choice (defining a quantum task) and how to track, log, recover, or cancel a quantum task efficiently. -***************************************************************************************************************************************************** -`Superdense coding `_ -***************************************************************************************************************************************************** +*************************************************************************************************************************************************************** +`Superdense coding `_ +*************************************************************************************************************************************************************** This tutorial constructs an implementation of the superdense coding protocol using the Amazon Braket SDK. Superdense coding is a method of transmitting two classical diff --git a/doc/examples-hybrid-jobs.rst b/doc/examples-hybrid-jobs.rst index 76b2026eb..56a1d9ecf 100644 --- a/doc/examples-hybrid-jobs.rst +++ b/doc/examples-hybrid-jobs.rst @@ -7,27 +7,27 @@ Learn more about hybrid jobs on Amazon Braket. .. toctree:: :maxdepth: 2 -************************************************************************************************************************************************************************************** -`Creating your first Hybrid Job `_ -************************************************************************************************************************************************************************************** +************************************************************************************************************************************************************************************************ +`Creating your first Hybrid Job `_ +************************************************************************************************************************************************************************************************ This tutorial shows how to run your first Amazon Braket Hybrid Job. -*********************************************************************************************************************************************************************************************************************************************************** -`Quantum machine learning in Amazon Braket Hybrid Jobs `_ -*********************************************************************************************************************************************************************************************************************************************************** +********************************************************************************************************************************************************************************************************************************************************************* +`Quantum machine learning in Amazon Braket Hybrid Jobs `_ +********************************************************************************************************************************************************************************************************************************************************************* This notebook demonstrates a typical quantum machine learning workflow, including uploading data, monitoring training, and tuning hyperparameters. -******************************************************************************************************************************************************************************************** -`Using Pennylane with Braket Hybrid Jobs `_ -******************************************************************************************************************************************************************************************** +*************************************************************************************************************************************************************************************************************************** +`Using Pennylane with Braket Hybrid Jobs `_ +*************************************************************************************************************************************************************************************************************************** In this tutorial, we use PennyLane within Amazon Braket Hybrid Jobs to run the Quantum Approximate Optimization Algorithm (QAOA) on a Max-Cut problem. -******************************************************************************************************************************************************************** -`Bring your own container `_ -******************************************************************************************************************************************************************** +****************************************************************************************************************************************************************************** +`Bring your own container `_ +****************************************************************************************************************************************************************************** Amazon Braket has pre-configured containers for executing Amazon Braket Hybrid Jobs, which are sufficient for many use cases involving the Braket SDK and PennyLane. However, if we want to use custom packages outside the scope of pre-configured containers, we have the ability to supply a custom-built container. In this tutorial, we show how to use Braket Hybrid Jobs to train a quantum machine learning model using BYOC (Bring Your Own Container). diff --git a/doc/examples-hybrid-quantum.rst b/doc/examples-hybrid-quantum.rst index b30083af9..9c7f3aca2 100644 --- a/doc/examples-hybrid-quantum.rst +++ b/doc/examples-hybrid-quantum.rst @@ -7,23 +7,23 @@ Learn more about hybrid quantum algorithms. .. toctree:: :maxdepth: 2 -*************************************************************************************************************************** -`QAOA `_ -*************************************************************************************************************************** +************************************************************************************************************************************* +`QAOA `_ +************************************************************************************************************************************* This tutorial shows how to (approximately) solve binary combinatorial optimization problems using the Quantum Approximate Optimization Algorithm (QAOA). -************************************************************************************************************************************************************************** -`VQE Transverse Ising `_ -************************************************************************************************************************************************************************** +************************************************************************************************************************************************************************************ +`VQE Transverse Ising `_ +************************************************************************************************************************************************************************************ This tutorial shows how to solve for the ground state of the Transverse Ising Model using the variational quantum eigenvalue solver (VQE). -****************************************************************************************************************************************************** -`VQE Chemistry `_ -****************************************************************************************************************************************************** +**************************************************************************************************************************************************************** +`VQE Chemistry `_ +**************************************************************************************************************************************************************** This tutorial shows how to implement the Variational Quantum Eigensolver (VQE) algorithm in Amazon Braket SDK to compute the potential energy surface (PES) for the Hydrogen molecule. diff --git a/doc/examples-ml-pennylane.rst b/doc/examples-ml-pennylane.rst index dae859127..5c7db93aa 100644 --- a/doc/examples-ml-pennylane.rst +++ b/doc/examples-ml-pennylane.rst @@ -7,16 +7,16 @@ Learn more about how to combine PennyLane with Amazon Braket. .. toctree:: :maxdepth: 2 -**************************************************************************************************************************************************************** -`Combining PennyLane with Amazon Braket `_ -**************************************************************************************************************************************************************** +************************************************************************************************************************************************************************** +`Combining PennyLane with Amazon Braket `_ +************************************************************************************************************************************************************************** This tutorial shows you how to construct circuits and evaluate their gradients in PennyLane with execution performed using Amazon Braket. -******************************************************************************************************************************************************************************************************************************************* -`Computing gradients in parallel with PennyLane-Braket `_ -******************************************************************************************************************************************************************************************************************************************* +***************************************************************************************************************************************************************************************************************************************************** +`Computing gradients in parallel with PennyLane-Braket `_ +***************************************************************************************************************************************************************************************************************************************************** Learn how to speed up training of quantum circuits by using parallel execution on Amazon Braket. Quantum circuit training involving gradients @@ -25,9 +25,9 @@ The tutorial benchmarks SV1 against a local simulator, showing that SV1 outperfo local simulator for both executions and gradient calculations. This illustrates how parallel capabilities can be combined between PennyLane and SV1. -******************************************************************************************************************************************************************************** -`Graph optimization with QAOA `_ -******************************************************************************************************************************************************************************** +****************************************************************************************************************************************************************************************** +`Graph optimization with QAOA `_ +****************************************************************************************************************************************************************************************** In this tutorial, you learn how quantum circuit training can be applied to a problem of practical relevance in graph optimization. It easy it is to train a QAOA circuit in @@ -36,9 +36,9 @@ then extends to a more difficult 20-node graph and uses the parallel capabilitie the Amazon Braket SV1 simulator to speed up gradient calculations and hence train the quantum circuit faster, using around 1-2 minutes per iteration. -***************************************************************************************************************************************************************************************************** -`Hydrogen Molecule geometry with VQE `_ -***************************************************************************************************************************************************************************************************** +*************************************************************************************************************************************************************************************************************** +`Hydrogen Molecule geometry with VQE `_ +*************************************************************************************************************************************************************************************************************** In this tutorial, you will learn how PennyLane and Amazon Braket can be combined to solve an important problem in quantum chemistry. The ground state energy of molecular hydrogen is calculated diff --git a/doc/examples.rst b/doc/examples.rst index e607920a1..87c2e1f7a 100644 --- a/doc/examples.rst +++ b/doc/examples.rst @@ -3,7 +3,7 @@ Examples ######## There are several examples available in the Amazon Braket repo: -https://github.com/aws/amazon-braket-examples. +https://github.com/amazon-braket/amazon-braket-examples. .. toctree:: :maxdepth: 2 @@ -15,4 +15,4 @@ https://github.com/aws/amazon-braket-examples. examples-ml-pennylane.rst examples-hybrid-jobs.rst - \ No newline at end of file + diff --git a/doc/index.rst b/doc/index.rst index 38dce7284..8d996f4cc 100644 --- a/doc/index.rst +++ b/doc/index.rst @@ -6,7 +6,7 @@ The Amazon Braket Python SDK is an open source library to design and build quant submit them to Amazon Braket devices as quantum tasks, and monitor their execution. This documentation provides information about the Amazon Braket Python SDK library. The project -homepage is in Github https://github.com/aws/amazon-braket-sdk-python. The project +homepage is in Github https://github.com/amazon-braket/amazon-braket-sdk-python. The project includes SDK source, installation instructions, and other information. *************** diff --git a/doc/requirements.txt b/doc/requirements.txt index 6a86be511..b80e6baf0 100644 --- a/doc/requirements.txt +++ b/doc/requirements.txt @@ -1,4 +1,3 @@ -docutils<0.18 -sphinx -sphinx-rtd-theme +sphinx>7 +sphinx-rtd-theme>=1.3.0 sphinxcontrib-apidoc diff --git a/examples/hybrid_job.py b/examples/hybrid_job.py new file mode 100644 index 000000000..2e09d6463 --- /dev/null +++ b/examples/hybrid_job.py @@ -0,0 +1,49 @@ +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"). You +# may not use this file except in compliance with the License. A copy of +# the License is located at +# +# http://aws.amazon.com/apache2.0/ +# +# or in the "license" file accompanying this file. This file is +# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF +# ANY KIND, either express or implied. See the License for the specific +# language governing permissions and limitations under the License. + +from braket.aws import AwsDevice +from braket.circuits import Circuit, FreeParameter, Observable +from braket.devices import Devices +from braket.jobs import get_job_device_arn, hybrid_job +from braket.jobs.metrics import log_metric + + +@hybrid_job(device=Devices.Amazon.SV1, wait_until_complete=True) +def run_hybrid_job(num_tasks=1): + # declare AwsDevice within the hybrid job + device = AwsDevice(get_job_device_arn()) + + # create a parametric circuit + circ = Circuit() + circ.rx(0, FreeParameter("theta")) + circ.cnot(0, 1) + circ.expectation(observable=Observable.X(), target=0) + + # initial parameter + theta = 0.0 + + for i in range(num_tasks): + # run task, specifying input parameter + task = device.run(circ, shots=100, inputs={"theta": theta}) + exp_val = task.result().values[0] + + # modify the parameter (e.g. gradient descent) + theta += exp_val + + log_metric(metric_name="exp_val", value=exp_val, iteration_number=i) + + return {"final_theta": theta, "final_exp_val": exp_val} + + +job = run_hybrid_job(num_tasks=5) +print(job.result()) diff --git a/examples/hybrid_job_script.py b/examples/hybrid_job_script.py new file mode 100644 index 000000000..b544ff9df --- /dev/null +++ b/examples/hybrid_job_script.py @@ -0,0 +1,56 @@ +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"). You +# may not use this file except in compliance with the License. A copy of +# the License is located at +# +# http://aws.amazon.com/apache2.0/ +# +# or in the "license" file accompanying this file. This file is +# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF +# ANY KIND, either express or implied. See the License for the specific +# language governing permissions and limitations under the License. + + +from braket.aws import AwsDevice, AwsQuantumJob +from braket.circuits import Circuit, FreeParameter, Observable +from braket.devices import Devices +from braket.jobs import get_job_device_arn, save_job_result +from braket.jobs.metrics import log_metric + + +def run_hybrid_job(num_tasks: int): + # use the device specified in the hybrid job + device = AwsDevice(get_job_device_arn()) + + # create a parametric circuit + circ = Circuit() + circ.rx(0, FreeParameter("theta")) + circ.cnot(0, 1) + circ.expectation(observable=Observable.X(), target=0) + + # initial parameter + theta = 0.0 + + for i in range(num_tasks): + # run task, specifying input parameter + task = device.run(circ, shots=100, inputs={"theta": theta}) + exp_val = task.result().values[0] + + # modify the parameter (e.g. gradient descent) + theta += exp_val + + log_metric(metric_name="exp_val", value=exp_val, iteration_number=i) + + save_job_result({"final_theta": theta, "final_exp_val": exp_val}) + + +if __name__ == "__main__": + job = AwsQuantumJob.create( + device=Devices.Amazon.SV1, # choose priority device + source_module="hybrid_job_script.py", # specify file or directory with code to run + entry_point="hybrid_job_script:run_hybrid_job", # specify function to run + hyperparameters={"num_tasks": 5}, + wait_until_complete=True, + ) + print(job.result()) diff --git a/examples/job.py b/examples/job.py deleted file mode 100644 index fdb2cec61..000000000 --- a/examples/job.py +++ /dev/null @@ -1,45 +0,0 @@ -# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"). You -# may not use this file except in compliance with the License. A copy of -# the License is located at -# -# http://aws.amazon.com/apache2.0/ -# -# or in the "license" file accompanying this file. This file is -# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF -# ANY KIND, either express or implied. See the License for the specific -# language governing permissions and limitations under the License. - -import os - -from braket.aws import AwsDevice, AwsQuantumJob -from braket.circuits import Circuit -from braket.devices import Devices -from braket.jobs import save_job_result - - -def run_job(): - device = AwsDevice(os.environ.get("AMZN_BRAKET_DEVICE_ARN")) - - bell = Circuit().h(0).cnot(0, 1) - num_tasks = 10 - results = [] - - for i in range(num_tasks): - task = device.run(bell, shots=100) - result = task.result().measurement_counts - results.append(result) - print(f"iter {i}: {result}") - - save_job_result({"results": results}) - - -if __name__ == "__main__": - job = AwsQuantumJob.create( - device=Devices.Amazon.SV1, - source_module="job.py", - entry_point="job:run_job", - wait_until_complete=True, - ) - print(job.result()) diff --git a/setup.cfg b/setup.cfg index 03f3d1acf..94f28ec7d 100644 --- a/setup.cfg +++ b/setup.cfg @@ -12,6 +12,7 @@ testpaths = test/unit_tests line_length = 100 multi_line_output = 3 include_trailing_comma = true +profile = black [flake8] ignore = diff --git a/setup.py b/setup.py index cb4bb778e..eb243c5d9 100644 --- a/setup.py +++ b/setup.py @@ -23,7 +23,7 @@ name="amazon-braket-sdk", version=version, license="Apache License 2.0", - python_requires=">= 3.8.2", + python_requires=">= 3.9", packages=find_namespace_packages(where="src", exclude=("test",)), package_dir={"": "src"}, install_requires=[ @@ -33,7 +33,8 @@ "setuptools", "backoff", "boltons", - "boto3>=1.22.3", + "boto3>=1.28.53", + "cloudpickle==2.2.1", "nest-asyncio", "networkx", "numpy", @@ -62,7 +63,7 @@ ] }, include_package_data=True, - url="https://github.com/aws/amazon-braket-sdk-python", + url="https://github.com/amazon-braket/amazon-braket-sdk-python", author="Amazon Web Services", description=( "An open source library for interacting with quantum computing devices on Amazon Braket" @@ -73,10 +74,11 @@ classifiers=[ "Development Status :: 5 - Production/Stable", "Intended Audience :: Developers", + "Intended Audience :: Education", + "Intended Audience :: Science/Research", "Natural Language :: English", "License :: OSI Approved :: Apache Software License", "Programming Language :: Python", - "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", diff --git a/src/braket/_sdk/_version.py b/src/braket/_sdk/_version.py index 46367d959..c0e18d0a0 100644 --- a/src/braket/_sdk/_version.py +++ b/src/braket/_sdk/_version.py @@ -15,4 +15,4 @@ Version number (major.minor.patch[-label]) """ -__version__ = "1.55.1.dev0" +__version__ = "1.62.2.dev0" diff --git a/src/braket/ahs/analog_hamiltonian_simulation.py b/src/braket/ahs/analog_hamiltonian_simulation.py index 80c4f2f59..b02091d4f 100644 --- a/src/braket/ahs/analog_hamiltonian_simulation.py +++ b/src/braket/ahs/analog_hamiltonian_simulation.py @@ -15,7 +15,6 @@ from collections import defaultdict from functools import singledispatch -from typing import Tuple import braket.ir.ahs as ir from braket.ahs.atom_arrangement import AtomArrangement, SiteType @@ -113,12 +112,12 @@ def discretize(self, device) -> AnalogHamiltonianSimulation: # noqa @singledispatch def _get_term_ir( term: Hamiltonian, -) -> Tuple[str, dict]: +) -> tuple[str, dict]: raise TypeError(f"Unable to convert Hamiltonian term type {type(term)}.") @_get_term_ir.register -def _(term: ShiftingField) -> Tuple[str, ir.ShiftingField]: +def _(term: ShiftingField) -> tuple[str, ir.ShiftingField]: return AnalogHamiltonianSimulation.SHIFTING_FIELDS_PROPERTY, ir.ShiftingField( magnitude=ir.PhysicalField( time_series=ir.TimeSeries( @@ -131,7 +130,7 @@ def _(term: ShiftingField) -> Tuple[str, ir.ShiftingField]: @_get_term_ir.register -def _(term: DrivingField) -> Tuple[str, ir.DrivingField]: +def _(term: DrivingField) -> tuple[str, ir.DrivingField]: return AnalogHamiltonianSimulation.DRIVING_FIELDS_PROPERTY, ir.DrivingField( amplitude=ir.PhysicalField( time_series=ir.TimeSeries( diff --git a/src/braket/ahs/atom_arrangement.py b/src/braket/ahs/atom_arrangement.py index 70fc64b0d..bb7088347 100644 --- a/src/braket/ahs/atom_arrangement.py +++ b/src/braket/ahs/atom_arrangement.py @@ -13,11 +13,12 @@ from __future__ import annotations +from collections.abc import Iterator from dataclasses import dataclass from decimal import Decimal from enum import Enum from numbers import Number -from typing import Iterator, List, Tuple, Union +from typing import Union import numpy as np @@ -33,7 +34,7 @@ class SiteType(Enum): class AtomArrangementItem: """Represents an item (coordinate and metadata) in an atom arrangement.""" - coordinate: Tuple[Number, Number] + coordinate: tuple[Number, Number] site_type: SiteType def _validate_coordinate(self) -> None: @@ -62,13 +63,13 @@ def __init__(self): def add( self, - coordinate: Union[Tuple[Number, Number], np.ndarray], + coordinate: Union[tuple[Number, Number], np.ndarray], site_type: SiteType = SiteType.FILLED, ) -> AtomArrangement: """Add a coordinate to the atom arrangement. Args: - coordinate (Union[Tuple[Number, Number], ndarray]): The coordinate of the + coordinate (Union[tuple[Number, Number], ndarray]): The coordinate of the atom (in meters). The coordinates can be a numpy array of shape (2,) or a tuple of int, float, Decimal site_type (SiteType): The type of site. Optional. Default is FILLED. @@ -78,14 +79,14 @@ def add( self._sites.append(AtomArrangementItem(tuple(coordinate), site_type)) return self - def coordinate_list(self, coordinate_index: Number) -> List[Number]: + def coordinate_list(self, coordinate_index: Number) -> list[Number]: """Returns all the coordinates at the given index. Args: coordinate_index (Number): The index to get for each coordinate. Returns: - List[Number]:The list of coordinates at the given index. + list[Number]:The list of coordinates at the given index. Example: To get a list of all x-coordinates: coordinate_list(0) diff --git a/src/braket/ahs/driving_field.py b/src/braket/ahs/driving_field.py index 45914063a..02c8bd276 100644 --- a/src/braket/ahs/driving_field.py +++ b/src/braket/ahs/driving_field.py @@ -13,7 +13,7 @@ from __future__ import annotations -from typing import List, Union +from typing import Union from braket.ahs.discretization_types import DiscretizationProperties from braket.ahs.field import Field @@ -65,7 +65,7 @@ def __init__( self._detuning = detuning if isinstance(detuning, Field) else Field(detuning) @property - def terms(self) -> List[Hamiltonian]: + def terms(self) -> list[Hamiltonian]: return [self] @property @@ -141,7 +141,7 @@ def discretize(self, properties: DiscretizationProperties) -> DrivingField: @staticmethod def from_lists( - times: List[float], amplitudes: List[float], detunings: List[float], phases: List[float] + times: list[float], amplitudes: list[float], detunings: list[float], phases: list[float] ) -> DrivingField: """ Builds DrivingField Hamiltonian from lists defining time evolution @@ -149,10 +149,10 @@ def from_lists( The values of the parameters at each time points are global for all atoms. Args: - times (List[float]): The time points of the driving field - amplitudes (List[float]): The values of the amplitude - detunings (List[float]): The values of the detuning - phases (List[float]): The values of the phase + times (list[float]): The time points of the driving field + amplitudes (list[float]): The values of the amplitude + detunings (list[float]): The values of the detuning + phases (list[float]): The values of the phase Returns: DrivingField: DrivingField Hamiltonian. diff --git a/src/braket/ahs/hamiltonian.py b/src/braket/ahs/hamiltonian.py index 83b80e69c..548befc58 100644 --- a/src/braket/ahs/hamiltonian.py +++ b/src/braket/ahs/hamiltonian.py @@ -13,13 +13,13 @@ from __future__ import annotations -from typing import List, Optional +from typing import Optional from braket.ahs.discretization_types import DiscretizationProperties class Hamiltonian: - def __init__(self, terms: Optional[List[Hamiltonian]] = None): + def __init__(self, terms: Optional[list[Hamiltonian]] = None): r"""A Hamiltonian representing a system to be simulated. A Hamiltonian :math:`H` may be expressed as a sum of multiple terms @@ -30,8 +30,8 @@ def __init__(self, terms: Optional[List[Hamiltonian]] = None): self._terms = terms or [] @property - def terms(self) -> List[Hamiltonian]: - """List[Hamiltonian]: The list of terms in this Hamiltonian.""" + def terms(self) -> list[Hamiltonian]: + """list[Hamiltonian]: The list of terms in this Hamiltonian.""" return self._terms def discretize(self, properties: DiscretizationProperties) -> Hamiltonian: diff --git a/src/braket/ahs/pattern.py b/src/braket/ahs/pattern.py index ebd1eafc1..17e40a36f 100644 --- a/src/braket/ahs/pattern.py +++ b/src/braket/ahs/pattern.py @@ -15,22 +15,21 @@ from decimal import Decimal from numbers import Number -from typing import List class Pattern: - def __init__(self, series: List[Number]): + def __init__(self, series: list[Number]): """Represents the spatial dependence of a Field. Args: - series (List[Number]): A series of numbers representing the the local + series (list[Number]): A series of numbers representing the the local pattern of real numbers. """ self._series = series @property - def series(self) -> List[Number]: - """List[Number]: A series of numbers representing the local + def series(self) -> list[Number]: + """list[Number]: A series of numbers representing the local pattern of real numbers.""" return self._series diff --git a/src/braket/ahs/shifting_field.py b/src/braket/ahs/shifting_field.py index bd3f59da9..846d7ad2e 100644 --- a/src/braket/ahs/shifting_field.py +++ b/src/braket/ahs/shifting_field.py @@ -13,8 +13,6 @@ from __future__ import annotations -from typing import List - from braket.ahs.discretization_types import DiscretizationProperties from braket.ahs.field import Field from braket.ahs.hamiltonian import Hamiltonian @@ -51,7 +49,7 @@ def __init__(self, magnitude: Field) -> None: self._magnitude = magnitude @property - def terms(self) -> List[Hamiltonian]: + def terms(self) -> list[Hamiltonian]: return [self] @property @@ -62,13 +60,13 @@ def magnitude(self) -> Field: return self._magnitude @staticmethod - def from_lists(times: List[float], values: List[float], pattern: List[float]) -> ShiftingField: + def from_lists(times: list[float], values: list[float], pattern: list[float]) -> ShiftingField: """Get the shifting field from a set of time points, values and pattern Args: - times (List[float]): The time points of the shifting field - values (List[float]): The values of the shifting field - pattern (List[float]): The pattern of the shifting field + times (list[float]): The time points of the shifting field + values (list[float]): The values of the shifting field + pattern (list[float]): The pattern of the shifting field Returns: ShiftingField: The shifting field obtained diff --git a/src/braket/annealing/problem.py b/src/braket/annealing/problem.py index 4959df52e..d8b40c372 100644 --- a/src/braket/annealing/problem.py +++ b/src/braket/annealing/problem.py @@ -37,16 +37,16 @@ class Problem: def __init__( self, problem_type: ProblemType, - linear: Dict[int, float] = None, - quadratic: Dict[Tuple[int, int], float] = None, + linear: Dict[int, float] | None = None, + quadratic: Dict[Tuple[int, int], float] | None = None, ): """ Args: problem_type (ProblemType): The type of annealing problem - linear (Dict[int, float]): The linear terms of this problem, + linear (Dict[int, float] | None): The linear terms of this problem, as a map of variable to coefficient - quadratic (Dict[Tuple[int, int], float]): The quadratic terms of this problem, + quadratic (Dict[Tuple[int, int], float] | None): The quadratic terms of this problem, as a map of variables to coefficient Examples: diff --git a/src/braket/aws/aws_device.py b/src/braket/aws/aws_device.py index fbd493071..0cff446db 100644 --- a/src/braket/aws/aws_device.py +++ b/src/braket/aws/aws_device.py @@ -17,9 +17,10 @@ import json import os import urllib.request +import warnings from datetime import datetime from enum import Enum -from typing import Dict, List, Optional, Tuple, Union +from typing import Optional, Union from botocore.errorfactory import ClientError from networkx import DiGraph, complete_graph, from_edgelist @@ -29,6 +30,7 @@ from braket.aws.aws_quantum_task import AwsQuantumTask from braket.aws.aws_quantum_task_batch import AwsQuantumTaskBatch from braket.aws.aws_session import AwsSession +from braket.aws.queue_information import QueueDepthInfo, QueueType from braket.circuits import Circuit, Gate, QubitSet from braket.circuits.gate_calibrations import GateCalibrations from braket.device_schema import DeviceCapabilities, ExecutionDay, GateModelQpuParadigmProperties @@ -117,8 +119,8 @@ def run( shots: Optional[int] = None, poll_timeout_seconds: float = AwsQuantumTask.DEFAULT_RESULTS_POLL_TIMEOUT, poll_interval_seconds: Optional[float] = None, - inputs: Optional[Dict[str, float]] = None, - gate_definitions: Optional[Dict[Tuple[Gate, QubitSet], PulseSequence]] = None, + inputs: Optional[dict[str, float]] = None, + gate_definitions: Optional[dict[tuple[Gate, QubitSet], PulseSequence]] = None, *aws_quantum_task_args, **aws_quantum_task_kwargs, ) -> AwsQuantumTask: @@ -140,11 +142,11 @@ def run( poll_interval_seconds (Optional[float]): The polling interval for `AwsQuantumTask.result()`, in seconds. Defaults to the ``getTaskPollIntervalMillis`` value specified in ``self.properties.service`` (divided by 1000) if provided, otherwise 1 second. - inputs (Optional[Dict[str, float]]): Inputs to be passed along with the + inputs (Optional[dict[str, float]]): Inputs to be passed along with the IR. If the IR supports inputs, the inputs will be updated with this value. Default: {}. - gate_definitions (Optional[Dict[Tuple[Gate, QubitSet], PulseSequence]]): A - `Dict[Tuple[Gate, QubitSet], PulseSequence]]` for a user defined gate calibration. + gate_definitions (Optional[dict[tuple[Gate, QubitSet], PulseSequence]]): A + `dict[tuple[Gate, QubitSet], PulseSequence]]` for a user defined gate calibration. The calibration is defined for a particular `Gate` on a particular `QubitSet` and is represented by a `PulseSequence`. Default: None. @@ -212,7 +214,7 @@ def run_batch( PulseSequence, AnalogHamiltonianSimulation, ], - List[ + list[ Union[ Circuit, Problem, @@ -229,15 +231,15 @@ def run_batch( max_connections: int = AwsQuantumTaskBatch.MAX_CONNECTIONS_DEFAULT, poll_timeout_seconds: float = AwsQuantumTask.DEFAULT_RESULTS_POLL_TIMEOUT, poll_interval_seconds: float = AwsQuantumTask.DEFAULT_RESULTS_POLL_INTERVAL, - inputs: Optional[Union[Dict[str, float], List[Dict[str, float]]]] = None, - gate_definitions: Optional[Dict[Tuple[Gate, QubitSet], PulseSequence]] = None, + inputs: Optional[Union[dict[str, float], list[dict[str, float]]]] = None, + gate_definitions: Optional[dict[tuple[Gate, QubitSet], PulseSequence]] = None, *aws_quantum_task_args, **aws_quantum_task_kwargs, ) -> AwsQuantumTaskBatch: """Executes a batch of quantum tasks in parallel Args: - task_specifications (Union[Union[Circuit, Problem, OpenQasmProgram, BlackbirdProgram, PulseSequence, AnalogHamiltonianSimulation], List[Union[ Circuit, Problem, OpenQasmProgram, BlackbirdProgram, PulseSequence, AnalogHamiltonianSimulation]]]): # noqa + task_specifications (Union[Union[Circuit, Problem, OpenQasmProgram, BlackbirdProgram, PulseSequence, AnalogHamiltonianSimulation], list[Union[ Circuit, Problem, OpenQasmProgram, BlackbirdProgram, PulseSequence, AnalogHamiltonianSimulation]]]): # noqa Single instance or list of circuits, annealing problems, pulse sequences, or photonics program to run on device. s3_destination_folder (Optional[S3DestinationFolder]): The S3 location to @@ -255,11 +257,11 @@ def run_batch( poll_interval_seconds (float): The polling interval for `AwsQuantumTask.result()`, in seconds. Defaults to the ``getTaskPollIntervalMillis`` value specified in ``self.properties.service`` (divided by 1000) if provided, otherwise 1 second. - inputs (Optional[Union[Dict[str, float], List[Dict[str, float]]]]): Inputs to be + inputs (Optional[Union[dict[str, float], list[dict[str, float]]]]): Inputs to be passed along with the IR. If the IR supports inputs, the inputs will be updated with this value. Default: {}. - gate_definitions (Optional[Dict[Tuple[Gate, QubitSet], PulseSequence]]): A - `Dict[Tuple[Gate, QubitSet], PulseSequence]]` for a user defined gate calibration. + gate_definitions (Optional[dict[tuple[Gate, QubitSet], PulseSequence]]): A + `dict[tuple[Gate, QubitSet], PulseSequence]]` for a user defined gate calibration. The calibration is defined for a particular `Gate` on a particular `QubitSet` and is represented by a `PulseSequence`. Default: None. @@ -529,29 +531,29 @@ def __eq__(self, other): return NotImplemented @property - def frames(self) -> Dict[str, Frame]: - """Returns a Dict mapping frame ids to the frame objects for predefined frames + def frames(self) -> dict[str, Frame]: + """Returns a dict mapping frame ids to the frame objects for predefined frames for this device.""" self._update_pulse_properties() return self._frames or dict() @property - def ports(self) -> Dict[str, Port]: - """Returns a Dict mapping port ids to the port objects for predefined ports + def ports(self) -> dict[str, Port]: + """Returns a dict mapping port ids to the port objects for predefined ports for this device.""" self._update_pulse_properties() return self._ports or dict() @staticmethod def get_devices( - arns: Optional[List[str]] = None, - names: Optional[List[str]] = None, - types: Optional[List[AwsDeviceType]] = None, - statuses: Optional[List[str]] = None, - provider_names: Optional[List[str]] = None, + arns: Optional[list[str]] = None, + names: Optional[list[str]] = None, + types: Optional[list[AwsDeviceType]] = None, + statuses: Optional[list[str]] = None, + provider_names: Optional[list[str]] = None, order_by: str = "name", aws_session: Optional[AwsSession] = None, - ) -> List[AwsDevice]: + ) -> list[AwsDevice]: """ Get devices based on filters and desired ordering. The result is the AND of all the filters `arns`, `names`, `types`, `statuses`, `provider_names`. @@ -562,20 +564,22 @@ def get_devices( >>> AwsDevice.get_devices(types=['SIMULATOR']) Args: - arns (Optional[List[str]]): device ARN list, default is `None` - names (Optional[List[str]]): device name list, default is `None` - types (Optional[List[AwsDeviceType]]): device type list, default is `None` + arns (Optional[list[str]]): device ARN filter, default is `None` + names (Optional[list[str]]): device name filter, default is `None` + types (Optional[list[AwsDeviceType]]): device type filter, default is `None` QPUs will be searched for all regions and simulators will only be searched for the region of the current session. - statuses (Optional[List[str]]): device status list, default is `None` - provider_names (Optional[List[str]]): provider name list, default is `None` + statuses (Optional[list[str]]): device status filter, default is `None`. When `None` + is used, RETIRED devices will not be returned. To include RETIRED devices in + the results, use a filter that includes "RETIRED" for this parameter. + provider_names (Optional[list[str]]): provider name filter, default is `None` order_by (str): field to order result by, default is `name`. Accepted values are ['arn', 'name', 'type', 'provider_name', 'status'] aws_session (Optional[AwsSession]): An AWS session object. Default is `None`. Returns: - List[AwsDevice]: list of AWS devices + list[AwsDevice]: list of AWS devices """ if order_by not in AwsDevice._GET_DEVICES_ORDER_BY_KEYS: @@ -601,23 +605,32 @@ def get_devices( types_for_region = sorted( types if region == session_region else types - {AwsDeviceType.SIMULATOR} ) - region_device_arns = [ - result["deviceArn"] - for result in session_for_region.search_devices( - arns=arns, - names=names, - types=types_for_region, - statuses=statuses, - provider_names=provider_names, + try: + region_device_arns = [ + result["deviceArn"] + for result in session_for_region.search_devices( + arns=arns, + names=names, + types=types_for_region, + statuses=statuses, + provider_names=provider_names, + ) + ] + device_map.update( + { + arn: AwsDevice(arn, session_for_region) + for arn in region_device_arns + if arn not in device_map + } ) - ] - device_map.update( - { - arn: AwsDevice(arn, session_for_region) - for arn in region_device_arns - if arn not in device_map - } - ) + except ClientError as e: + error_code = e.response["Error"]["Code"] + warnings.warn( + f"{error_code}: Unable to search region '{region}' for devices." + " Please check your settings or try again later." + f" Continuing without devices in '{region}'." + ) + devices = list(device_map.values()) devices.sort(key=lambda x: getattr(x, order_by)) return devices @@ -667,6 +680,54 @@ def get_device_region(device_arn: str) -> str: "see 'https://docs.aws.amazon.com/braket/latest/developerguide/braket-devices.html'" ) + def queue_depth(self) -> QueueDepthInfo: + """ + Task queue depth refers to the total number of quantum tasks currently waiting + to run on a particular device. + + Returns: + QueueDepthInfo: Instance of the QueueDepth class representing queue depth + information for quantum tasks and hybrid jobs. + Queue depth refers to the number of quantum tasks and hybrid jobs queued on a particular + device. The normal tasks refers to the quantum tasks not submitted via Hybrid Jobs. + Whereas, the priority tasks refers to the total number of quantum tasks waiting to run + submitted through Amazon Braket Hybrid Jobs. These tasks run before the normal tasks. + If the queue depth for normal or priority quantum tasks is greater than 4000, we display + their respective queue depth as '>4000'. Similarly, for hybrid jobs if there are more + than 1000 jobs queued on a device, display the hybrid jobs queue depth as '>1000'. + Additionally, for QPUs if hybrid jobs queue depth is 0, we display information about + priority and count of the running hybrid job. + + Example: + Queue depth information for a running job. + >>> device = AwsDevice(Device.Amazon.SV1) + >>> print(device.queue_depth()) + QueueDepthInfo(quantum_tasks={: '0', + : '1'}, jobs='0 (1 prioritized job(s) running)') + + If more than 4000 quantum tasks queued on a device. + >>> device = AwsDevice(Device.Amazon.DM1) + >>> print(device.queue_depth()) + QueueDepthInfo(quantum_tasks={: '>4000', + : '2000'}, jobs='100') + """ + metadata = self.aws_session.get_device(arn=self.arn) + queue_metadata = metadata.get("deviceQueueInfo") + queue_info = {} + + for response in queue_metadata: + queue_name = response.get("queue") + queue_priority = response.get("queuePriority") + queue_size = response.get("queueSize") + + if queue_name == "QUANTUM_TASKS_QUEUE": + priority_enum = QueueType(queue_priority) + queue_info.setdefault("quantum_tasks", {})[priority_enum] = queue_size + else: + queue_info["jobs"] = queue_size + + return QueueDepthInfo(**queue_info) + def refresh_gate_calibrations(self) -> Optional[GateCalibrations]: """ Refreshes the gate calibration data upon request. @@ -700,7 +761,7 @@ def refresh_gate_calibrations(self) -> Optional[GateCalibrations]: else: return None - def _parse_waveforms(self, waveforms_json: Dict) -> Dict: + def _parse_waveforms(self, waveforms_json: dict) -> dict: waveforms = dict() for waveform in waveforms_json: parsed_waveform = _parse_waveform_from_calibration_schema(waveforms_json[waveform]) @@ -708,24 +769,24 @@ def _parse_waveforms(self, waveforms_json: Dict) -> Dict: return waveforms def _parse_pulse_sequence( - self, calibration: Dict, waveforms: Dict[ArbitraryWaveform] + self, calibration: dict, waveforms: dict[ArbitraryWaveform] ) -> PulseSequence: return PulseSequence._parse_from_calibration_schema(calibration, waveforms, self.frames) def _parse_calibration_json( - self, calibration_data: Dict - ) -> Dict[Tuple[Gate, QubitSet], PulseSequence]: + self, calibration_data: dict + ) -> dict[tuple[Gate, QubitSet], PulseSequence]: """ Takes the json string from the device calibration URL and returns a structured dictionary of - corresponding `Dict[Tuple[Gate, QubitSet], PulseSequence]` to represent the calibration data. + corresponding `dict[tuple[Gate, QubitSet], PulseSequence]` to represent the calibration data. Args: - calibration_data (Dict): The data to be parsed. Based on + calibration_data (dict): The data to be parsed. Based on https://github.com/aws/amazon-braket-schemas-python/blob/main/src/braket/device_schema/pulse/native_gate_calibrations_v1.py. Returns: - Dict[Tuple[Gate, QubitSet], PulseSequence]: The - structured data based on a mapping of `Tuple[Gate, Qubit]` to its calibration repesented as a + dict[tuple[Gate, QubitSet], PulseSequence]: The + structured data based on a mapping of `tuple[Gate, Qubit]` to its calibration repesented as a `PulseSequence`. """ # noqa: E501 diff --git a/src/braket/aws/aws_quantum_job.py b/src/braket/aws/aws_quantum_job.py index 288f17257..8ae4bc129 100644 --- a/src/braket/aws/aws_quantum_job.py +++ b/src/braket/aws/aws_quantum_job.py @@ -20,13 +20,14 @@ from enum import Enum from logging import Logger, getLogger from pathlib import Path -from typing import Any, Dict, List, Union +from typing import Any import boto3 from botocore.exceptions import ClientError from braket.aws import AwsDevice from braket.aws.aws_session import AwsSession +from braket.aws.queue_information import HybridJobQueueInfo from braket.jobs import logs from braket.jobs.config import ( CheckpointConfig, @@ -35,6 +36,7 @@ S3DataSourceConfig, StoppingCondition, ) +from braket.jobs.data_persistence import load_job_result from braket.jobs.metrics_data.cwl_insights_metrics_fetcher import CwlInsightsMetricsFetcher # TODO: Have added metric file in metrics folder, but have to decide on the name for keep @@ -42,8 +44,6 @@ from braket.jobs.metrics_data.definitions import MetricStatistic, MetricType from braket.jobs.quantum_job import QuantumJob from braket.jobs.quantum_job_creation import prepare_quantum_job -from braket.jobs.serialization import deserialize_values -from braket.jobs_data import PersistedJobData class AwsQuantumJob(QuantumJob): @@ -64,68 +64,70 @@ def create( cls, device: str, source_module: str, - entry_point: str = None, - image_uri: str = None, - job_name: str = None, - code_location: str = None, - role_arn: str = None, + entry_point: str | None = None, + image_uri: str | None = None, + job_name: str | None = None, + code_location: str | None = None, + role_arn: str | None = None, wait_until_complete: bool = False, - hyperparameters: Dict[str, Any] = None, - input_data: Union[str, Dict, S3DataSourceConfig] = None, - instance_config: InstanceConfig = None, - distribution: str = None, - stopping_condition: StoppingCondition = None, - output_data_config: OutputDataConfig = None, - copy_checkpoints_from_job: str = None, - checkpoint_config: CheckpointConfig = None, - aws_session: AwsSession = None, - tags: Dict[str, str] = None, + hyperparameters: dict[str, Any] | None = None, + input_data: str | dict | S3DataSourceConfig | None = None, + instance_config: InstanceConfig | None = None, + distribution: str | None = None, + stopping_condition: StoppingCondition | None = None, + output_data_config: OutputDataConfig | None = None, + copy_checkpoints_from_job: str | None = None, + checkpoint_config: CheckpointConfig | None = None, + aws_session: AwsSession | None = None, + tags: dict[str, str] | None = None, logger: Logger = getLogger(__name__), ) -> AwsQuantumJob: """Creates a hybrid job by invoking the Braket CreateJob API. Args: - device (str): ARN for the AWS device which is primarily accessed for the execution - of this hybrid job. Alternatively, a string of the format - "local:/" for using a local simulator for the hybrid job. - This string will be available as the environment variable `AMZN_BRAKET_DEVICE_ARN` - inside the hybrid job container when using a Braket container. + device (str): Device ARN of the QPU device that receives priority quantum + task queueing once the hybrid job begins running. Each QPU has a separate hybrid + jobs queue so that only one hybrid job is running at a time. The device string is + accessible in the hybrid job instance as the environment variable + "AMZN_BRAKET_DEVICE_ARN". When using embedded simulators, you may provide the device + argument as a string of the form: "local:/". source_module (str): Path (absolute, relative or an S3 URI) to a python module to be tarred and uploaded. If `source_module` is an S3 URI, it must point to a tar.gz file. Otherwise, source_module may be a file or directory. - entry_point (str): A str that specifies the entry point of the hybrid job, relative to - the source module. The entry point must be in the format + entry_point (str | None): A str that specifies the entry point of the hybrid job, + relative to the source module. The entry point must be in the format `importable.module` or `importable.module:callable`. For example, `source_module.submodule:start_here` indicates the `start_here` function contained in `source_module.submodule`. If source_module is an S3 URI, entry point must be given. Default: source_module's name - image_uri (str): A str that specifies the ECR image to use for executing the hybrid job. - `image_uris.retrieve_image()` function may be used for retrieving the ECR image URIs - for the containers supported by Braket. Default = ``. + image_uri (str | None): A str that specifies the ECR image to use for executing the + hybrid job. `image_uris.retrieve_image()` function may be used for retrieving the + ECR image URIs for the containers supported by Braket. + Default = ``. - job_name (str): A str that specifies the name with which the hybrid job is created. - Allowed pattern for hybrid job name: `^[a-zA-Z0-9](-*[a-zA-Z0-9]){0,50}$` + job_name (str | None): A str that specifies the name with which the hybrid job is + created. Allowed pattern for hybrid job name: `^[a-zA-Z0-9](-*[a-zA-Z0-9]){0,50}$` Default: f'{image_uri_type}-{timestamp}'. - code_location (str): The S3 prefix URI where custom code will be uploaded. + code_location (str | None): The S3 prefix URI where custom code will be uploaded. Default: f's3://{default_bucket_name}/jobs/{job_name}/script'. - role_arn (str): A str providing the IAM role ARN used to execute the + role_arn (str | None): A str providing the IAM role ARN used to execute the script. Default: IAM role returned by AwsSession's `get_default_jobs_role()`. wait_until_complete (bool): `True` if we should wait until the hybrid job completes. This would tail the hybrid job logs as it waits. Otherwise `False`. Default: `False`. - hyperparameters (Dict[str, Any]): Hyperparameters accessible to the hybrid job. - The hyperparameters are made accessible as a Dict[str, str] to the hybrid job. + hyperparameters (dict[str, Any] | None): Hyperparameters accessible to the hybrid job. + The hyperparameters are made accessible as a dict[str, str] to the hybrid job. For convenience, this accepts other types for keys and values, but `str()` is called to convert them before being passed on. Default: None. - input_data (Union[str, Dict, S3DataSourceConfig]): Information about the training + input_data (str | dict | S3DataSourceConfig | None): Information about the training data. Dictionary maps channel names to local paths or S3 URIs. Contents found at any local paths will be uploaded to S3 at f's3://{default_bucket_name}/jobs/{job_name}/data/{channel_name}. If a local @@ -133,39 +135,40 @@ def create( channel name "input". Default: {}. - instance_config (InstanceConfig): Configuration of the instances to be used - to execute the hybrid job. Default: InstanceConfig(instanceType='ml.m5.large', - instanceCount=1, volumeSizeInGB=30). + instance_config (InstanceConfig | None): Configuration of the instance(s) for running + the classical code for the hybrid job. Default: + `InstanceConfig(instanceType='ml.m5.large', instanceCount=1, volumeSizeInGB=30)`. - distribution (str): A str that specifies how the hybrid job should be distributed. - If set to "data_parallel", the hyperparameters for the hybrid job will be set - to use data parallelism features for PyTorch or TensorFlow. Default: None. + distribution (str | None): A str that specifies how the hybrid job should be + distributed. If set to "data_parallel", the hyperparameters for the hybrid job will + be set to use data parallelism features for PyTorch or TensorFlow. Default: None. - stopping_condition (StoppingCondition): The maximum length of time, in seconds, + stopping_condition (StoppingCondition | None): The maximum length of time, in seconds, and the maximum number of quantum tasks that a hybrid job can run before being forcefully stopped. Default: StoppingCondition(maxRuntimeInSeconds=5 * 24 * 60 * 60). - output_data_config (OutputDataConfig): Specifies the location for the output of the - hybrid job. + output_data_config (OutputDataConfig | None): Specifies the location for the output of + the hybrid job. Default: OutputDataConfig(s3Path=f's3://{default_bucket_name}/jobs/{job_name}/data', kmsKeyId=None). - copy_checkpoints_from_job (str): A str that specifies the hybrid job ARN whose + copy_checkpoints_from_job (str | None): A str that specifies the hybrid job ARN whose checkpoint you want to use in the current hybrid job. Specifying this value will copy over the checkpoint data from `use_checkpoints_from_job`'s checkpoint_config s3Uri to the current hybrid job's checkpoint_config s3Uri, making it available at checkpoint_config.localPath during the hybrid job execution. Default: None - checkpoint_config (CheckpointConfig): Configuration that specifies the location where - checkpoint data is stored. + checkpoint_config (CheckpointConfig | None): Configuration that specifies the location + where checkpoint data is stored. Default: CheckpointConfig(localPath='/opt/jobs/checkpoints', s3Uri=f's3://{default_bucket_name}/jobs/{job_name}/checkpoints'). - aws_session (AwsSession): AwsSession for connecting to AWS Services. + aws_session (AwsSession | None): AwsSession for connecting to AWS Services. Default: AwsSession() - tags (Dict[str, str]): Dict specifying the key-value pairs for tagging this hybrid job. + tags (dict[str, str] | None): Dict specifying the key-value pairs for tagging this + hybrid job. Default: {}. logger (Logger): Logger object with which to write logs, such as quantum task statuses @@ -209,11 +212,11 @@ def create( return job - def __init__(self, arn: str, aws_session: AwsSession = None): + def __init__(self, arn: str, aws_session: AwsSession | None = None): """ Args: arn (str): The ARN of the hybrid job. - aws_session (AwsSession): The `AwsSession` for connecting to AWS services. + aws_session (AwsSession | None): The `AwsSession` for connecting to AWS services. Default is `None`, in which case an `AwsSession` object will be created with the region of the hybrid job. """ @@ -234,7 +237,7 @@ def _is_valid_aws_session_region_for_job_arn(aws_session: AwsSession, job_arn: s bool: `True` when the aws_session region matches the job_arn region; otherwise `False`. """ job_region = job_arn.split(":")[3] - return job_region == aws_session.braket_client.meta.region_name + return job_region == aws_session.region @staticmethod def _default_session_for_job_arn(job_arn: str) -> AwsSession: @@ -278,6 +281,38 @@ def state(self, use_cached_value: bool = False) -> str: """ return self.metadata(use_cached_value).get("status") + def queue_position(self) -> HybridJobQueueInfo: + """ + The queue position details for the hybrid job. + + Returns: + HybridJobQueueInfo: Instance of HybridJobQueueInfo class representing + the queue position information for the hybrid job. The queue_position is + only returned when the hybrid job is not in RUNNING/CANCELLING/TERMINAL states, + else queue_position is returned as None. If the queue position of the hybrid + job is greater than 15, we return '>15' as the queue_position return value. + + Examples: + job status = QUEUED and position is 2 in the queue. + >>> job.queue_position() + HybridJobQueueInfo(queue_position='2', message=None) + + job status = QUEUED and position is 18 in the queue. + >>> job.queue_position() + HybridJobQueueInfo(queue_position='>15', message=None) + + job status = COMPLETED + >>> job.queue_position() + HybridJobQueueInfo(queue_position=None, + message='Job is in COMPLETED status. AmazonBraket does + not show queue position for this status.') + """ + response = self.metadata()["queueInfo"] + queue_position = None if response.get("position") == "None" else response.get("position") + message = response.get("message") + + return HybridJobQueueInfo(queue_position=queue_position, message=message) + def logs(self, wait: bool = False, poll_interval_seconds: int = 5) -> None: """Display logs for a given hybrid job, optionally tailing them until hybrid job is complete. @@ -353,7 +388,7 @@ def logs(self, wait: bool = False, poll_interval_seconds: int = 5) -> None: elif self.state() in AwsQuantumJob.TERMINAL_STATES: log_state = AwsQuantumJob.LogState.JOB_COMPLETE - def metadata(self, use_cached_value: bool = False) -> Dict[str, Any]: + def metadata(self, use_cached_value: bool = False) -> dict[str, Any]: """Gets the hybrid job metadata defined in Amazon Braket. Args: @@ -362,7 +397,7 @@ def metadata(self, use_cached_value: bool = False) -> Dict[str, Any]: `GetJob` is called to retrieve the metadata. If `False`, always calls `GetJob`, which also updates the cached value. Default: `False`. Returns: - Dict[str, Any]: Dict that specifies the hybrid job metadata defined in Amazon Braket. + dict[str, Any]: Dict that specifies the hybrid job metadata defined in Amazon Braket. """ if not use_cached_value or not self._metadata: self._metadata = self._aws_session.get_job(self._arn) @@ -372,7 +407,7 @@ def metrics( self, metric_type: MetricType = MetricType.TIMESTAMP, statistic: MetricStatistic = MetricStatistic.MAX, - ) -> Dict[str, List[Any]]: + ) -> dict[str, list[Any]]: """Gets all the metrics data, where the keys are the column names, and the values are a list containing the values in each row. For example, the table: timestamp energy @@ -389,7 +424,7 @@ def metrics( when there is a conflict. Default: MetricStatistic.MAX. Returns: - Dict[str, List[Any]] : The metrics data. + dict[str, list[Any]] : The metrics data. """ fetcher = CwlInsightsMetricsFetcher(self._aws_session) metadata = self.metadata(True) @@ -418,7 +453,7 @@ def result( self, poll_timeout_seconds: float = QuantumJob.DEFAULT_RESULTS_POLL_TIMEOUT, poll_interval_seconds: float = QuantumJob.DEFAULT_RESULTS_POLL_INTERVAL, - ) -> Dict[str, Any]: + ) -> dict[str, Any]: """Retrieves the hybrid job result persisted using save_job_result() function. Args: @@ -428,7 +463,7 @@ def result( Default: 5 seconds. Returns: - Dict[str, Any]: Dict specifying the job results. + dict[str, Any]: Dict specifying the job results. Raises: RuntimeError: if hybrid job is in a FAILED or CANCELLED state. @@ -448,20 +483,12 @@ def result( return AwsQuantumJob._read_and_deserialize_results(temp_dir, job_name) @staticmethod - def _read_and_deserialize_results(temp_dir: str, job_name: str) -> Dict[str, Any]: - try: - with open(f"{temp_dir}/{job_name}/{AwsQuantumJob.RESULTS_FILENAME}", "r") as f: - persisted_data = PersistedJobData.parse_raw(f.read()) - deserialized_data = deserialize_values( - persisted_data.dataDictionary, persisted_data.dataFormat - ) - return deserialized_data - except FileNotFoundError: - return {} + def _read_and_deserialize_results(temp_dir: str, job_name: str) -> dict[str, Any]: + return load_job_result(Path(temp_dir, job_name, AwsQuantumJob.RESULTS_FILENAME)) def download_result( self, - extract_to: str = None, + extract_to: str | None = None, poll_timeout_seconds: float = QuantumJob.DEFAULT_RESULTS_POLL_TIMEOUT, poll_interval_seconds: float = QuantumJob.DEFAULT_RESULTS_POLL_INTERVAL, ) -> None: @@ -470,7 +497,7 @@ def download_result( the results are extracted to the current directory. Args: - extract_to (str): The directory to which the results are extracted. The results + extract_to (str | None): The directory to which the results are extracted. The results are extracted to a folder titled with the hybrid job name within this directory. Default= `Current working directory`. poll_timeout_seconds (float): The polling timeout, in seconds, for `download_result()`. @@ -541,9 +568,7 @@ def __hash__(self) -> int: return hash(self.arn) @staticmethod - def _initialize_session( - session_value: AwsSession, device: AwsDevice, logger: Logger - ) -> AwsSession: + def _initialize_session(session_value: AwsSession, device: str, logger: Logger) -> AwsSession: aws_session = session_value or AwsSession() if device.startswith("local:"): return aws_session diff --git a/src/braket/aws/aws_quantum_task.py b/src/braket/aws/aws_quantum_task.py index a4538cdd9..fa58d911d 100644 --- a/src/braket/aws/aws_quantum_task.py +++ b/src/braket/aws/aws_quantum_task.py @@ -17,13 +17,14 @@ import time from functools import singledispatch from logging import Logger, getLogger -from typing import Any, Dict, Optional, Tuple, Union +from typing import Any, Optional, Union import boto3 from braket.ahs.analog_hamiltonian_simulation import AnalogHamiltonianSimulation from braket.annealing.problem import Problem from braket.aws.aws_session import AwsSession +from braket.aws.queue_information import QuantumTaskQueueInfo, QueueType from braket.circuits import Instruction from braket.circuits.circuit import Circuit, Gate, QubitSet from braket.circuits.circuit_helpers import validate_circuit_and_shots @@ -99,11 +100,11 @@ def create( ], s3_destination_folder: AwsSession.S3DestinationFolder, shots: int, - device_parameters: Dict[str, Any] = None, + device_parameters: dict[str, Any] | None = None, disable_qubit_rewiring: bool = False, - tags: Dict[str, str] = None, - inputs: Dict[str, float] = None, - gate_definitions: Optional[Dict[Tuple[Gate, QubitSet], PulseSequence]] = None, + tags: dict[str, str] | None = None, + inputs: dict[str, float] | None = None, + gate_definitions: Optional[dict[tuple[Gate, QubitSet], PulseSequence]] | None = None, *args, **kwargs, ) -> AwsQuantumTask: @@ -128,7 +129,7 @@ def create( `shots=0` is only available on simulators and means that the simulator will compute the exact results based on the quantum task specification. - device_parameters (Dict[str, Any]): Additional parameters to send to the device. + device_parameters (dict[str, Any] | None): Additional parameters to send to the device. disable_qubit_rewiring (bool): Whether to run the circuit with the exact qubits chosen, without any rewiring downstream, if this is supported by the device. @@ -136,15 +137,15 @@ def create( If ``True``, no qubit rewiring is allowed; if ``False``, qubit rewiring is allowed. Default: False - tags (Dict[str, str]): Tags, which are Key-Value pairs to add to this quantum task. - An example would be: + tags (dict[str, str] | None): Tags, which are Key-Value pairs to add to this quantum + task. An example would be: `{"state": "washington"}` - inputs (Dict[str, float]): Inputs to be passed along with the + inputs (dict[str, float] | None): Inputs to be passed along with the IR. If the IR supports inputs, the inputs will be updated with this value. Default: {}. - gate_definitions (Optional[Dict[Tuple[Gate, QubitSet], PulseSequence]]): + gate_definitions (Optional[dict[tuple[Gate, QubitSet], PulseSequence]] | None): A `Dict` for user defined gate calibration. The calibration is defined for for a particular `Gate` on a particular `QubitSet` and is represented by a `PulseSequence`. @@ -202,7 +203,7 @@ def create( def __init__( self, arn: str, - aws_session: AwsSession = None, + aws_session: AwsSession | None = None, poll_timeout_seconds: float = DEFAULT_RESULTS_POLL_TIMEOUT, poll_interval_seconds: float = DEFAULT_RESULTS_POLL_INTERVAL, logger: Logger = getLogger(__name__), @@ -210,7 +211,7 @@ def __init__( """ Args: arn (str): The ARN of the quantum task. - aws_session (AwsSession): The `AwsSession` for connecting to AWS services. + aws_session (AwsSession | None): The `AwsSession` for connecting to AWS services. Default is `None`, in which case an `AwsSession` object will be created with the region of the quantum task. poll_timeout_seconds (float): The polling timeout for `result()`. Default: 5 days. @@ -240,7 +241,7 @@ def __init__( self._logger = logger - self._metadata: Dict[str, Any] = {} + self._metadata: dict[str, Any] = {} self._result: Union[ GateModelQuantumTaskResult, AnnealingQuantumTaskResult, PhotonicModelQuantumTaskResult ] = None @@ -277,7 +278,7 @@ def cancel(self) -> None: self._cancel_future() self._aws_session.cancel_quantum_task(self._arn) - def metadata(self, use_cached_value: bool = False) -> Dict[str, Any]: + def metadata(self, use_cached_value: bool = False) -> dict[str, Any]: """ Get quantum task metadata defined in Amazon Braket. @@ -287,7 +288,7 @@ def metadata(self, use_cached_value: bool = False) -> Dict[str, Any]: `GetQuantumTask` will be called to retrieve the metadata. If `False`, always calls `GetQuantumTask`, which also updates the cached value. Default: `False`. Returns: - Dict[str, Any]: The response from the Amazon Braket `GetQuantumTask` operation. + dict[str, Any]: The response from the Amazon Braket `GetQuantumTask` operation. If `use_cached_value` is `True`, Amazon Braket is not called and the most recently retrieved value is used, unless `GetQuantumTask` was never called, in which case it wil still be called to populate the metadata for the first time. @@ -314,6 +315,40 @@ def state(self, use_cached_value: bool = False) -> str: """ return self._status(use_cached_value) + def queue_position(self) -> QuantumTaskQueueInfo: + """ + The queue position details for the quantum task. + + Returns: + QuantumTaskQueueInfo: Instance of QuantumTaskQueueInfo class + representing the queue position information for the quantum task. + The queue_position is only returned when quantum task is not in + RUNNING/CANCELLING/TERMINAL states, else queue_position is returned as None. + The normal tasks refers to the quantum tasks not submitted via Hybrid Jobs. + Whereas, the priority tasks refers to the total number of quantum tasks waiting to run + submitted through Amazon Braket Hybrid Jobs. These tasks run before the normal tasks. + If the queue position for normal or priority quantum tasks is greater than 2000, + we display their respective queue position as '>2000'. + + Examples: + task status = QUEUED and queue position is 2050 + >>> task.queue_position() + QuantumTaskQueueInfo(queue_type=, + queue_position='>2000', message=None) + + task status = COMPLETED + >>> task.queue_position() + QuantumTaskQueueInfo(queue_type=, + queue_position=None, message='Task is in COMPLETED status. AmazonBraket does + not show queue position for this status.') + """ + response = self.metadata()["queueInfo"] + queue_type = QueueType(response["queuePriority"]) + queue_position = None if response.get("position") == "None" else response.get("position") + message = response.get("message") + + return QuantumTaskQueueInfo(queue_type, queue_position, message) + def _status(self, use_cached_value: bool = False) -> str: metadata = self.metadata(use_cached_value) status = metadata.get("status") @@ -479,12 +514,12 @@ def __hash__(self) -> int: def _create_internal( task_specification: Union[Circuit, Problem, BlackbirdProgram], aws_session: AwsSession, - create_task_kwargs: Dict[str, Any], + create_task_kwargs: dict[str, Any], device_arn: str, device_parameters: Union[dict, BraketSchemaBase], disable_qubit_rewiring: bool, - inputs: Dict[str, float], - gate_definitions: Optional[Dict[Tuple[Gate, QubitSet], PulseSequence]], + inputs: dict[str, float], + gate_definitions: Optional[dict[tuple[Gate, QubitSet], PulseSequence]], *args, **kwargs, ) -> AwsQuantumTask: @@ -495,12 +530,12 @@ def _create_internal( def _( pulse_sequence: PulseSequence, aws_session: AwsSession, - create_task_kwargs: Dict[str, Any], + create_task_kwargs: dict[str, Any], device_arn: str, _device_parameters: Union[dict, BraketSchemaBase], # Not currently used for OpenQasmProgram _disable_qubit_rewiring: bool, - inputs: Dict[str, float], - gate_definitions: Optional[Dict[Tuple[Gate, QubitSet], PulseSequence]], + inputs: dict[str, float], + gate_definitions: Optional[dict[tuple[Gate, QubitSet], PulseSequence]], *args, **kwargs, ) -> AwsQuantumTask: @@ -513,12 +548,12 @@ def _( def _( openqasm_program: OpenQASMProgram, aws_session: AwsSession, - create_task_kwargs: Dict[str, Any], + create_task_kwargs: dict[str, Any], device_arn: str, device_parameters: Union[dict, BraketSchemaBase], _disable_qubit_rewiring: bool, - inputs: Dict[str, float], - gate_definitions: Optional[Dict[Tuple[Gate, QubitSet], PulseSequence]], + inputs: dict[str, float], + gate_definitions: Optional[dict[tuple[Gate, QubitSet], PulseSequence]], *args, **kwargs, ) -> AwsQuantumTask: @@ -552,12 +587,12 @@ def _( def _( blackbird_program: BlackbirdProgram, aws_session: AwsSession, - create_task_kwargs: Dict[str, any], + create_task_kwargs: dict[str, any], device_arn: str, _device_parameters: Union[dict, BraketSchemaBase], _disable_qubit_rewiring: bool, - inputs: Dict[str, float], - gate_definitions: Optional[Dict[Tuple[Gate, QubitSet], PulseSequence]], + inputs: dict[str, float], + gate_definitions: Optional[dict[tuple[Gate, QubitSet], PulseSequence]], *args, **kwargs, ) -> AwsQuantumTask: @@ -570,12 +605,12 @@ def _( def _( circuit: Circuit, aws_session: AwsSession, - create_task_kwargs: Dict[str, Any], + create_task_kwargs: dict[str, Any], device_arn: str, device_parameters: Union[dict, BraketSchemaBase], disable_qubit_rewiring: bool, - inputs: Dict[str, float], - gate_definitions: Optional[Dict[Tuple[Gate, QubitSet], PulseSequence]], + inputs: dict[str, float], + gate_definitions: Optional[dict[tuple[Gate, QubitSet], PulseSequence]], *args, **kwargs, ) -> AwsQuantumTask: @@ -633,7 +668,7 @@ def _( def _( problem: Problem, aws_session: AwsSession, - create_task_kwargs: Dict[str, Any], + create_task_kwargs: dict[str, Any], device_arn: str, device_parameters: Union[ dict, @@ -642,8 +677,8 @@ def _( Dwave2000QDeviceParameters, ], _, - inputs: Dict[str, float], - gate_definitions: Optional[Dict[Tuple[Gate, QubitSet], PulseSequence]], + inputs: dict[str, float], + gate_definitions: Optional[dict[tuple[Gate, QubitSet], PulseSequence]], *args, **kwargs, ) -> AwsQuantumTask: @@ -663,12 +698,12 @@ def _( def _( analog_hamiltonian_simulation: AnalogHamiltonianSimulation, aws_session: AwsSession, - create_task_kwargs: Dict[str, Any], + create_task_kwargs: dict[str, Any], device_arn: str, device_parameters: dict, _, - inputs: Dict[str, float], - gate_definitions: Optional[Dict[Tuple[Gate, QubitSet], PulseSequence]], + inputs: dict[str, float], + gate_definitions: Optional[dict[tuple[Gate, QubitSet], PulseSequence]], *args, **kwargs, ) -> AwsQuantumTask: @@ -697,12 +732,12 @@ def _circuit_device_params_from_dict( def _create_annealing_device_params( - device_params: Dict[str, Any], device_arn: str + device_params: dict[str, Any], device_arn: str ) -> Union[DwaveAdvantageDeviceParameters, Dwave2000QDeviceParameters]: """Gets Annealing Device Parameters. Args: - device_params (Dict[str, Any]): Additional parameters for the device. + device_params (dict[str, Any]): Additional parameters for the device. device_arn (str): The ARN of the quantum device. Returns: @@ -739,7 +774,7 @@ def _create_annealing_device_params( def _create_common_params( device_arn: str, s3_destination_folder: AwsSession.S3DestinationFolder, shots: int -) -> Dict[str, Any]: +) -> dict[str, Any]: return { "deviceArn": device_arn, "outputS3Bucket": s3_destination_folder[0], diff --git a/src/braket/aws/aws_quantum_task_batch.py b/src/braket/aws/aws_quantum_task_batch.py index caa48b39c..adf15bda3 100644 --- a/src/braket/aws/aws_quantum_task_batch.py +++ b/src/braket/aws/aws_quantum_task_batch.py @@ -16,7 +16,7 @@ import time from concurrent.futures.thread import ThreadPoolExecutor from itertools import repeat -from typing import Dict, List, Set, Tuple, Union +from typing import Union from braket.ahs.analog_hamiltonian_simulation import AnalogHamiltonianSimulation from braket.annealing import Problem @@ -48,7 +48,7 @@ def __init__( device_arn: str, task_specifications: Union[ Union[Circuit, Problem, OpenQasmProgram, BlackbirdProgram, AnalogHamiltonianSimulation], - List[ + list[ Union[ Circuit, Problem, OpenQasmProgram, BlackbirdProgram, AnalogHamiltonianSimulation ] @@ -60,7 +60,7 @@ def __init__( max_workers: int = MAX_CONNECTIONS_DEFAULT, poll_timeout_seconds: float = AwsQuantumTask.DEFAULT_RESULTS_POLL_TIMEOUT, poll_interval_seconds: float = AwsQuantumTask.DEFAULT_RESULTS_POLL_INTERVAL, - inputs: Union[Dict[str, float], List[Dict[str, float]]] = None, + inputs: Union[dict[str, float], list[dict[str, float]]] | None = None, *aws_quantum_task_args, **aws_quantum_task_kwargs, ): @@ -69,7 +69,7 @@ def __init__( Args: aws_session (AwsSession): AwsSession to connect to AWS with. device_arn (str): The ARN of the quantum device. - task_specifications (Union[Union[Circuit,Problem,OpenQasmProgram,BlackbirdProgram,AnalogHamiltonianSimulation],List[Union[Circuit,Problem,OpenQasmProgram,BlackbirdProgram,AnalogHamiltonianSimulation]]]): # noqa + task_specifications (Union[Union[Circuit,Problem,OpenQasmProgram,BlackbirdProgram,AnalogHamiltonianSimulation],list[Union[Circuit,Problem,OpenQasmProgram,BlackbirdProgram,AnalogHamiltonianSimulation]]]): # noqa Single instance or list of circuits, annealing problems, pulse sequences, or photonics program as specification of quantum task to run on device. @@ -88,7 +88,7 @@ def __init__( in seconds. Default: 5 days. poll_interval_seconds (float): The polling interval for results in seconds. Default: 1 second. - inputs (Union[Dict[str, float], List[Dict[str, float]]]): Inputs to be passed + inputs (Union[dict[str, float], list[dict[str, float]]] | None): Inputs to be passed along with the IR. If the IR supports inputs, the inputs will be updated with this value. Default: {}. """ @@ -127,17 +127,17 @@ def __init__( def _tasks_and_inputs( task_specifications: Union[ Union[Circuit, Problem, OpenQasmProgram, BlackbirdProgram, AnalogHamiltonianSimulation], - List[ + list[ Union[ Circuit, Problem, OpenQasmProgram, BlackbirdProgram, AnalogHamiltonianSimulation ] ], ], - inputs: Union[Dict[str, float], List[Dict[str, float]]] = None, - ) -> List[ - Tuple[ + inputs: Union[dict[str, float], list[dict[str, float]]] = None, + ) -> list[ + tuple[ Union[Circuit, Problem, OpenQasmProgram, BlackbirdProgram, AnalogHamiltonianSimulation], - Dict[str, float], + dict[str, float], ] ]: inputs = inputs or {} @@ -184,7 +184,7 @@ def _execute( device_arn: str, task_specifications: Union[ Union[Circuit, Problem, OpenQasmProgram, BlackbirdProgram, AnalogHamiltonianSimulation], - List[ + list[ Union[ Circuit, Problem, OpenQasmProgram, BlackbirdProgram, AnalogHamiltonianSimulation ] @@ -196,10 +196,10 @@ def _execute( max_workers: int = MAX_CONNECTIONS_DEFAULT, poll_timeout_seconds: float = AwsQuantumTask.DEFAULT_RESULTS_POLL_TIMEOUT, poll_interval_seconds: float = AwsQuantumTask.DEFAULT_RESULTS_POLL_INTERVAL, - inputs: Union[Dict[str, float], List[Dict[str, float]]] = None, + inputs: Union[dict[str, float], list[dict[str, float]]] = None, *args, **kwargs, - ) -> List[AwsQuantumTask]: + ) -> list[AwsQuantumTask]: tasks_and_inputs = AwsQuantumTaskBatch._tasks_and_inputs(task_specifications, inputs) max_threads = min(max_parallel, max_workers) remaining = [0 for _ in tasks_and_inputs] @@ -238,7 +238,7 @@ def _execute( @staticmethod def _create_task( - remaining: List[int], + remaining: list[int], aws_session: AwsSession, device_arn: str, task_specification: Union[ @@ -247,7 +247,7 @@ def _create_task( s3_destination_folder: AwsSession.S3DestinationFolder, shots: int, poll_interval_seconds: float = AwsQuantumTask.DEFAULT_RESULTS_POLL_INTERVAL, - inputs: Dict[str, float] = None, + inputs: dict[str, float] = None, *args, **kwargs, ) -> AwsQuantumTask: @@ -278,7 +278,7 @@ def results( fail_unsuccessful: bool = False, max_retries: int = MAX_RETRIES, use_cached_value: bool = True, - ) -> List[AwsQuantumTask]: + ) -> list[AwsQuantumTask]: """Retrieves the result of every quantum task in the batch. Polling for results happens in parallel; this method returns when all quantum tasks @@ -295,7 +295,7 @@ def results( even when results have already been cached. Default: `True`. Returns: - List[AwsQuantumTask]: The results of all of the quantum tasks in the batch. + list[AwsQuantumTask]: The results of all of the quantum tasks in the batch. `FAILED`, `CANCELLED`, or timed out quantum tasks will have a result of None """ if not self._results or not use_cached_value: @@ -316,7 +316,7 @@ def results( return self._results @staticmethod - def _retrieve_results(tasks: List[AwsQuantumTask], max_workers: int) -> List[AwsQuantumTask]: + def _retrieve_results(tasks: list[AwsQuantumTask], max_workers: int) -> list[AwsQuantumTask]: with ThreadPoolExecutor(max_workers=max_workers) as executor: result_futures = [executor.submit(task.result) for task in tasks] return [future.result() for future in result_futures] @@ -362,8 +362,8 @@ def retry_unsuccessful_tasks(self) -> bool: return not self._unsuccessful @property - def tasks(self) -> List[AwsQuantumTask]: - """List[AwsQuantumTask]: The quantum tasks in this batch, as a list of AwsQuantumTask + def tasks(self) -> list[AwsQuantumTask]: + """list[AwsQuantumTask]: The quantum tasks in this batch, as a list of AwsQuantumTask objects""" return list(self._tasks) @@ -373,10 +373,10 @@ def size(self) -> int: return len(self._tasks) @property - def unfinished(self) -> Set[str]: + def unfinished(self) -> set[str]: """Gets all the IDs of all the quantum tasks in teh batch that have yet to complete. Returns: - Set[str]: The IDs of all the quantum tasks in the batch that have yet to complete. + set[str]: The IDs of all the quantum tasks in the batch that have yet to complete. """ with ThreadPoolExecutor(max_workers=self._max_workers) as executor: status_futures = {task.id: executor.submit(task.state) for task in self._tasks} @@ -390,7 +390,7 @@ def unfinished(self) -> Set[str]: return unfinished @property - def unsuccessful(self) -> Set[str]: - """Set[str]: The IDs of all the FAILED, CANCELLED, or timed out quantum tasks in the + def unsuccessful(self) -> set[str]: + """set[str]: The IDs of all the FAILED, CANCELLED, or timed out quantum tasks in the batch.""" return set(self._unsuccessful) diff --git a/src/braket/aws/aws_session.py b/src/braket/aws/aws_session.py index 300aee13e..2ebc3c8d2 100644 --- a/src/braket/aws/aws_session.py +++ b/src/braket/aws/aws_session.py @@ -17,8 +17,9 @@ import os import os.path import re +from functools import cache from pathlib import Path -from typing import Any, Dict, List, NamedTuple, Optional, Tuple +from typing import Any, NamedTuple, Optional import backoff import boto3 @@ -39,17 +40,17 @@ class AwsSession(object): def __init__( self, - boto_session: boto3.Session = None, - braket_client: client = None, - config: Config = None, - default_bucket: str = None, + boto_session: boto3.Session | None = None, + braket_client: client | None = None, + config: Config | None = None, + default_bucket: str | None = None, ): """ Args: - boto_session (Session): A boto3 session object. - braket_client (client): A boto3 Braket client. - config (Config): A botocore Config object. - default_bucket (str): The name of the default bucket of the AWS Session. + boto_session (Session | None): A boto3 session object. + braket_client (client | None): A boto3 Braket client. + config (Config | None): A botocore Config object. + default_bucket (str | None): The name of the default bucket of the AWS Session. """ if ( boto_session @@ -269,7 +270,7 @@ def _should_giveup(err: Exception) -> bool: jitter=backoff.full_jitter, giveup=_should_giveup.__func__, ) - def get_quantum_task(self, arn: str) -> Dict[str, Any]: + def get_quantum_task(self, arn: str) -> dict[str, Any]: """ Gets the quantum task. @@ -277,9 +278,11 @@ def get_quantum_task(self, arn: str) -> Dict[str, Any]: arn (str): The ARN of the quantum task to get. Returns: - Dict[str, Any]: The response from the Amazon Braket `GetQuantumTask` operation. + dict[str, Any]: The response from the Amazon Braket `GetQuantumTask` operation. """ - response = self.braket_client.get_quantum_task(quantumTaskArn=arn) + response = self.braket_client.get_quantum_task( + quantumTaskArn=arn, additionalAttributeNames=["QueueInfo"] + ) broadcast_event(_TaskStatusEvent(arn=response["quantumTaskArn"], status=response["status"])) return response @@ -314,7 +317,7 @@ def get_default_jobs_role(self) -> str: jitter=backoff.full_jitter, giveup=_should_giveup.__func__, ) - def get_job(self, arn: str) -> Dict[str, Any]: + def get_job(self, arn: str) -> dict[str, Any]: """ Gets the hybrid job. @@ -322,11 +325,11 @@ def get_job(self, arn: str) -> Dict[str, Any]: arn (str): The ARN of the hybrid job to get. Returns: - Dict[str, Any]: The response from the Amazon Braket `GetQuantumJob` operation. + dict[str, Any]: The response from the Amazon Braket `GetQuantumJob` operation. """ - return self.braket_client.get_job(jobArn=arn) + return self.braket_client.get_job(jobArn=arn, additionalAttributeNames=["QueueInfo"]) - def cancel_job(self, arn: str) -> Dict[str, Any]: + def cancel_job(self, arn: str) -> dict[str, Any]: """ Cancel the hybrid job. @@ -334,7 +337,7 @@ def cancel_job(self, arn: str) -> Dict[str, Any]: arn (str): The ARN of the hybrid job to cancel. Returns: - Dict[str, Any]: The response from the Amazon Braket `CancelJob` operation. + dict[str, Any]: The response from the Amazon Braket `CancelJob` operation. """ return self.braket_client.cancel_job(jobArn=arn) @@ -471,7 +474,7 @@ def copy_s3_directory(self, source_s3_path: str, destination_s3_path: str) -> No key.replace(source_prefix, destination_prefix, 1), ) - def list_keys(self, bucket: str, prefix: str) -> List[str]: + def list_keys(self, bucket: str, prefix: str) -> list[str]: """ Lists keys matching prefix in bucket. @@ -480,7 +483,7 @@ def list_keys(self, bucket: str, prefix: str) -> List[str]: prefix (str): The S3 path prefix to be matched Returns: - List[str]: A list of all keys matching the prefix in + list[str]: A list of all keys matching the prefix in the bucket. """ list_objects = self.s3_client.list_objects_v2( @@ -594,7 +597,7 @@ def _create_s3_bucket_if_it_does_not_exist(self, bucket_name: str, region: str) else: raise - def get_device(self, arn: str) -> Dict[str, Any]: + def get_device(self, arn: str) -> dict[str, Any]: """ Calls the Amazon Braket `get_device` API to retrieve device metadata. @@ -602,31 +605,33 @@ def get_device(self, arn: str) -> Dict[str, Any]: arn (str): The ARN of the device. Returns: - Dict[str, Any]: The response from the Amazon Braket `GetDevice` operation. + dict[str, Any]: The response from the Amazon Braket `GetDevice` operation. """ return self.braket_client.get_device(deviceArn=arn) def search_devices( self, - arns: Optional[List[str]] = None, - names: Optional[List[str]] = None, - types: Optional[List[str]] = None, - statuses: Optional[List[str]] = None, - provider_names: Optional[List[str]] = None, - ) -> List[Dict[str, Any]]: + arns: Optional[list[str]] = None, + names: Optional[list[str]] = None, + types: Optional[list[str]] = None, + statuses: Optional[list[str]] = None, + provider_names: Optional[list[str]] = None, + ) -> list[dict[str, Any]]: """ Get devices based on filters. The result is the AND of all the filters `arns`, `names`, `types`, `statuses`, `provider_names`. Args: - arns (Optional[List[str]]): device ARN list, default is `None`. - names (Optional[List[str]]): device name list, default is `None`. - types (Optional[List[str]]): device type list, default is `None`. - statuses (Optional[List[str]]): device status list, default is `None`. - provider_names (Optional[List[str]]): provider name list, default is `None`. + arns (Optional[list[str]]): device ARN filter, default is `None`. + names (Optional[list[str]]): device name filter, default is `None`. + types (Optional[list[str]]): device type filter, default is `None`. + statuses (Optional[list[str]]): device status filter, default is `None`. When `None` + is used, RETIRED devices will not be returned. To include RETIRED devices in + the results, use a filter that includes "RETIRED" for this parameter. + provider_names (Optional[list[str]]): provider name list, default is `None`. Returns: - List[Dict[str, Any]]: The response from the Amazon Braket `SearchDevices` operation. + list[dict[str, Any]]: The response from the Amazon Braket `SearchDevices` operation. """ filters = [] if arns: @@ -642,6 +647,8 @@ def search_devices( continue if statuses and result["deviceStatus"] not in statuses: continue + if statuses is None and result["deviceStatus"] == "RETIRED": + continue if provider_names and result["providerName"] not in provider_names: continue results.append(result) @@ -663,7 +670,7 @@ def is_s3_uri(string: str) -> bool: return True @staticmethod - def parse_s3_uri(s3_uri: str) -> Tuple[str, str]: + def parse_s3_uri(s3_uri: str) -> tuple[str, str]: """ Parse S3 URI to get bucket and key @@ -671,7 +678,7 @@ def parse_s3_uri(s3_uri: str) -> Tuple[str, str]: s3_uri (str): S3 URI. Returns: - Tuple[str, str]: Bucket and Key tuple. + tuple[str, str]: Bucket and Key tuple. Raises: ValueError: Raises a ValueError if the provided string is not @@ -713,22 +720,22 @@ def describe_log_streams( self, log_group: str, log_stream_prefix: str, - limit: int = None, + limit: Optional[int] = None, next_token: Optional[str] = None, - ) -> Dict[str, Any]: + ) -> dict[str, Any]: """ Describes CloudWatch log streams in a log group with a given prefix. Args: log_group (str): Name of the log group. log_stream_prefix (str): Prefix for log streams to include. - limit (int): Limit for number of log streams returned. + limit (Optional[int]): Limit for number of log streams returned. default is 50. next_token (Optional[str]): The token for the next set of items to return. Would have been received in a previous call. Returns: - Dict[str, Any]: Dicionary containing logStreams and nextToken + dict[str, Any]: Dicionary containing logStreams and nextToken """ log_stream_args = { "logGroupName": log_group, @@ -751,7 +758,7 @@ def get_log_events( start_time: int, start_from_head: bool = True, next_token: Optional[str] = None, - ) -> Dict[str, Any]: + ) -> dict[str, Any]: """ Gets CloudWatch log events from a given log stream. @@ -765,7 +772,7 @@ def get_log_events( Would have been received in a previous call. Returns: - Dict[str, Any]: Dicionary containing events, nextForwardToken, and nextBackwardToken + dict[str, Any]: Dicionary containing events, nextForwardToken, and nextBackwardToken """ log_events_args = { "logGroupName": log_group, @@ -823,3 +830,38 @@ def copy_session( # Preserve user_agent information copied_session._braket_user_agents = self._braket_user_agents return copied_session + + @cache + def get_full_image_tag(self, image_uri: str) -> str: + """ + Get verbose image tag from image uri. + + Args: + image_uri (str): Image uri to get tag for. + + Returns: + str: Verbose image tag for given image. + """ + registry = image_uri.split(".")[0] + repository, tag = image_uri.split("/")[-1].split(":") + + # get image digest of latest image + digest = self.ecr_client.batch_get_image( + registryId=registry, + repositoryName=repository, + imageIds=[{"imageTag": tag}], + )["images"][0]["imageId"]["imageDigest"] + + # get all images matching digest (same image, different tags) + images = self.ecr_client.batch_get_image( + registryId=registry, + repositoryName=repository, + imageIds=[{"imageDigest": digest}], + )["images"] + + # find the tag with the python version info + for image in images: + if re.search(r"py\d\d+", tag := image["imageId"]["imageTag"]): + return tag + + raise ValueError("Full image tag missing.") diff --git a/src/braket/aws/queue_information.py b/src/braket/aws/queue_information.py new file mode 100644 index 000000000..109632751 --- /dev/null +++ b/src/braket/aws/queue_information.py @@ -0,0 +1,85 @@ +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"). You +# may not use this file except in compliance with the License. A copy of +# the License is located at +# +# http://aws.amazon.com/apache2.0/ +# +# or in the "license" file accompanying this file. This file is +# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF +# ANY KIND, either express or implied. See the License for the specific +# language governing permissions and limitations under the License. + +from dataclasses import dataclass +from enum import Enum +from typing import Optional + + +class QueueType(str, Enum): + """ + Enumerates the possible priorities for the queue. + + Values: + NORMAL: Represents normal queue for the device. + PRIORITY: Represents priority queue for the device. + """ + + NORMAL = "Normal" + PRIORITY = "Priority" + + +@dataclass() +class QueueDepthInfo: + """ + Represents quantum tasks and hybrid jobs queue depth information. + + Attributes: + quantum_tasks (dict[QueueType, str]): number of quantum tasks waiting + to run on a device. This includes both 'Normal' and 'Priority' tasks. + For Example, {'quantum_tasks': {QueueType.NORMAL: '7', QueueType.PRIORITY: '3'}} + jobs (str): number of hybrid jobs waiting to run on a device. Additionally, for QPUs if + hybrid jobs queue depth is 0, we display information about priority and count of the + running hybrid jobs. Example, 'jobs': '0 (1 prioritized job(s) running)' + """ + + quantum_tasks: dict[QueueType, str] + jobs: str + + +@dataclass +class QuantumTaskQueueInfo: + """ + Represents quantum tasks queue information. + + Attributes: + queue_type (QueueType): type of the quantum_task queue either 'Normal' + or 'Priority'. + queue_position (Optional[str]): current position of your quantum task within a respective + device queue. This value can be None based on the state of the task. Default: None. + message (Optional[str]): Additional message information. This key is present only + if 'queue_position' is None. Default: None. + """ + + queue_type: QueueType + queue_position: Optional[str] = None + message: Optional[str] = None + + +@dataclass +class HybridJobQueueInfo: + """ + Represents hybrid job queue information. + + Attributes: + queue_position (Optional[str]): current position of your hybrid job within a respective + device queue. If the queue position of the hybrid job is greater than 15, we + return '>15' as the queue_position return value. The queue_position is only + returned when hybrid job is not in RUNNING/CANCELLING/TERMINAL states, else + queue_position is returned as None. + message (Optional[str]): Additional message information. This key is present only + if 'queue_position' is None. Default: None. + """ + + queue_position: Optional[str] = None + message: Optional[str] = None diff --git a/src/braket/circuits/angled_gate.py b/src/braket/circuits/angled_gate.py index f45bf6165..e453177a7 100644 --- a/src/braket/circuits/angled_gate.py +++ b/src/braket/circuits/angled_gate.py @@ -15,8 +15,9 @@ import copy import math +from collections.abc import Sequence from functools import singledispatch -from typing import List, Optional, Sequence, Union +from typing import Optional, Union from sympy import Float @@ -61,13 +62,13 @@ def __init__( self._parameters = [float(angle)] # explicit casting in case angle is e.g. np.float32 @property - def parameters(self) -> List[Union[FreeParameterExpression, float]]: + def parameters(self) -> list[Union[FreeParameterExpression, float]]: """ Returns the parameters associated with the object, either unbound free parameters or bound values. Returns: - List[Union[FreeParameterExpression, float]]: The free parameters or fixed value + list[Union[FreeParameterExpression, float]]: The free parameters or fixed value associated with the object. """ return self._parameters @@ -93,11 +94,11 @@ def bind_values(self, **kwargs) -> AngledGate: """ raise NotImplementedError - def adjoint(self) -> List[Gate]: + def adjoint(self) -> list[Gate]: """Returns the adjoint of this gate as a singleton list. Returns: - List[Gate]: A list containing the gate with negated angle. + list[Gate]: A list containing the gate with negated angle. """ gate_ascii_name_index = self.ascii_symbols[0].find("(") gate_ascii_name = self.ascii_symbols[0][:gate_ascii_name_index] @@ -166,13 +167,13 @@ def __init__( ] @property - def parameters(self) -> List[Union[FreeParameterExpression, float]]: + def parameters(self) -> list[Union[FreeParameterExpression, float]]: """ Returns the parameters associated with the object, either unbound free parameters or bound values. Returns: - List[Union[FreeParameterExpression, float]]: The free parameters or fixed value + list[Union[FreeParameterExpression, float]]: The free parameters or fixed value associated with the object. """ return self._parameters @@ -212,11 +213,11 @@ def bind_values(self, **kwargs) -> AngledGate: """ raise NotImplementedError - def adjoint(self) -> List[Gate]: + def adjoint(self) -> list[Gate]: """Returns the adjoint of this gate as a singleton list. Returns: - List[Gate]: A list containing the gate with negated angle. + list[Gate]: A list containing the gate with negated angle. """ raise NotImplementedError @@ -285,13 +286,13 @@ def __init__( ] @property - def parameters(self) -> List[Union[FreeParameterExpression, float]]: + def parameters(self) -> list[Union[FreeParameterExpression, float]]: """ Returns the parameters associated with the object, either unbound free parameters or bound values. Returns: - List[Union[FreeParameterExpression, float]]: The free parameters or fixed value + list[Union[FreeParameterExpression, float]]: The free parameters or fixed value associated with the object. """ return self._parameters @@ -341,11 +342,11 @@ def bind_values(self, **kwargs) -> AngledGate: """ raise NotImplementedError - def adjoint(self) -> List[Gate]: + def adjoint(self) -> list[Gate]: """Returns the adjoint of this gate as a singleton list. Returns: - List[Gate]: A list containing the gate with negated angle. + list[Gate]: A list containing the gate with negated angle. """ raise NotImplementedError diff --git a/src/braket/circuits/ascii_circuit_diagram.py b/src/braket/circuits/ascii_circuit_diagram.py index f43f37a36..daef01793 100644 --- a/src/braket/circuits/ascii_circuit_diagram.py +++ b/src/braket/circuits/ascii_circuit_diagram.py @@ -14,7 +14,7 @@ from __future__ import annotations from functools import reduce -from typing import List, Tuple, Union +from typing import Union import braket.circuits.circuit as cir from braket.circuits.circuit_diagram import CircuitDiagram @@ -22,8 +22,8 @@ from braket.circuits.gate import Gate from braket.circuits.instruction import Instruction from braket.circuits.noise import Noise -from braket.circuits.qubit_set import QubitSet from braket.circuits.result_type import ResultType +from braket.registers.qubit_set import QubitSet class AsciiCircuitDiagram(CircuitDiagram): @@ -100,17 +100,17 @@ def build_diagram(circuit: cir.Circuit) -> str: @staticmethod def _ascii_group_items( circuit_qubits: QubitSet, - items: List[Union[Instruction, ResultType]], - ) -> List[Tuple[QubitSet, List[Instruction]]]: + items: list[Union[Instruction, ResultType]], + ) -> list[tuple[QubitSet, list[Instruction]]]: """ Group instructions in a moment for ASCII diagram Args: circuit_qubits (QubitSet): set of qubits in circuit - items (List[Union[Instruction, ResultType]]): list of instructions or result types + items (list[Union[Instruction, ResultType]]): list of instructions or result types Returns: - List[Tuple[QubitSet, List[Instruction]]]: list of grouped instructions or result types. + list[tuple[QubitSet, list[Instruction]]]: list of grouped instructions or result types. """ groupings = [] for item in items: @@ -151,16 +151,16 @@ def _ascii_group_items( @staticmethod def _categorize_result_types( - result_types: List[ResultType], - ) -> Tuple[List[str], List[ResultType]]: + result_types: list[ResultType], + ) -> tuple[list[str], list[ResultType]]: """ Categorize result types into result types with target and those without. Args: - result_types (List[ResultType]): list of result types + result_types (list[ResultType]): list of result types Returns: - Tuple[List[str], List[ResultType]]: first element is a list of result types + tuple[list[str], list[ResultType]]: first element is a list of result types without `target` attribute; second element is a list of result types with `target` attribute """ @@ -175,7 +175,7 @@ def _categorize_result_types( @staticmethod def _ascii_diagram_column_set( - col_title: str, circuit_qubits: QubitSet, items: List[Union[Instruction, ResultType]] + col_title: str, circuit_qubits: QubitSet, items: list[Union[Instruction, ResultType]] ) -> str: """ Return a set of columns in the ASCII string diagram of the circuit for a list of items. @@ -183,7 +183,7 @@ def _ascii_diagram_column_set( Args: col_title (str): title of column set circuit_qubits (QubitSet): qubits in circuit - items (List[Union[Instruction, ResultType]]): list of instructions or result types + items (list[Union[Instruction, ResultType]]): list of instructions or result types Returns: str: An ASCII string diagram for the column set. @@ -220,14 +220,14 @@ def _ascii_diagram_column_set( @staticmethod def _ascii_diagram_column( - circuit_qubits: QubitSet, items: List[Union[Instruction, ResultType]] + circuit_qubits: QubitSet, items: list[Union[Instruction, ResultType]] ) -> str: """ Return a column in the ASCII string diagram of the circuit for a given list of items. Args: circuit_qubits (QubitSet): qubits in circuit - items (List[Union[Instruction, ResultType]]): list of instructions or result types + items (list[Union[Instruction, ResultType]]): list of instructions or result types Returns: str: An ASCII string diagram for the specified moment in time for a column. diff --git a/src/braket/circuits/basis_state.py b/src/braket/circuits/basis_state.py index 66e406aa2..814444e75 100644 --- a/src/braket/circuits/basis_state.py +++ b/src/braket/circuits/basis_state.py @@ -1,5 +1,5 @@ from functools import singledispatch -from typing import List, Optional, Union +from typing import Optional, Union import numpy as np @@ -34,7 +34,7 @@ def __eq__(self, other): return self.state == other.state -BasisStateInput = Union[int, List[int], str, BasisState] +BasisStateInput = Union[int, list[int], str, BasisState] @singledispatch diff --git a/src/braket/circuits/braket_program_context.py b/src/braket/circuits/braket_program_context.py index 6d939882c..9d8c69afe 100644 --- a/src/braket/circuits/braket_program_context.py +++ b/src/braket/circuits/braket_program_context.py @@ -11,7 +11,7 @@ # ANY KIND, either express or implied. See the License for the specific # language governing permissions and limitations under the License. -from typing import List, Optional, Tuple, Union +from typing import Optional, Union import numpy as np from sympy import Expr @@ -56,18 +56,18 @@ def is_builtin_gate(self, name: str) -> bool: user_defined_gate = self.is_user_defined_gate(name) return name in BRAKET_GATES and not user_defined_gate - def add_phase_instruction(self, target: Tuple[int], phase_value: int) -> None: + def add_phase_instruction(self, target: tuple[int], phase_value: int) -> None: raise NotImplementedError def add_gate_instruction( - self, gate_name: str, target: Tuple[int], *params, ctrl_modifiers: List[int], power: float + self, gate_name: str, target: tuple[int], *params, ctrl_modifiers: list[int], power: float ) -> None: """Add Braket gate to the circuit. Args: gate_name (str): name of the built-in Braket gate. - target (Tuple[int]): control_qubits + target_qubits. - ctrl_modifiers (List[int]): Quantum state on which to control the + target (tuple[int]): control_qubits + target_qubits. + ctrl_modifiers (list[int]): Quantum state on which to control the operation. Must be a binary sequence of same length as number of qubits in `control-qubits` in target. For example "0101", [0, 1, 0, 1], 5 all represent controlling on qubits 0 and 2 being in the \\|0⟩ state and qubits 1 and 3 being @@ -88,26 +88,26 @@ def add_gate_instruction( def add_custom_unitary( self, unitary: np.ndarray, - target: Tuple[int], + target: tuple[int], ) -> None: """Add a custom Unitary instruction to the circuit Args: unitary (np.ndarray): unitary matrix - target (Tuple[int]): control_qubits + target_qubits + target (tuple[int]): control_qubits + target_qubits """ instruction = Instruction(Unitary(unitary), target) self._circuit.add_instruction(instruction) def add_noise_instruction( - self, noise_instruction: str, target: List[int], probabilities: List[float] + self, noise_instruction: str, target: list[int], probabilities: list[float] ) -> None: """Method to add a noise instruction to the circuit Args: noise_instruction (str): The name of the noise operation - target (List[int]): The target qubit or qubits to which the noise operation is applied. - probabilities (List[float]): The probabilities associated with each possible outcome + target (list[int]): The target qubit or qubits to which the noise operation is applied. + probabilities (list[float]): The probabilities associated with each possible outcome of the noise operation. """ instruction = Instruction( @@ -115,12 +115,12 @@ def add_noise_instruction( ) self._circuit.add_instruction(instruction) - def add_kraus_instruction(self, matrices: List[np.ndarray], target: List[int]) -> None: + def add_kraus_instruction(self, matrices: list[np.ndarray], target: list[int]) -> None: """Method to add a Kraus instruction to the circuit Args: - matrices (List[ndarray]): The matrices defining the Kraus operation - target (List[int]): The target qubit or qubits to which the Kraus operation is applied. + matrices (list[ndarray]): The matrices defining the Kraus operation + target (list[int]): The target qubit or qubits to which the Kraus operation is applied. """ instruction = Instruction(Kraus(matrices), target) self._circuit.add_instruction(instruction) diff --git a/src/braket/circuits/circuit.py b/src/braket/circuits/circuit.py index f8bcf1848..3939e5089 100644 --- a/src/braket/circuits/circuit.py +++ b/src/braket/circuits/circuit.py @@ -13,22 +13,9 @@ from __future__ import annotations -import warnings +from collections.abc import Callable, Iterable from numbers import Number -from typing import ( - TYPE_CHECKING, - Any, - Callable, - Dict, - Iterable, - List, - Optional, - Set, - Tuple, - Type, - TypeVar, - Union, -) +from typing import TYPE_CHECKING, Any, Optional, Type, TypeVar, Union if TYPE_CHECKING: from braket.aws.aws_device import AwsDevice @@ -56,8 +43,6 @@ from braket.circuits.observable import Observable from braket.circuits.observables import TensorProduct from braket.circuits.parameterizable import Parameterizable -from braket.circuits.qubit import QubitInput -from braket.circuits.qubit_set import QubitSet, QubitSetInput from braket.circuits.result_type import ( ObservableParameterResultType, ObservableResultType, @@ -69,7 +54,7 @@ QubitReferenceType, SerializationProperties, ) -from braket.circuits.unitary_calculation import calculate_unitary, calculate_unitary_big_endian +from braket.circuits.unitary_calculation import calculate_unitary_big_endian from braket.default_simulator.openqasm.interpreter import Interpreter from braket.ir.jaqcd import Program as JaqcdProgram from braket.ir.openqasm import Program as OpenQasmProgram @@ -77,6 +62,8 @@ from braket.pulse import ArbitraryWaveform, Frame from braket.pulse.ast.qasm_parser import ast_to_qasm from braket.pulse.pulse_sequence import PulseSequence, _validate_uniqueness +from braket.registers.qubit import QubitInput +from braket.registers.qubit_set import QubitSet, QubitSetInput SubroutineReturn = TypeVar( "SubroutineReturn", Iterable[Instruction], Instruction, ResultType, Iterable[ResultType] @@ -134,10 +121,10 @@ def method_from_subroutine(self, *args, **kwargs) -> SubroutineReturn: function_attr = getattr(cls, function_name) setattr(function_attr, "__doc__", func.__doc__) - def __init__(self, addable: AddableTypes = None, *args, **kwargs): + def __init__(self, addable: AddableTypes | None = None, *args, **kwargs): """ Args: - addable (AddableTypes): The item(s) to add to self. + addable (AddableTypes | None): The item(s) to add to self. Default = None. Raises: @@ -157,9 +144,9 @@ def __init__(self, addable: AddableTypes = None, *args, **kwargs): """ self._moments: Moments = Moments() - self._result_types: Dict[ResultType] = {} - self._qubit_observable_mapping: Dict[Union[int, Circuit._ALL_QUBITS], Observable] = {} - self._qubit_observable_target_mapping: Dict[int, Tuple[int]] = {} + self._result_types: dict[ResultType] = {} + self._qubit_observable_mapping: dict[Union[int, Circuit._ALL_QUBITS], Observable] = {} + self._qubit_observable_target_mapping: dict[int, tuple[int]] = {} self._qubit_observable_set = set() self._parameters = set() self._observables_simultaneously_measurable = True @@ -174,21 +161,21 @@ def depth(self) -> int: return self._moments.depth @property - def instructions(self) -> List[Instruction]: + def instructions(self) -> list[Instruction]: """Iterable[Instruction]: Get an `iterable` of instructions in the circuit.""" return list(self._moments.values()) @property - def result_types(self) -> List[ResultType]: - """List[ResultType]: Get a list of requested result types in the circuit.""" + def result_types(self) -> list[ResultType]: + """list[ResultType]: Get a list of requested result types in the circuit.""" return list(self._result_types.keys()) @property - def basis_rotation_instructions(self) -> List[Instruction]: + def basis_rotation_instructions(self) -> list[Instruction]: """Gets a list of basis rotation instructions. Returns: - List[Instruction]: Get a list of basis rotation instructions in the circuit. + list[Instruction]: Get a list of basis rotation instructions in the circuit. These basis rotation instructions are added if result types are requested for an observable other than Pauli-Z. @@ -216,8 +203,8 @@ def basis_rotation_instructions(self) -> List[Instruction]: @staticmethod def _observable_to_instruction( - observable: Observable, target_list: List[int] - ) -> List[Instruction]: + observable: Observable, target_list: list[int] + ) -> list[Instruction]: return [Instruction(gate, target_list) for gate in observable.basis_rotation_gates] @property @@ -240,30 +227,30 @@ def qubits(self) -> QubitSet: return QubitSet(self._moments.qubits.union(self._qubit_observable_set)) @property - def parameters(self) -> Set[FreeParameter]: + def parameters(self) -> set[FreeParameter]: """ Gets a set of the parameters in the Circuit. Returns: - Set[FreeParameter]: The `FreeParameters` in the Circuit. + set[FreeParameter]: The `FreeParameters` in the Circuit. """ return self._parameters def add_result_type( self, result_type: ResultType, - target: QubitSetInput = None, - target_mapping: Dict[QubitInput, QubitInput] = None, + target: QubitSetInput | None = None, + target_mapping: dict[QubitInput, QubitInput] | None = None, ) -> Circuit: """ Add a requested result type to `self`, returns `self` for chaining ability. Args: result_type (ResultType): `ResultType` to add into `self`. - target (QubitSetInput): Target qubits for the + target (QubitSetInput | None): Target qubits for the `result_type`. Default = `None`. - target_mapping (Dict[QubitInput, QubitInput]): A dictionary of + target_mapping (dict[QubitInput, QubitInput] | None): A dictionary of qubit mappings to apply to the `result_type.target`. Key is the qubit in `result_type.target` and the value is what the key will be changed to. Default = `None`. @@ -393,7 +380,7 @@ def _add_to_qubit_observable_mapping( @staticmethod def _tensor_product_index_dict( observable: TensorProduct, observable_target: QubitSet - ) -> Dict[int, Tuple[Observable, Tuple[int, ...]]]: + ) -> dict[int, tuple[Observable, tuple[int, ...]]]: obj_dict = {} i = 0 factors = list(observable.factors) @@ -416,19 +403,19 @@ def _add_to_qubit_observable_set(self, result_type: ResultType) -> None: def add_instruction( self, instruction: Instruction, - target: QubitSetInput = None, - target_mapping: Dict[QubitInput, QubitInput] = None, + target: QubitSetInput | None = None, + target_mapping: dict[QubitInput, QubitInput] | None = None, ) -> Circuit: """ Add an instruction to `self`, returns `self` for chaining ability. Args: instruction (Instruction): `Instruction` to add into `self`. - target (QubitSetInput): Target qubits for the + target (QubitSetInput | None): Target qubits for the `instruction`. If a single qubit gate, an instruction is created for every index in `target`. Default = `None`. - target_mapping (Dict[QubitInput, QubitInput]): A dictionary of + target_mapping (dict[QubitInput, QubitInput] | None): A dictionary of qubit mappings to apply to the `instruction.target`. Key is the qubit in `instruction.target` and the value is what the key will be changed to. Default = `None`. @@ -508,19 +495,19 @@ def _check_for_params(self, instruction: Instruction) -> bool: def add_circuit( self, circuit: Circuit, - target: QubitSetInput = None, - target_mapping: Dict[QubitInput, QubitInput] = None, + target: QubitSetInput | None = None, + target_mapping: dict[QubitInput, QubitInput] | None = None, ) -> Circuit: """ Add a `circuit` to self, returns self for chaining ability. Args: circuit (Circuit): Circuit to add into self. - target (QubitSetInput): Target qubits for the + target (QubitSetInput | None): Target qubits for the supplied circuit. This is a macro over `target_mapping`; `target` is converted to a `target_mapping` by zipping together a sorted `circuit.qubits` and `target`. Default = `None`. - target_mapping (Dict[QubitInput, QubitInput]): A dictionary of + target_mapping (dict[QubitInput, QubitInput] | None): A dictionary of qubit mappings to apply to the qubits of `circuit.instructions`. Key is the qubit to map, and the value is what to change it to. Default = `None`. @@ -584,8 +571,8 @@ def add_circuit( def add_verbatim_box( self, verbatim_circuit: Circuit, - target: QubitSetInput = None, - target_mapping: Dict[QubitInput, QubitInput] = None, + target: QubitSetInput | None = None, + target_mapping: dict[QubitInput, QubitInput] | None = None, ) -> Circuit: """ Add a verbatim `circuit` to self, that is, ensures that `circuit` is not modified in any way @@ -593,11 +580,11 @@ def add_verbatim_box( Args: verbatim_circuit (Circuit): Circuit to add into self. - target (QubitSetInput): Target qubits for the + target (QubitSetInput | None): Target qubits for the supplied circuit. This is a macro over `target_mapping`; `target` is converted to a `target_mapping` by zipping together a sorted `circuit.qubits` and `target`. Default = `None`. - target_mapping (Dict[QubitInput, QubitInput]): A dictionary of + target_mapping (dict[QubitInput, QubitInput] | None): A dictionary of qubit mappings to apply to the qubits of `circuit.instructions`. Key is the qubit to map, and the value is what to change it to. Default = `None`. @@ -653,9 +640,9 @@ def add_verbatim_box( def apply_gate_noise( self, - noise: Union[Type[Noise], Iterable[Type[Noise]]], - target_gates: Optional[Union[Type[Gate], Iterable[Type[Gate]]]] = None, - target_unitary: np.ndarray = None, + noise: Union[type[Noise], Iterable[type[Noise]]], + target_gates: Optional[Union[type[Gate], Iterable[type[Gate]]]] = None, + target_unitary: Optional[np.ndarray] = None, target_qubits: Optional[QubitSetInput] = None, ) -> Circuit: """Apply `noise` to the circuit according to `target_gates`, `target_unitary` and @@ -680,11 +667,11 @@ def apply_gate_noise( only applied to gates with the same qubit_count in target_qubits. Args: - noise (Union[Type[Noise], Iterable[Type[Noise]]]): Noise channel(s) to be applied + noise (Union[type[Noise], Iterable[type[Noise]]]): Noise channel(s) to be applied to the circuit. - target_gates (Optional[Union[Type[Gate], Iterable[Type[Gate]]]]): Gate class or - List of Gate classes which `noise` is applied to. Default=None. - target_unitary (ndarray): matrix of the target unitary gates. Default=None. + target_gates (Optional[Union[type[Gate], Iterable[type[Gate]]]]): Gate class or + list of Gate classes which `noise` is applied to. Default=None. + target_unitary (Optional[ndarray]): matrix of the target unitary gates. Default=None. target_qubits (Optional[QubitSetInput]): Index or indices of qubit(s). Default=None. @@ -797,7 +784,7 @@ def apply_gate_noise( def apply_initialization_noise( self, - noise: Union[Type[Noise], Iterable[Type[Noise]]], + noise: Union[type[Noise], Iterable[type[Noise]]], target_qubits: Optional[QubitSetInput] = None, ) -> Circuit: """Apply `noise` at the beginning of the circuit for every qubit (default) or @@ -809,7 +796,7 @@ def apply_initialization_noise( to `noise.qubit_count`. Args: - noise (Union[Type[Noise], Iterable[Type[Noise]]]): Noise channel(s) to be applied + noise (Union[type[Noise], Iterable[type[Noise]]]): Noise channel(s) to be applied to the circuit. target_qubits (Optional[QubitSetInput]): Index or indices of qubit(s). Default=None. @@ -864,16 +851,16 @@ def apply_initialization_noise( return apply_noise_to_moments(self, noise, target_qubits, "initialization") - def make_bound_circuit(self, param_values: Dict[str, Number], strict: bool = False) -> Circuit: + def make_bound_circuit(self, param_values: dict[str, Number], strict: bool = False) -> Circuit: """ Binds FreeParameters based upon their name and values passed in. If parameters share the same name, all the parameters of that name will be set to the mapped value. Args: - param_values (Dict[str, Number]): A mapping of FreeParameter names + param_values (dict[str, Number]): A mapping of FreeParameter names to a value to assign to them. - strict (bool): If True, raises a ValueError if none of the FreeParameters - in param_values appear in the circuit. False by default." + strict (bool): If True, raises a ValueError if any of the FreeParameters + in param_values do not appear in the circuit. False by default. Returns: Circuit: Returns a circuit with all present parameters fixed to their respective @@ -883,17 +870,17 @@ def make_bound_circuit(self, param_values: Dict[str, Number], strict: bool = Fal self._validate_parameters(param_values) return self._use_parameter_value(param_values) - def _validate_parameters(self, parameter_values: Dict[str, Number]) -> None: + def _validate_parameters(self, parameter_values: dict[str, Number]) -> None: """ This runs a check to see that the parameters are in the Circuit. Args: - parameter_values (Dict[str, Number]): A mapping of FreeParameter names + parameter_values (dict[str, Number]): A mapping of FreeParameter names to a value to assign to them. Raises: - ValueError: If there are no parameters that match the key for the arg - param_values. + ValueError: If a parameter name is given which does not appear in the circuit. + """ parameter_strings = set() for parameter in self.parameters: @@ -902,12 +889,12 @@ def _validate_parameters(self, parameter_values: Dict[str, Number]) -> None: if param not in parameter_strings: raise ValueError(f"No parameter in the circuit named: {param}") - def _use_parameter_value(self, param_values: Dict[str, Number]) -> Circuit: + def _use_parameter_value(self, param_values: dict[str, Number]) -> Circuit: """ Creates a Circuit that uses the parameter values passed in. Args: - param_values (Dict[str, Number]): A mapping of FreeParameter names + param_values (dict[str, Number]): A mapping of FreeParameter names to a value to assign to them. Returns: @@ -948,7 +935,7 @@ def _validate_parameter_value(val: Any) -> None: def apply_readout_noise( self, - noise: Union[Type[Noise], Iterable[Type[Noise]]], + noise: Union[type[Noise], Iterable[type[Noise]]], target_qubits: Optional[QubitSetInput] = None, ) -> Circuit: """Apply `noise` right before measurement in every qubit (default) or target_qubits`. @@ -959,7 +946,7 @@ def apply_readout_noise( to `noise.qubit_count`. Args: - noise (Union[Type[Noise], Iterable[Type[Noise]]]): Noise channel(s) to be applied + noise (Union[type[Noise], Iterable[type[Noise]]]): Noise channel(s) to be applied to the circuit. target_qubits (Optional[QubitSetInput]): Index or indices of qubit(s). Default=None. @@ -1098,12 +1085,12 @@ def adjoint(self) -> Circuit: circ.add_result_type(result_type) return circ - def diagram(self, circuit_diagram_class: Type = AsciiCircuitDiagram) -> str: + def diagram(self, circuit_diagram_class: type = AsciiCircuitDiagram) -> str: """ Get a diagram for the current circuit. Args: - circuit_diagram_class (Type): A `CircuitDiagram` class that builds the + circuit_diagram_class (type): A `CircuitDiagram` class that builds the diagram for this circuit. Default = `AsciiCircuitDiagram`. Returns: @@ -1114,15 +1101,16 @@ def diagram(self, circuit_diagram_class: Type = AsciiCircuitDiagram) -> str: def pulse_sequence( self, device: AwsDevice, - gate_definitions: Optional[Dict[Tuple[Gate, QubitSet], PulseSequence]], + gate_definitions: Optional[dict[tuple[Gate, QubitSet], PulseSequence]] = None, pulse_sequence_builder_class: Type = CircuitPulseSequenceBuilder, ) -> PulseSequence: """ Get the associated pulse sequence for the current circuit. Args: - gate_definitions (Optional[Dict[Tuple[Gate, QubitSet], PulseSequence]]): - Additional gate definitions + device (AwsDevice): an AWS device. + gate_definitions (Optional[dict[tuple[Gate, QubitSet], PulseSequence]]): Additional + gate definitions. pulse_sequence_builder_class (Type): A `CircuitPulseSequenceBuilder` class that builds the pulse sequence for this circuit. Default = `CircuitPulseSequenceBuilder`. @@ -1134,8 +1122,8 @@ def pulse_sequence( def to_ir( self, ir_type: IRType = IRType.JAQCD, - serialization_properties: SerializationProperties = None, - gate_definitions: Optional[Dict[Tuple[Gate, QubitSet], PulseSequence]] = None, + serialization_properties: Optional[SerializationProperties] = None, + gate_definitions: Optional[dict[tuple[Gate, QubitSet], PulseSequence]] = None, ) -> Union[OpenQasmProgram, JaqcdProgram]: """ Converts the circuit into the canonical intermediate representation. @@ -1144,10 +1132,11 @@ def to_ir( Args: ir_type (IRType): The IRType to use for converting the circuit object to its IR representation. - serialization_properties (SerializationProperties): The serialization properties to use - while serializing the object to the IR representation. The serialization properties - supplied must correspond to the supplied `ir_type`. Defaults to None. - gate_definitions (Optional[Dict[Tuple[Gate, QubitSet], PulseSequence]]): The + serialization_properties (Optional[SerializationProperties]): The serialization + properties to use while serializing the object to the IR representation. The + serialization properties supplied must correspond to the supplied `ir_type`. + Defaults to None. + gate_definitions (Optional[dict[tuple[Gate, QubitSet], PulseSequence]]): The calibration data for the device. default: None. Returns: @@ -1177,14 +1166,14 @@ def to_ir( @staticmethod def from_ir( - source: Union[str, OpenQasmProgram], inputs: Optional[Dict[str, io_type]] = None + source: Union[str, OpenQasmProgram], inputs: Optional[dict[str, io_type]] = None ) -> Circuit: """ Converts an OpenQASM program to a Braket Circuit object. Args: source (Union[str, OpenQasmProgram]): OpenQASM string. - inputs (Optional[Dict[str, io_type]]): Inputs to the circuit. + inputs (Optional[dict[str, io_type]]): Inputs to the circuit. Returns: Circuit: Braket Circuit implementing the OpenQASM program. @@ -1219,7 +1208,7 @@ def _to_jaqcd(self) -> JaqcdProgram: def _to_openqasm( self, serialization_properties: OpenQASMSerializationProperties, - gate_definitions: Optional[Dict[Tuple[Gate, QubitSet], PulseSequence]], + gate_definitions: Optional[dict[tuple[Gate, QubitSet], PulseSequence]], ) -> OpenQasmProgram: ir_instructions = self._create_openqasm_header(serialization_properties, gate_definitions) openqasm_ir_type = IRType.OPENQASM @@ -1249,24 +1238,24 @@ def _to_openqasm( ) for idx, qubit in enumerate(qubits): qubit_target = serialization_properties.format_target(int(qubit)) - ir_instructions.append(f"__bits__[{idx}] = measure {qubit_target};") + ir_instructions.append(f"b[{idx}] = measure {qubit_target};") return OpenQasmProgram.construct(source="\n".join(ir_instructions), inputs={}) def _create_openqasm_header( self, serialization_properties: OpenQASMSerializationProperties, - gate_definitions: Optional[Dict[Tuple[Gate, QubitSet], PulseSequence]], - ) -> List[str]: + gate_definitions: Optional[dict[tuple[Gate, QubitSet], PulseSequence]], + ) -> list[str]: ir_instructions = ["OPENQASM 3.0;"] for parameter in self.parameters: ir_instructions.append(f"input float {parameter};") if not self.result_types: - ir_instructions.append(f"bit[{self.qubit_count}] __bits__;") + ir_instructions.append(f"bit[{self.qubit_count}] b;") if serialization_properties.qubit_reference_type == QubitReferenceType.VIRTUAL: total_qubits = max(self.qubits).real + 1 - ir_instructions.append(f"qubit[{total_qubits}] __qubits__;") + ir_instructions.append(f"qubit[{total_qubits}] q;") elif serialization_properties.qubit_reference_type != QubitReferenceType.PHYSICAL: raise ValueError( f"Invalid qubit_reference_type " @@ -1280,9 +1269,9 @@ def _create_openqasm_header( def _validate_gate_calbrations_uniqueness( self, - gate_definitions: Dict[Tuple[Gate, QubitSet], PulseSequence], - frames: Dict[Frame], - waveforms: Dict[ArbitraryWaveform], + gate_definitions: dict[tuple[Gate, QubitSet], PulseSequence], + frames: dict[Frame], + waveforms: dict[ArbitraryWaveform], ) -> None: for key, calibration in gate_definitions.items(): for frame in calibration._frames.values(): @@ -1293,7 +1282,7 @@ def _validate_gate_calbrations_uniqueness( waveforms[waveform.id] = waveform def _generate_frame_wf_defcal_declarations( - self, gate_definitions: Optional[Dict[Tuple[Gate, QubitSet], PulseSequence]] + self, gate_definitions: Optional[dict[tuple[Gate, QubitSet], PulseSequence]] ) -> Optional[str]: program = oqpy.Program(None) @@ -1339,8 +1328,8 @@ def _generate_frame_wf_defcal_declarations( return None def _get_frames_waveforms_from_instrs( - self, gate_definitions: Optional[Dict[Tuple[Gate, QubitSet], PulseSequence]] - ) -> Tuple[Dict[Frame], Dict[ArbitraryWaveform]]: + self, gate_definitions: Optional[dict[tuple[Gate, QubitSet], PulseSequence]] + ) -> tuple[dict[Frame], dict[ArbitraryWaveform]]: from braket.circuits.gates import PulseGate frames = {} @@ -1363,9 +1352,9 @@ def _get_frames_waveforms_from_instrs( def _add_fixed_argument_calibrations( self, - gate_definitions: Dict[Tuple[Gate, QubitSet], PulseSequence], + gate_definitions: dict[tuple[Gate, QubitSet], PulseSequence], instruction: Instruction, - ) -> Dict[Tuple[Gate, QubitSet], PulseSequence]: + ) -> dict[tuple[Gate, QubitSet], PulseSequence]: """Adds calibrations with arguments set to the instruction parameter values Given the collection of parameters in instruction.operator, this function looks for matching @@ -1378,12 +1367,12 @@ def _add_fixed_argument_calibrations( If N=0, we ignore it as it will not be removed by _generate_frame_wf_defcal_declarations. Args: - gate_definitions (Dict[Tuple[Gate, QubitSet], PulseSequence]): a dictionary of + gate_definitions (dict[tuple[Gate, QubitSet], PulseSequence]): a dictionary of calibrations instruction (Instruction): a Circuit instruction Returns: - Dict[Tuple[Gate, QubitSet], PulseSequence]: additional calibrations + dict[tuple[Gate, QubitSet], PulseSequence]: additional calibrations Raises: NotImplementedError: in two cases: (i) if the instruction contains unbound parameters @@ -1427,52 +1416,6 @@ def _add_fixed_argument_calibrations( ) return additional_calibrations - def as_unitary(self) -> np.ndarray: - r""" - Returns the unitary matrix representation, in little endian format, of the entire circuit. - *Note*: The performance of this method degrades with qubit count. It might be slow for - qubit count > 10. - - Returns: - ndarray: A numpy array with shape (2^qubit_count, 2^qubit_count) representing the - circuit as a unitary. *Note*: For an empty circuit, an empty numpy array is - returned (`array([], dtype=complex128)`) - - Warnings: - This method has been deprecated, please use to_unitary() instead. - The unitary returned by this method is *little-endian*; the first qubit in the circuit - is the _least_ significant. For example, a circuit `Circuit().h(0).x(1)` will yield the - unitary :math:`X(1) \otimes H(0)`. - - Raises: - TypeError: If circuit is not composed only of `Gate` instances, - i.e. a circuit with `Noise` operators will raise this error. - - Examples: - >>> circ = Circuit().h(0).cnot(0, 1) - >>> circ.as_unitary() - array([[ 0.70710678+0.j, 0.70710678+0.j, 0. +0.j, - 0. +0.j], - [ 0. +0.j, 0. +0.j, 0.70710678+0.j, - -0.70710678+0.j], - [ 0. +0.j, 0. +0.j, 0.70710678+0.j, - 0.70710678+0.j], - [ 0.70710678+0.j, -0.70710678+0.j, 0. +0.j, - 0. +0.j]]) - """ - warnings.warn( - "Matrix returned will have qubits in little-endian order; " - "This method has been deprecated. Please use to_unitary() instead.", - category=DeprecationWarning, - ) - - qubits = self.qubits - if not qubits: - return np.zeros(0, dtype=complex) - qubit_count = max(qubits) + 1 - - return calculate_unitary(qubit_count, self.instructions) - def to_unitary(self) -> np.ndarray: """ Returns the unitary matrix representation of the entire circuit. @@ -1573,12 +1516,12 @@ def __eq__(self, other): ) return NotImplemented - def __call__(self, arg: Any = None, **kwargs) -> Circuit: + def __call__(self, arg: Any | None = None, **kwargs) -> Circuit: """ Implements the call function to easily make a bound Circuit. Args: - arg (Any): A value to bind to all parameters. Defaults to None and + arg (Any | None): A value to bind to all parameters. Defaults to None and can be overridden if the parameter is in kwargs. Returns: diff --git a/src/braket/circuits/circuit_diagram.py b/src/braket/circuits/circuit_diagram.py index ee09d73f7..5b156d290 100644 --- a/src/braket/circuits/circuit_diagram.py +++ b/src/braket/circuits/circuit_diagram.py @@ -10,6 +10,7 @@ # distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF # ANY KIND, either express or implied. See the License for the specific # language governing permissions and limitations under the License. + from __future__ import annotations from abc import ABC, abstractmethod diff --git a/src/braket/circuits/compiler_directive.py b/src/braket/circuits/compiler_directive.py index cd40a596c..628422c7e 100644 --- a/src/braket/circuits/compiler_directive.py +++ b/src/braket/circuits/compiler_directive.py @@ -13,11 +13,12 @@ from __future__ import annotations -from typing import Any, Sequence, Tuple +from collections.abc import Sequence +from typing import Any from braket.circuits.operator import Operator -from braket.circuits.qubit_set import QubitSet from braket.circuits.serialization import IRType, SerializationProperties +from braket.registers.qubit_set import QubitSet class CompilerDirective(Operator): @@ -41,26 +42,26 @@ def name(self) -> str: return self.__class__.__name__ @property - def ascii_symbols(self) -> Tuple[str, ...]: - """Tuple[str, ...]: Returns the ascii symbols for the compiler directive.""" + def ascii_symbols(self) -> tuple[str, ...]: + """tuple[str, ...]: Returns the ascii symbols for the compiler directive.""" return self._ascii_symbols def to_ir( self, - target: QubitSet = None, + target: QubitSet | None = None, ir_type: IRType = IRType.JAQCD, - serialization_properties: SerializationProperties = None, + serialization_properties: SerializationProperties | None = None, **kwargs, ) -> Any: """Returns IR object of the compiler directive. Args: - target (QubitSet): target qubit(s). Defaults to None + target (QubitSet | None): target qubit(s). Defaults to None ir_type(IRType) : The IRType to use for converting the compiler directive object to its IR representation. Defaults to IRType.JAQCD. - serialization_properties (SerializationProperties): The serialization properties to use - while serializing the object to the IR representation. The serialization properties - supplied must correspond to the supplied `ir_type`. Defaults to None. + serialization_properties (SerializationProperties | None): The serialization properties + to use while serializing the object to the IR representation. The serialization + properties supplied must correspond to the supplied `ir_type`. Defaults to None. Returns: Any: IR object of the compiler directive. diff --git a/src/braket/circuits/gate.py b/src/braket/circuits/gate.py index 81f2499d1..3c59409e5 100644 --- a/src/braket/circuits/gate.py +++ b/src/braket/circuits/gate.py @@ -13,17 +13,18 @@ from __future__ import annotations +from collections.abc import Sequence from itertools import groupby -from typing import Any, List, Optional, Sequence, Tuple, Type +from typing import Any, Optional from braket.circuits.basis_state import BasisState, BasisStateInput from braket.circuits.quantum_operator import QuantumOperator -from braket.circuits.qubit_set import QubitSet from braket.circuits.serialization import ( IRType, OpenQASMSerializationProperties, SerializationProperties, ) +from braket.registers.qubit_set import QubitSet class Gate(QuantumOperator): @@ -55,13 +56,13 @@ def __init__(self, qubit_count: Optional[int], ascii_symbols: Sequence[str]): def _qasm_name(self) -> NotImplementedError: raise NotImplementedError() - def adjoint(self) -> List[Gate]: + def adjoint(self) -> list[Gate]: """Returns a list of gates that implement the adjoint of this gate. This is a list because some gates do not have an inverse defined by a single existing gate. Returns: - List[Gate]: The gates comprising the adjoint of this gate. + list[Gate]: The gates comprising the adjoint of this gate. """ raise NotImplementedError(f"Gate {self.name} does not have adjoint implemented") @@ -69,7 +70,7 @@ def to_ir( self, target: QubitSet, ir_type: IRType = IRType.JAQCD, - serialization_properties: SerializationProperties = None, + serialization_properties: Optional[SerializationProperties] = None, *, control: Optional[QubitSet] = None, control_state: Optional[BasisStateInput] = None, @@ -81,9 +82,10 @@ def to_ir( target (QubitSet): target qubit(s). ir_type(IRType) : The IRType to use for converting the gate object to its IR representation. Defaults to IRType.JAQCD. - serialization_properties (SerializationProperties): The serialization properties to use - while serializing the object to the IR representation. The serialization properties - supplied must correspond to the supplied `ir_type`. Defaults to None. + serialization_properties (Optional[SerializationProperties]): The serialization + properties to use while serializing the object to the IR representation. The + serialization properties supplied must correspond to the supplied `ir_type`. + Defaults to None. control (Optional[QubitSet]): Control qubit(s). Only supported for OpenQASM. Default None. control_state (Optional[BasisStateInput]): Quantum state on which to control the @@ -200,8 +202,8 @@ def _to_openqasm( ) @property - def ascii_symbols(self) -> Tuple[str, ...]: - """Tuple[str, ...]: Returns the ascii symbols for the quantum operator.""" + def ascii_symbols(self) -> tuple[str, ...]: + """tuple[str, ...]: Returns the ascii symbols for the quantum operator.""" return self._ascii_symbols def __eq__(self, other): @@ -214,10 +216,10 @@ def __hash__(self): return hash((self.name, self.qubit_count)) @classmethod - def register_gate(cls, gate: Type[Gate]) -> None: + def register_gate(cls, gate: type[Gate]) -> None: """Register a gate implementation by adding it into the Gate class. Args: - gate (Type[Gate]): Gate class to register. + gate (type[Gate]): Gate class to register. """ setattr(cls, gate.__name__, gate) diff --git a/src/braket/circuits/gate_calibrations.py b/src/braket/circuits/gate_calibrations.py index 01694a1e7..6cbdd97d1 100644 --- a/src/braket/circuits/gate_calibrations.py +++ b/src/braket/circuits/gate_calibrations.py @@ -14,16 +14,16 @@ from __future__ import annotations from copy import deepcopy -from typing import Any, Dict, List, Optional, Tuple +from typing import Any, Optional from braket.circuits.gate import Gate -from braket.circuits.qubit_set import QubitSet from braket.circuits.serialization import ( IRType, OpenQASMSerializationProperties, QubitReferenceType, ) from braket.pulse.pulse_sequence import PulseSequence +from braket.registers.qubit_set import QubitSet class GateCalibrations: @@ -35,23 +35,23 @@ class GateCalibrations: def __init__( self, - pulse_sequences: Dict[Tuple[Gate, QubitSet], PulseSequence], + pulse_sequences: dict[tuple[Gate, QubitSet], PulseSequence], ): """ Args: - pulse_sequences (Dict[Tuple[Gate, QubitSet], PulseSequence]): A mapping containing a key of + pulse_sequences (dict[tuple[Gate, QubitSet], PulseSequence]): A mapping containing a key of `(Gate, QubitSet)` mapped to the corresponding pulse sequence. """ # noqa: E501 - self.pulse_sequences: Dict[Tuple[Gate, QubitSet], PulseSequence] = pulse_sequences + self.pulse_sequences: dict[tuple[Gate, QubitSet], PulseSequence] = pulse_sequences @property - def pulse_sequences(self) -> Dict[Tuple[Gate, QubitSet], PulseSequence]: + def pulse_sequences(self) -> dict[tuple[Gate, QubitSet], PulseSequence]: """ Gets the mapping of (Gate, Qubit) to the corresponding `PulseSequence`. Returns: - Dict[Tuple[Gate, QubitSet], PulseSequence]: The calibration data Dictionary. + dict[tuple[Gate, QubitSet], PulseSequence]: The calibration data Dictionary. """ return self._pulse_sequences @@ -64,7 +64,7 @@ def pulse_sequences(self, value: Any) -> None: value(Any): The value for the pulse_sequences property to be set to. Raises: - TypeError: Raised if the type is not Dict[Tuple[Gate, QubitSet], PulseSequence] + TypeError: Raised if the type is not dict[tuple[Gate, QubitSet], PulseSequence] """ if isinstance(value, dict) and all( @@ -75,7 +75,7 @@ def pulse_sequences(self, value: Any) -> None: else: raise TypeError( "The value for pulse_sequence must be of type: " - "Dict[Tuple[Gate, QubitSet], PulseSequence]" + "dict[tuple[Gate, QubitSet], PulseSequence]" ) def copy(self) -> GateCalibrations: @@ -91,13 +91,13 @@ def __len__(self): return len(self._pulse_sequences) def filter( - self, gates: Optional[List[Gate]] = None, qubits: Optional[QubitSet] = None + self, gates: Optional[list[Gate]] = None, qubits: Optional[QubitSet] = None ) -> Optional[GateCalibrations]: """ Filters the data based on optional lists of gates and QubitSets. Args: - gates (Optional[List[Gate]]): An optional list of gates to filter on. + gates (Optional[list[Gate]]): An optional list of gates to filter on. qubits (Optional[QubitSet]): An optional `QubitSet` to filter on. Returns: @@ -114,12 +114,12 @@ def filter( {k: v for (k, v) in self.pulse_sequences.items() if k in filtered_calibration_keys}, ) - def to_ir(self, calibration_key: Optional[Tuple[Gate, QubitSet]] = None) -> str: + def to_ir(self, calibration_key: Optional[tuple[Gate, QubitSet]] = None) -> str: """ Returns the defcal representation for the `GateCalibrations` object. Args: - calibration_key (Optional[Tuple[Gate, QubitSet]]): An optional key to get a specific defcal. + calibration_key (Optional[tuple[Gate, QubitSet]]): An optional key to get a specific defcal. Default: None Returns: @@ -143,7 +143,7 @@ def to_ir(self, calibration_key: Optional[Tuple[Gate, QubitSet]] = None) -> str: ) return defcal - def _def_cal_gate(self, gate_key: Tuple[Gate, QubitSet]) -> str: + def _def_cal_gate(self, gate_key: tuple[Gate, QubitSet]) -> str: return " ".join( [ "defcal", diff --git a/src/braket/circuits/gates.py b/src/braket/circuits/gates.py index ac57d05b4..9a4214c37 100644 --- a/src/braket/circuits/gates.py +++ b/src/braket/circuits/gates.py @@ -13,8 +13,9 @@ from __future__ import annotations +from collections.abc import Iterable from copy import deepcopy -from typing import Any, Iterable, List, Optional, Union +from typing import Any, Optional, Union import numpy as np from oqpy import Program @@ -39,11 +40,11 @@ is_unitary, verify_quantum_operator_matrix_dimensions, ) -from braket.circuits.qubit import QubitInput -from braket.circuits.qubit_set import QubitSet, QubitSetInput from braket.circuits.serialization import OpenQASMSerializationProperties from braket.pulse.ast.qasm_parser import ast_to_qasm from braket.pulse.pulse_sequence import PulseSequence +from braket.registers.qubit import QubitInput +from braket.registers.qubit_set import QubitSet, QubitSetInput """ To add a new gate: @@ -59,7 +60,14 @@ class H(Gate): - """Hadamard gate.""" + r"""Hadamard gate. + + Unitary matrix: + + .. math:: \mathtt{H} = \frac{1}{\sqrt{2}} \begin{bmatrix} + 1 & 1 \\ + 1 & -1 \end{bmatrix}. + """ def __init__(self): super().__init__(qubit_count=None, ascii_symbols=["H"]) @@ -68,7 +76,7 @@ def __init__(self): def _qasm_name(self) -> str: return "h" - def adjoint(self) -> List[Gate]: + def adjoint(self) -> list[Gate]: return [H()] def _to_jaqcd(self, target: QubitSet) -> Any: @@ -90,7 +98,13 @@ def h( control_state: Optional[BasisStateInput] = None, power: float = 1, ) -> Iterable[Instruction]: - """Registers this function into the circuit class. + r"""Hadamard gate. + + Unitary matrix: + + .. math:: \mathtt{H} = \frac{1}{\sqrt{2}} \begin{bmatrix} + 1 & 1 \\ + 1 & -1 \end{bmatrix}. Args: target (QubitSetInput): Target qubit(s) @@ -124,7 +138,14 @@ def h( class I(Gate): # noqa: E742, E261 - """Identity gate.""" + r"""Identity gate. + + Unitary matrix: + + .. math:: \mathtt{I} = \begin{bmatrix} + 1 & 0 \\ + 0 & 1 \end{bmatrix}. + """ def __init__(self): super().__init__(qubit_count=None, ascii_symbols=["I"]) @@ -133,7 +154,7 @@ def __init__(self): def _qasm_name(self) -> str: return "i" - def adjoint(self) -> List[Gate]: + def adjoint(self) -> list[Gate]: return [I()] def _to_jaqcd(self, target: QubitSet) -> Any: @@ -155,7 +176,13 @@ def i( control_state: Optional[BasisStateInput] = None, power: float = 1, ) -> Iterable[Instruction]: - """Registers this function into the circuit class. + r"""Identity gate. + + Unitary matrix: + + .. math:: \mathtt{I} = \begin{bmatrix} + 1 & 0 \\ + 0 & 1 \end{bmatrix}. Args: target (QubitSetInput): Target qubit(s) @@ -189,7 +216,15 @@ def i( class X(Gate): - """Pauli-X gate.""" + r"""Pauli-X gate. + + Unitary matrix: + + .. math:: \mathtt{X} = \begin{bmatrix} + 0 & 1 \\ + 1 & 0 + \end{bmatrix}. + """ def __init__(self): super().__init__(qubit_count=None, ascii_symbols=["X"]) @@ -198,7 +233,7 @@ def __init__(self): def _qasm_name(self) -> str: return "x" - def adjoint(self) -> List[Gate]: + def adjoint(self) -> list[Gate]: return [X()] def _to_jaqcd(self, target: QubitSet) -> Any: @@ -220,7 +255,14 @@ def x( control_state: Optional[BasisStateInput] = None, power: float = 1, ) -> Iterable[Instruction]: - """Registers this function into the circuit class. + r"""Pauli-X gate. + + Unitary matrix: + + .. math:: \mathtt{X} = \begin{bmatrix} + 0 & 1 \\ + 1 & 0 + \end{bmatrix}. Args: target (QubitSetInput): Target qubit(s) @@ -254,7 +296,15 @@ def x( class Y(Gate): - """Pauli-Y gate.""" + r"""Pauli-Y gate. + + Unitary matrix: + + .. math:: \mathtt{Y} = \begin{bmatrix} + 0 & -i \\ + i & 0 + \end{bmatrix}. + """ def __init__(self): super().__init__(qubit_count=None, ascii_symbols=["Y"]) @@ -263,7 +313,7 @@ def __init__(self): def _qasm_name(self) -> str: return "y" - def adjoint(self) -> List[Gate]: + def adjoint(self) -> list[Gate]: return [Y()] def _to_jaqcd(self, target: QubitSet) -> Any: @@ -285,7 +335,14 @@ def y( control_state: Optional[BasisStateInput] = None, power: float = 1, ) -> Iterable[Instruction]: - """Registers this function into the circuit class. + r"""Pauli-Y gate. + + Unitary matrix: + + .. math:: \mathtt{Y} = \begin{bmatrix} + 0 & -i \\ + i & 0 + \end{bmatrix}. Args: target (QubitSetInput): Target qubit(s) @@ -319,7 +376,15 @@ def y( class Z(Gate): - """Pauli-Z gate.""" + r"""Pauli-Z gate. + + Unitary matrix: + + .. math:: \mathtt{Z} = \begin{bmatrix} + 1 & 0 \\ + 0 & -1 + \end{bmatrix}. + """ def __init__(self): super().__init__(qubit_count=None, ascii_symbols=["Z"]) @@ -328,7 +393,7 @@ def __init__(self): def _qasm_name(self) -> str: return "z" - def adjoint(self) -> List[Gate]: + def adjoint(self) -> list[Gate]: return [Z()] def _to_jaqcd(self, target: QubitSet) -> Any: @@ -350,7 +415,12 @@ def z( control_state: Optional[BasisStateInput] = None, power: float = 1, ) -> Iterable[Instruction]: - """Registers this function into the circuit class. + r"""Pauli-Z gate. + + .. math:: \mathtt{Z} = \begin{bmatrix} + 1 & 0 \\ + 0 & -1 + \end{bmatrix}. Args: target (QubitSetInput): Target qubit(s) @@ -384,7 +454,15 @@ def z( class S(Gate): - """S gate.""" + r"""S gate. + + Unitary matrix: + + .. math:: \mathtt{S} = \begin{bmatrix} + 1 & 0 \\ + 0 & i + \end{bmatrix}. + """ def __init__(self): super().__init__(qubit_count=None, ascii_symbols=["S"]) @@ -393,7 +471,7 @@ def __init__(self): def _qasm_name(self) -> str: return "s" - def adjoint(self) -> List[Gate]: + def adjoint(self) -> list[Gate]: return [Si()] def _to_jaqcd(self, target: QubitSet) -> Any: @@ -415,7 +493,12 @@ def s( control_state: Optional[BasisStateInput] = None, power: float = 1, ) -> Iterable[Instruction]: - """Registers this function into the circuit class. + r"""S gate. + + .. math:: \mathtt{S} = \begin{bmatrix} + 1 & 0 \\ + 0 & i + \end{bmatrix}. Args: target (QubitSetInput): Target qubit(s) @@ -449,7 +532,15 @@ def s( class Si(Gate): - """Conjugate transpose of S gate.""" + r"""Conjugate transpose of S gate. + + Unitary matrix: + + .. math:: \mathtt{S}^\dagger = \begin{bmatrix} + 1 & 0 \\ + 0 & -i + \end{bmatrix}. + """ def __init__(self): super().__init__(qubit_count=None, ascii_symbols=["Si"]) @@ -458,7 +549,7 @@ def __init__(self): def _qasm_name(self) -> str: return "si" - def adjoint(self) -> List[Gate]: + def adjoint(self) -> list[Gate]: return [S()] def _to_jaqcd(self, target: QubitSet) -> Any: @@ -480,7 +571,12 @@ def si( control_state: Optional[BasisStateInput] = None, power: float = 1, ) -> Iterable[Instruction]: - """Registers this function into the circuit class. + r"""Conjugate transpose of S gate. + + .. math:: \mathtt{S}^\dagger = \begin{bmatrix} + 1 & 0 \\ + 0 & -i + \end{bmatrix}. Args: target (QubitSetInput): Target qubit(s) @@ -514,7 +610,15 @@ def si( class T(Gate): - """T gate.""" + r"""T gate. + + Unitary matrix: + + .. math:: \mathtt{T} = \begin{bmatrix} + 1 & 0 \\ + 0 & e^{i \pi/4} + \end{bmatrix}. + """ def __init__(self): super().__init__(qubit_count=None, ascii_symbols=["T"]) @@ -523,7 +627,7 @@ def __init__(self): def _qasm_name(self) -> str: return "t" - def adjoint(self) -> List[Gate]: + def adjoint(self) -> list[Gate]: return [Ti()] def _to_jaqcd(self, target: QubitSet) -> Any: @@ -545,7 +649,12 @@ def t( control_state: Optional[BasisStateInput] = None, power: float = 1, ) -> Iterable[Instruction]: - """Registers this function into the circuit class. + r"""T gate. + + .. math:: \mathtt{T} = \begin{bmatrix} + 1 & 0 \\ + 0 & e^{i \pi/4} + \end{bmatrix}. Args: target (QubitSetInput): Target qubit(s) @@ -579,7 +688,15 @@ def t( class Ti(Gate): - """Conjugate transpose of T gate.""" + r"""Conjugate transpose of T gate. + + Unitary matrix: + + .. math:: \mathtt{T}^\dagger = \begin{bmatrix} + 1 & 0 \\ + 0 & e^{-i \pi/4} + \end{bmatrix}. + """ def __init__(self): super().__init__(qubit_count=None, ascii_symbols=["Ti"]) @@ -588,7 +705,7 @@ def __init__(self): def _qasm_name(self) -> str: return "ti" - def adjoint(self) -> List[Gate]: + def adjoint(self) -> list[Gate]: return [T()] def _to_jaqcd(self, target: QubitSet) -> Any: @@ -610,7 +727,12 @@ def ti( control_state: Optional[BasisStateInput] = None, power: float = 1, ) -> Iterable[Instruction]: - """Registers this function into the circuit class. + r"""Conjugate transpose of T gate. + + .. math:: \mathtt{T}^\dagger = \begin{bmatrix} + 1 & 0 \\ + 0 & e^{-i \pi/4} + \end{bmatrix}. Args: target (QubitSetInput): Target qubit(s) @@ -644,7 +766,15 @@ def ti( class V(Gate): - """Square root of not gate.""" + r"""Square root of X gate (V gate). + + Unitary matrix: + + .. math:: \mathtt{V} = \frac{1}{2}\begin{bmatrix} + 1+i & 1-i \\ + 1-i & 1+i + \end{bmatrix}. + """ def __init__(self): super().__init__(qubit_count=None, ascii_symbols=["V"]) @@ -653,7 +783,7 @@ def __init__(self): def _qasm_name(self) -> str: return "v" - def adjoint(self) -> List[Gate]: + def adjoint(self) -> list[Gate]: return [Vi()] def _to_jaqcd(self, target: QubitSet) -> Any: @@ -675,7 +805,12 @@ def v( control_state: Optional[BasisStateInput] = None, power: float = 1, ) -> Iterable[Instruction]: - """Registers this function into the circuit class. + r"""Square root of X gate (V gate). + + .. math:: \mathtt{V} = \frac{1}{2}\begin{bmatrix} + 1+i & 1-i \\ + 1-i & 1+i + \end{bmatrix}. Args: target (QubitSetInput): Target qubit(s) @@ -709,7 +844,15 @@ def v( class Vi(Gate): - """Conjugate transpose of square root of not gate.""" + r"""Conjugate transpose of square root of X gate (conjugate transpose of V). + + Unitary matrix: + + .. math:: \mathtt{V}^\dagger = \frac{1}{2}\begin{bmatrix} + 1-i & 1+i \\ + 1+i & 1-i + \end{bmatrix}. + """ def __init__(self): super().__init__(qubit_count=None, ascii_symbols=["Vi"]) @@ -718,7 +861,7 @@ def __init__(self): def _qasm_name(self) -> str: return "vi" - def adjoint(self) -> List[Gate]: + def adjoint(self) -> list[Gate]: return [V()] def _to_jaqcd(self, target: QubitSet) -> Any: @@ -740,7 +883,12 @@ def vi( control_state: Optional[BasisStateInput] = None, power: float = 1, ) -> Iterable[Instruction]: - """Registers this function into the circuit class. + r"""Conjugate transpose of square root of X gate (conjugate transpose of V). + + .. math:: \mathtt{V}^\dagger = \frac{1}{2}\begin{bmatrix} + 1-i & 1+i \\ + 1+i & 1-i + \end{bmatrix}. Args: target (QubitSetInput): Target qubit(s) @@ -777,7 +925,14 @@ def vi( class Rx(AngledGate): - """X-axis rotation gate. + r"""X-axis rotation gate. + + Unitary matrix: + + .. math:: \mathtt{R_x}(\phi) = \begin{bmatrix} + \cos{(\phi/2)} & -i \sin{(\phi/2)} \\ + -i \sin{(\phi/2)} & \cos{(\phi/2)} + \end{bmatrix}. Args: angle (Union[FreeParameterExpression, float]): angle in radians. @@ -798,7 +953,7 @@ def _to_jaqcd(self, target: QubitSet, **kwargs) -> Any: return ir.Rx.construct(target=target[0], angle=self.angle) def to_matrix(self) -> np.ndarray: - """Returns a matrix representation of this gate. + r"""Returns a matrix representation of this gate. Returns: ndarray: The matrix representation of this gate. """ @@ -823,7 +978,12 @@ def rx( control_state: Optional[BasisStateInput] = None, power: float = 1, ) -> Iterable[Instruction]: - """Registers this function into the circuit class. + r"""X-axis rotation gate. + + .. math:: \mathtt{R_x}(\phi) = \begin{bmatrix} + \cos{(\phi/2)} & -i \sin{(\phi/2)} \\ + -i \sin{(\phi/2)} & \cos{(\phi/2)} + \end{bmatrix}. Args: target (QubitSetInput): Target qubit(s). @@ -857,7 +1017,14 @@ def rx( class Ry(AngledGate): - """Y-axis rotation gate. + r"""Y-axis rotation gate. + + Unitary matrix: + + .. math:: \mathtt{R_y}(\phi) = \begin{bmatrix} + \cos{(\phi/2)} & -\sin{(\phi/2)} \\ + \sin{(\phi/2)} & \cos{(\phi/2)} + \end{bmatrix}. Args: angle (Union[FreeParameterExpression, float]): angle in radians. @@ -878,7 +1045,7 @@ def _to_jaqcd(self, target: QubitSet) -> Any: return ir.Ry.construct(target=target[0], angle=self.angle) def to_matrix(self) -> np.ndarray: - """Returns a matrix representation of this gate. + r"""Returns a matrix representation of this gate. Returns: ndarray: The matrix representation of this gate. """ @@ -903,7 +1070,12 @@ def ry( control_state: Optional[BasisStateInput] = None, power: float = 1, ) -> Iterable[Instruction]: - """Registers this function into the circuit class. + r"""Y-axis rotation gate. + + .. math:: \mathtt{R_y}(\phi) = \begin{bmatrix} + \cos{(\phi/2)} & -\sin{(\phi/2)} \\ + \sin{(\phi/2)} & \cos{(\phi/2)} + \end{bmatrix}. Args: target (QubitSetInput): Target qubit(s). @@ -922,6 +1094,7 @@ def ry( Returns: Iterable[Instruction]: Rx instruction. + Examples: >>> circ = Circuit().ry(0, 0.15) """ @@ -937,7 +1110,14 @@ def ry( class Rz(AngledGate): - """Z-axis rotation gate. + r"""Z-axis rotation gate. + + Unitary matrix: + + .. math:: \mathtt{R_z}(\phi) = \begin{bmatrix} + e^{-i \phi/2} & 0 \\ + 0 & e^{i \phi/2} + \end{bmatrix}. Args: angle (Union[FreeParameterExpression, float]): angle in radians. @@ -979,7 +1159,12 @@ def rz( control_state: Optional[BasisStateInput] = None, power: float = 1, ) -> Iterable[Instruction]: - """Registers this function into the circuit class. + r"""Z-axis rotation gate. + + .. math:: \mathtt{R_z}(\phi) = \begin{bmatrix} + e^{-i \phi/2} & 0 \\ + 0 & e^{i \phi/2} + \end{bmatrix}. Args: target (QubitSetInput): Target qubit(s). @@ -1013,7 +1198,14 @@ def rz( class PhaseShift(AngledGate): - """Phase shift gate. + r"""Phase shift gate. + + Unitary matrix: + + .. math:: \mathtt{PhaseShift}(\phi) = \begin{bmatrix} + 1 & 0 \\ + 0 & e^{i \phi} + \end{bmatrix} Args: angle (Union[FreeParameterExpression, float]): angle in radians. @@ -1053,7 +1245,12 @@ def phaseshift( control_state: Optional[BasisStateInput] = None, power: float = 1, ) -> Iterable[Instruction]: - """Registers this function into the circuit class. + r"""Phase shift gate. + + .. math:: \mathtt{PhaseShift}(\phi) = \begin{bmatrix} + 1 & 0 \\ + 0 & e^{i \phi} + \end{bmatrix} Args: target (QubitSetInput): Target qubit(s). @@ -1094,7 +1291,17 @@ def phaseshift( class CNot(Gate): - """Controlled NOT gate.""" + r"""Controlled NOT gate. + + Unitary matrix: + + .. math:: \mathtt{CNOT} = \begin{bmatrix} + 1 & 0 & 0 & 0 \\ + 0 & 1 & 0 & 0 \\ + 0 & 0 & 0 & 1 \\ + 0 & 0 & 1 & 0 \\ + \end{bmatrix}. + """ def __init__(self): super().__init__(qubit_count=None, ascii_symbols=["C", "X"]) @@ -1103,7 +1310,7 @@ def __init__(self): def _qasm_name(self) -> str: return "cnot" - def adjoint(self) -> List[Gate]: + def adjoint(self) -> list[Gate]: return [CNot()] def _to_jaqcd(self, target: QubitSet) -> Any: @@ -1127,7 +1334,14 @@ def fixed_qubit_count() -> int: @staticmethod @circuit.subroutine(register=True) def cnot(control: QubitSetInput, target: QubitInput, power: float = 1) -> Instruction: - """Registers this function into the circuit class. + r"""Controlled NOT gate. + + .. math:: \mathtt{CNOT} = \begin{bmatrix} + 1 & 0 & 0 & 0 \\ + 0 & 1 & 0 & 0 \\ + 0 & 0 & 0 & 1 \\ + 0 & 0 & 1 & 0 \\ + \end{bmatrix}. Args: control (QubitSetInput): Control qubit(s). The last control qubit @@ -1154,7 +1368,17 @@ def cnot(control: QubitSetInput, target: QubitInput, power: float = 1) -> Instru class Swap(Gate): - """Swap gate.""" + r"""Swap gate. + + Unitary matrix: + + .. math:: \mathtt{SWAP} = \begin{bmatrix} + 1 & 0 & 0 & 0 \\ + 0 & 0 & 1 & 0 \\ + 0 & 1 & 0 & 0 \\ + 0 & 0 & 0 & 1 \\ + \end{bmatrix}. + """ def __init__(self): super().__init__(qubit_count=None, ascii_symbols=["SWAP", "SWAP"]) @@ -1163,7 +1387,7 @@ def __init__(self): def _qasm_name(self) -> str: return "swap" - def adjoint(self) -> List[Gate]: + def adjoint(self) -> list[Gate]: return [Swap()] def _to_jaqcd(self, target: QubitSet) -> Any: @@ -1194,7 +1418,14 @@ def swap( control_state: Optional[BasisStateInput] = None, power: float = 1, ) -> Instruction: - """Registers this function into the circuit class. + r"""Swap gate. + + .. math:: \mathtt{SWAP} = \begin{bmatrix} + 1 & 0 & 0 & 0 \\ + 0 & 0 & 1 & 0 \\ + 0 & 1 & 0 & 0 \\ + 0 & 0 & 0 & 1 \\ + \end{bmatrix}. Args: target1 (QubitInput): Target qubit 1 index. @@ -1229,7 +1460,17 @@ def swap( class ISwap(Gate): - """ISwap gate.""" + r"""ISwap gate. + + Unitary matrix: + + .. math:: \mathtt{iSWAP} = \begin{bmatrix} + 1 & 0 & 0 & 0 \\ + 0 & 0 & i & 0 \\ + 0 & i & 0 & 0 \\ + 0 & 0 & 0 & 1 \\ + \end{bmatrix}. + """ def __init__(self): super().__init__(qubit_count=None, ascii_symbols=["ISWAP", "ISWAP"]) @@ -1238,7 +1479,7 @@ def __init__(self): def _qasm_name(self) -> str: return "iswap" - def adjoint(self) -> List[Gate]: + def adjoint(self) -> list[Gate]: return [self, self, self] def _to_jaqcd(self, target: QubitSet) -> Any: @@ -1269,7 +1510,14 @@ def iswap( control_state: Optional[BasisStateInput] = None, power: float = 1, ) -> Instruction: - """Registers this function into the circuit class. + r"""ISwap gate. + + .. math:: \mathtt{iSWAP} = \begin{bmatrix} + 1 & 0 & 0 & 0 \\ + 0 & 0 & i & 0 \\ + 0 & i & 0 & 0 \\ + 0 & 0 & 0 & 1 \\ + \end{bmatrix}. Args: target1 (QubitInput): Target qubit 1 index. @@ -1304,7 +1552,16 @@ def iswap( class PSwap(AngledGate): - """PSwap gate. + r"""PSwap gate. + + Unitary matrix: + + .. math:: \mathtt{PSWAP}(\phi) = \begin{bmatrix} + 1 & 0 & 0 & 0 \\ + 0 & 0 & e^{i \phi} & 0 \\ + 0 & e^{i \phi} & 0 & 0 \\ + 0 & 0 & 0 & 1 \\ + \end{bmatrix}. Args: angle (Union[FreeParameterExpression, float]): angle in radians. @@ -1356,7 +1613,14 @@ def pswap( control_state: Optional[BasisStateInput] = None, power: float = 1, ) -> Instruction: - """Registers this function into the circuit class. + r"""PSwap gate. + + .. math:: \mathtt{PSWAP}(\phi) = \begin{bmatrix} + 1 & 0 & 0 & 0 \\ + 0 & 0 & e^{i \phi} & 0 \\ + 0 & e^{i \phi} & 0 & 0 \\ + 0 & 0 & 0 & 1 \\ + \end{bmatrix}. Args: target1 (QubitInput): Target qubit 1 index. @@ -1392,10 +1656,20 @@ def pswap( class XY(AngledGate): - """XY gate. + r"""XY gate. + + Unitary matrix: + + .. math:: \mathtt{XY}(\phi) = \begin{bmatrix} + 1 & 0 & 0 & 0 \\ + 0 & \cos{(\phi/2)} & i\sin{(\phi/2)} & 0 \\ + 0 & i\sin{(\phi/2)} & \cos{(\phi/2)} & 0 \\ + 0 & 0 & 0 & 1 \\ + \end{bmatrix}. Reference: https://arxiv.org/abs/1912.04424v1 + Args: angle (Union[FreeParameterExpression, float]): angle in radians. """ @@ -1418,7 +1692,7 @@ def _to_jaqcd(self, target: QubitSet) -> Any: return ir.XY.construct(targets=[target[0], target[1]], angle=self.angle) def to_matrix(self) -> np.ndarray: - """Returns a matrix representation of this gate. + r"""Returns a matrix representation of this gate. Returns: ndarray: The matrix representation of this gate. """ @@ -1452,7 +1726,14 @@ def xy( control_state: Optional[BasisStateInput] = None, power: float = 1, ) -> Instruction: - """Registers this function into the circuit class. + r"""XY gate. + + .. math:: \mathtt{XY}(\phi) = \begin{bmatrix} + 1 & 0 & 0 & 0 \\ + 0 & \cos{(\phi/2)} & i\sin{(\phi/2)} & 0 \\ + 0 & i\sin{(\phi/2)} & \cos{(\phi/2)} & 0 \\ + 0 & 0 & 0 & 1 \\ + \end{bmatrix}. Args: target1 (QubitInput): Target qubit 1 index. @@ -1488,7 +1769,16 @@ def xy( class CPhaseShift(AngledGate): - """Controlled phase shift gate. + r"""Controlled phase shift gate. + + Unitary matrix: + + .. math:: \mathtt{CPhaseShift}(\phi) = \begin{bmatrix} + 1 & 0 & 0 & 0 \\ + 0 & 1 & 0 & 0 \\ + 0 & 0 & 1 & 0 \\ + 0 & 0 & 0 & e^{i \phi} + \end{bmatrix}. Args: angle (Union[FreeParameterExpression, float]): angle in radians. @@ -1526,7 +1816,14 @@ def cphaseshift( angle: Union[FreeParameterExpression, float], power: float = 1, ) -> Instruction: - """Registers this function into the circuit class. + r"""Controlled phase shift gate. + + .. math:: \mathtt{CPhaseShift}(\phi) = \begin{bmatrix} + 1 & 0 & 0 & 0 \\ + 0 & 1 & 0 & 0 \\ + 0 & 0 & 1 & 0 \\ + 0 & 0 & 0 & e^{i \phi} + \end{bmatrix}. Args: control (QubitSetInput): Control qubit(s). The last control qubit @@ -1557,7 +1854,16 @@ def cphaseshift( class CPhaseShift00(AngledGate): - """Controlled phase shift gate for phasing the \\|00> state. + r"""Controlled phase shift gate for phasing the \|00> state. + + Unitary matrix: + + .. math:: \mathtt{CPhaseShift00}(\phi) = \begin{bmatrix} + e^{i \phi} & 0 & 0 & 0 \\ + 0 & 1 & 0 & 0 \\ + 0 & 0 & 1 & 0 \\ + 0 & 0 & 0 & 1 + \end{bmatrix}. Args: angle (Union[FreeParameterExpression, float]): angle in radians. @@ -1595,7 +1901,14 @@ def cphaseshift00( angle: Union[FreeParameterExpression, float], power: float = 1, ) -> Instruction: - """Registers this function into the circuit class. + r"""Controlled phase shift gate for phasing the \|00> state. + + .. math:: \mathtt{CPhaseShift00}(\phi) = \begin{bmatrix} + e^{i \phi} & 0 & 0 & 0 \\ + 0 & 1 & 0 & 0 \\ + 0 & 0 & 1 & 0 \\ + 0 & 0 & 0 & 1 + \end{bmatrix}. Args: control (QubitSetInput): Control qubit(s). The last control qubit @@ -1626,7 +1939,16 @@ def cphaseshift00( class CPhaseShift01(AngledGate): - """Controlled phase shift gate for phasing the \\|01> state. + r"""Controlled phase shift gate for phasing the \|01> state. + + Unitary matrix: + + .. math:: \mathtt{CPhaseShift01}(\phi) = \begin{bmatrix} + 1 & 0 & 0 & 0 \\ + 0 & e^{i \phi} & 0 & 0 \\ + 0 & 0 & 1 & 0 \\ + 0 & 0 & 0 & 1 + \end{bmatrix}. Args: angle (Union[FreeParameterExpression, float]): angle in radians. @@ -1664,7 +1986,14 @@ def cphaseshift01( angle: Union[FreeParameterExpression, float], power: float = 1, ) -> Instruction: - """Registers this function into the circuit class. + r"""Controlled phase shift gate for phasing the \|01> state. + + .. math:: \mathtt{CPhaseShift01}(\phi) = \begin{bmatrix} + 1 & 0 & 0 & 0 \\ + 0 & e^{i \phi} & 0 & 0 \\ + 0 & 0 & 1 & 0 \\ + 0 & 0 & 0 & 1 + \end{bmatrix}. Args: control (QubitSetInput): Control qubit(s). The last control qubit @@ -1695,7 +2024,16 @@ def cphaseshift01( class CPhaseShift10(AngledGate): - """Controlled phase shift gate for phasing the \\|10> state. + r"""Controlled phase shift gate for phasing the \\|10> state. + + Unitary matrix: + + .. math:: \mathtt{CPhaseShift10}(\phi) = \begin{bmatrix} + 1 & 0 & 0 & 0 \\ + 0 & 1 & 0 & 0 \\ + 0 & 0 & e^{i \phi} & 0 \\ + 0 & 0 & 0 & 1 + \end{bmatrix}. Args: angle (Union[FreeParameterExpression, float]): angle in radians. @@ -1733,7 +2071,14 @@ def cphaseshift10( angle: Union[FreeParameterExpression, float], power: float = 1, ) -> Instruction: - """Registers this function into the circuit class. + r"""Controlled phase shift gate for phasing the \\|10> state. + + .. math:: \mathtt{CPhaseShift10}(\phi) = \begin{bmatrix} + 1 & 0 & 0 & 0 \\ + 0 & 1 & 0 & 0 \\ + 0 & 0 & e^{i \phi} & 0 \\ + 0 & 0 & 0 & 1 + \end{bmatrix}. Args: control (QubitSetInput): Control qubit(s). The last control qubit @@ -1764,7 +2109,17 @@ def cphaseshift10( class CV(Gate): - """Controlled Sqrt of NOT gate.""" + r"""Controlled Sqrt of X gate. + + Unitary matrix: + + .. math:: \mathtt{CV} = \begin{bmatrix} + 1 & 0 & 0 & 0 \\ + 0 & 1 & 0 & 0 \\ + 0 & 0 & 0.5+0.5i & 0.5-0.5i \\ + 0 & 0 & 0.5-0.5i & 0.5+0.5i + \end{bmatrix}. + """ def __init__(self): super().__init__(qubit_count=None, ascii_symbols=["C", "V"]) @@ -1773,7 +2128,7 @@ def __init__(self): def _qasm_name(self) -> str: return "cv" - def adjoint(self) -> List[Gate]: + def adjoint(self) -> list[Gate]: return [self, self, self] def _to_jaqcd(self, target: QubitSet) -> Any: @@ -1797,7 +2152,14 @@ def fixed_qubit_count() -> int: @staticmethod @circuit.subroutine(register=True) def cv(control: QubitSetInput, target: QubitInput, power: float = 1) -> Instruction: - """Registers this function into the circuit class. + r"""Controlled Sqrt of X gate. + + .. math:: \mathtt{CV} = \begin{bmatrix} + 1 & 0 & 0 & 0 \\ + 0 & 1 & 0 & 0 \\ + 0 & 0 & 0.5+0.5i & 0.5-0.5i \\ + 0 & 0 & 0.5-0.5i & 0.5+0.5i + \end{bmatrix}. Args: control (QubitSetInput): Control qubit(s). The last control qubit @@ -1824,7 +2186,17 @@ def cv(control: QubitSetInput, target: QubitInput, power: float = 1) -> Instruct class CY(Gate): - """Controlled Pauli-Y gate.""" + r"""Controlled Pauli-Y gate. + + Unitary matrix: + + .. math:: \mathtt{CY} = \begin{bmatrix} + 1 & 0 & 0 & 0 \\ + 0 & 1 & 0 & 0 \\ + 0 & 0 & 0 & -i \\ + 0 & 0 & i & 0 + \end{bmatrix}. + """ def __init__(self): super().__init__(qubit_count=None, ascii_symbols=["C", "Y"]) @@ -1833,7 +2205,7 @@ def __init__(self): def _qasm_name(self) -> str: return "cy" - def adjoint(self) -> List[Gate]: + def adjoint(self) -> list[Gate]: return [CY()] def _to_jaqcd(self, target: QubitSet) -> Any: @@ -1857,7 +2229,14 @@ def fixed_qubit_count() -> int: @staticmethod @circuit.subroutine(register=True) def cy(control: QubitSetInput, target: QubitInput, power: float = 1) -> Instruction: - """Registers this function into the circuit class. + r"""Controlled Pauli-Y gate. + + .. math:: \mathtt{CY} = \begin{bmatrix} + 1 & 0 & 0 & 0 \\ + 0 & 1 & 0 & 0 \\ + 0 & 0 & 0 & -i \\ + 0 & 0 & i & 0 + \end{bmatrix}. Args: control (QubitSetInput): Control qubit(s). The last control qubit @@ -1884,7 +2263,17 @@ def cy(control: QubitSetInput, target: QubitInput, power: float = 1) -> Instruct class CZ(Gate): - """Controlled Pauli-Z gate.""" + r"""Controlled Pauli-Z gate. + + Unitary matrix: + + .. math:: \mathtt{CZ} = \begin{bmatrix} + 1 & 0 & 0 & 0 \\ + 0 & 1 & 0 & 0 \\ + 0 & 0 & 1 & 0 \\ + 0 & 0 & 0 & -1 + \end{bmatrix}. + """ def __init__(self): super().__init__(qubit_count=None, ascii_symbols=["C", "Z"]) @@ -1893,7 +2282,7 @@ def __init__(self): def _qasm_name(self) -> str: return "cz" - def adjoint(self) -> List[Gate]: + def adjoint(self) -> list[Gate]: return [CZ()] def _to_jaqcd(self, target: QubitSet) -> Any: @@ -1909,7 +2298,14 @@ def fixed_qubit_count() -> int: @staticmethod @circuit.subroutine(register=True) def cz(control: QubitSetInput, target: QubitInput, power: float = 1) -> Instruction: - """Registers this function into the circuit class. + r"""Controlled Pauli-Z gate. + + .. math:: \mathtt{CZ} = \begin{bmatrix} + 1 & 0 & 0 & 0 \\ + 0 & 1 & 0 & 0 \\ + 0 & 0 & 1 & 0 \\ + 0 & 0 & 0 & -1 + \end{bmatrix}. Args: control (QubitSetInput): Control qubit(s). The last control qubit @@ -1936,7 +2332,17 @@ def cz(control: QubitSetInput, target: QubitInput, power: float = 1) -> Instruct class ECR(Gate): - """An echoed RZX(pi/2) gate.""" + r"""An echoed RZX(pi/2) gate (ECR gate). + + Unitary matrix: + + .. math:: \mathtt{ECR} = \begin{bmatrix} + 0 & 0 & 1 & i \\ + 0 & 0 & i & 1 \\ + 1 & -i & 0 & 0 \\ + -i & 1 & 0 & 0 + \end{bmatrix}. + """ def __init__(self): super().__init__(qubit_count=None, ascii_symbols=["ECR", "ECR"]) @@ -1945,7 +2351,7 @@ def __init__(self): def _qasm_name(self) -> str: return "ecr" - def adjoint(self) -> List[Gate]: + def adjoint(self) -> list[Gate]: return [ECR()] def _to_jaqcd(self, target: QubitSet) -> Any: @@ -1975,7 +2381,14 @@ def ecr( control_state: Optional[BasisStateInput] = None, power: float = 1, ) -> Instruction: - """Registers this function into the circuit class. + r"""An echoed RZX(pi/2) gate (ECR gate). + + .. math:: \mathtt{ECR} = \begin{bmatrix} + 0 & 0 & 1 & i \\ + 0 & 0 & i & 1 \\ + 1 & -i & 0 & 0 \\ + -i & 1 & 0 & 0 + \end{bmatrix}. Args: target1 (QubitInput): Target qubit 1 index. @@ -2010,7 +2423,16 @@ def ecr( class XX(AngledGate): - """Ising XX coupling gate. + r"""Ising XX coupling gate. + + Unitary matrix: + + .. math:: \mathtt{XX}(\phi) = \begin{bmatrix} + \cos{(\phi/2)} & 0 & 0 & -i \sin{(\phi/2)} \\ + 0 & \cos{(\phi/2)} & -i \sin{(\phi/2)} & 0 \\ + 0 & -i \sin{(\phi/2)} & \cos{(\phi/2)} & 0 \\ + -i \sin{(\phi/2)} & 0 & 0 & \cos{(\phi/2)} + \end{bmatrix}. Reference: https://arxiv.org/abs/1707.06356 @@ -2036,7 +2458,7 @@ def _to_jaqcd(self, target: QubitSet) -> Any: return ir.XX.construct(targets=[target[0], target[1]], angle=self.angle) def to_matrix(self) -> np.ndarray: - """Returns a matrix representation of this gate. + r"""Returns a matrix representation of this gate. Returns: ndarray: The matrix representation of this gate. """ @@ -2070,7 +2492,14 @@ def xx( control_state: Optional[BasisStateInput] = None, power: float = 1, ) -> Instruction: - """Registers this function into the circuit class. + r"""Ising XX coupling gate. + + .. math:: \mathtt{XX}(\phi) = \begin{bmatrix} + \cos{(\phi/2)} & 0 & 0 & -i \sin{(\phi/2)} \\ + 0 & \cos{(\phi/2)} & -i \sin{(\phi/2)} & 0 \\ + 0 & -i \sin{(\phi/2)} & \cos{(\phi/2)} & 0 \\ + -i \sin{(\phi/2)} & 0 & 0 & \cos{(\phi/2)} + \end{bmatrix}. Args: target1 (QubitInput): Target qubit 1 index. @@ -2106,7 +2535,16 @@ def xx( class YY(AngledGate): - """Ising YY coupling gate. + r"""Ising YY coupling gate. + + Unitary matrix: + + .. math:: \mathtt{YY}(\phi) = \begin{bmatrix} + \cos{(\phi/2)} & 0 & 0 & i \sin{(\phi/2)} \\ + 0 & \cos{(\phi/2)} & -i \sin{(\phi/2)} & 0 \\ + 0 & -i \sin{(\phi/2)} & \cos{(\phi/2)} & 0 \\ + i \sin{(\phi/2)} & 0 & 0 & \cos{(\phi/2)} + \end{bmatrix}. Reference: https://arxiv.org/abs/1707.06356 @@ -2132,7 +2570,7 @@ def _to_jaqcd(self, target: QubitSet) -> Any: return ir.YY.construct(targets=[target[0], target[1]], angle=self.angle) def to_matrix(self) -> np.ndarray: - """Returns a matrix representation of this gate. + r"""Returns a matrix representation of this gate. Returns: ndarray: The matrix representation of this gate. """ @@ -2166,7 +2604,14 @@ def yy( control_state: Optional[BasisStateInput] = None, power: float = 1, ) -> Instruction: - """Registers this function into the circuit class. + r"""Ising YY coupling gate. + + .. math:: \mathtt{YY}(\phi) = \begin{bmatrix} + \cos{(\phi/2)} & 0 & 0 & i \sin{(\phi/2)} \\ + 0 & \cos{(\phi/2)} & -i \sin{(\phi/2)} & 0 \\ + 0 & -i \sin{(\phi/2)} & \cos{(\phi/2)} & 0 \\ + i \sin{(\phi/2)} & 0 & 0 & \cos{(\phi/2)} + \end{bmatrix}. Args: target1 (QubitInput): Target qubit 1 index. @@ -2202,7 +2647,16 @@ def yy( class ZZ(AngledGate): - """Ising ZZ coupling gate. + r"""Ising ZZ coupling gate. + + Unitary matrix: + + .. math:: \mathtt{ZZ}(\phi) = \begin{bmatrix} + e^{-i\phi/2} & 0 & 0 & 0 \\ + 0 & e^{i\phi/2} & 0 & 0 \\ + 0 & 0 & e^{i\phi/2} & 0 \\ + 0 & 0 & 0 & e^{-i\phi/2} + \end{bmatrix}. Reference: https://arxiv.org/abs/1707.06356 @@ -2256,7 +2710,14 @@ def zz( control_state: Optional[BasisStateInput] = None, power: float = 1, ) -> Instruction: - """Registers this function into the circuit class. + r"""Ising ZZ coupling gate. + + .. math:: \mathtt{ZZ}(\phi) = \begin{bmatrix} + e^{-i\phi/2} & 0 & 0 & 0 \\ + 0 & e^{i\phi/2} & 0 & 0 \\ + 0 & 0 & e^{i\phi/2} & 0 \\ + 0 & 0 & 0 & e^{-i\phi/2} + \end{bmatrix}. Args: target1 (QubitInput): Target qubit 1 index. @@ -2295,7 +2756,21 @@ def zz( class CCNot(Gate): - """CCNOT gate or Toffoli gate.""" + r"""CCNOT gate or Toffoli gate. + + Unitary matrix: + + .. math:: \mathtt{CCNOT} = \begin{bmatrix} + 1 & 0 & 0 & 0 & 0 & 0 & 0 & 0 \\ + 0 & 1 & 0 & 0 & 0 & 0 & 0 & 0 \\ + 0 & 0 & 1 & 0 & 0 & 0 & 0 & 0 \\ + 0 & 0 & 0 & 1 & 0 & 0 & 0 & 0 \\ + 0 & 0 & 0 & 0 & 1 & 0 & 0 & 0 \\ + 0 & 0 & 0 & 0 & 0 & 1 & 0 & 0 \\ + 0 & 0 & 0 & 0 & 0 & 0 & 0 & 1 \\ + 0 & 0 & 0 & 0 & 0 & 0 & 1 & 0 \\ + \end{bmatrix}. + """ def __init__(self): super().__init__(qubit_count=None, ascii_symbols=["C", "C", "X"]) @@ -2304,7 +2779,7 @@ def __init__(self): def _qasm_name(self) -> str: return "ccnot" - def adjoint(self) -> List[Gate]: + def adjoint(self) -> list[Gate]: return [CCNot()] def _to_jaqcd(self, target: QubitSet) -> Any: @@ -2340,7 +2815,18 @@ def ccnot( control_state: Optional[BasisStateInput] = None, power: float = 1, ) -> Instruction: - """Registers this function into the circuit class. + r"""CCNOT gate or Toffoli gate. + + .. math:: \mathtt{CCNOT} = \begin{bmatrix} + 1 & 0 & 0 & 0 & 0 & 0 & 0 & 0 \\ + 0 & 1 & 0 & 0 & 0 & 0 & 0 & 0 \\ + 0 & 0 & 1 & 0 & 0 & 0 & 0 & 0 \\ + 0 & 0 & 0 & 1 & 0 & 0 & 0 & 0 \\ + 0 & 0 & 0 & 0 & 1 & 0 & 0 & 0 \\ + 0 & 0 & 0 & 0 & 0 & 1 & 0 & 0 \\ + 0 & 0 & 0 & 0 & 0 & 0 & 0 & 1 \\ + 0 & 0 & 0 & 0 & 0 & 0 & 1 & 0 \\ + \end{bmatrix}. Args: control1 (QubitInput): Control qubit 1 index. @@ -2378,7 +2864,21 @@ def ccnot( class CSwap(Gate): - """Controlled Swap gate.""" + r"""Controlled Swap gate. + + Unitary matrix: + + .. math:: \mathtt{CSWAP} = \begin{bmatrix} + 1 & 0 & 0 & 0 & 0 & 0 & 0 & 0 \\ + 0 & 1 & 0 & 0 & 0 & 0 & 0 & 0 \\ + 0 & 0 & 1 & 0 & 0 & 0 & 0 & 0 \\ + 0 & 0 & 0 & 1 & 0 & 0 & 0 & 0 \\ + 0 & 0 & 0 & 0 & 1 & 0 & 0 & 0 \\ + 0 & 0 & 0 & 0 & 0 & 0 & 1 & 0 \\ + 0 & 0 & 0 & 0 & 0 & 1 & 0 & 0 \\ + 0 & 0 & 0 & 0 & 0 & 0 & 0 & 1 \\ + \end{bmatrix}. + """ def __init__(self): super().__init__(qubit_count=None, ascii_symbols=["C", "SWAP", "SWAP"]) @@ -2387,7 +2887,7 @@ def __init__(self): def _qasm_name(self) -> str: return "cswap" - def adjoint(self) -> List[Gate]: + def adjoint(self) -> list[Gate]: return [CSwap()] def _to_jaqcd(self, target: QubitSet) -> Any: @@ -2420,7 +2920,18 @@ def cswap( target2: QubitInput, power: float = 1, ) -> Instruction: - """Registers this function into the circuit class. + r"""Controlled Swap gate. + + .. math:: \mathtt{CSWAP} = \begin{bmatrix} + 1 & 0 & 0 & 0 & 0 & 0 & 0 & 0 \\ + 0 & 1 & 0 & 0 & 0 & 0 & 0 & 0 \\ + 0 & 0 & 1 & 0 & 0 & 0 & 0 & 0 \\ + 0 & 0 & 0 & 1 & 0 & 0 & 0 & 0 \\ + 0 & 0 & 0 & 0 & 1 & 0 & 0 & 0 \\ + 0 & 0 & 0 & 0 & 0 & 0 & 1 & 0 \\ + 0 & 0 & 0 & 0 & 0 & 1 & 0 & 0 \\ + 0 & 0 & 0 & 0 & 0 & 0 & 0 & 1 \\ + \end{bmatrix}. Args: control (QubitSetInput): Control qubit(s). The last control qubit @@ -2451,7 +2962,14 @@ def cswap( class GPi(AngledGate): - """IonQ GPi gate. + r"""IonQ GPi gate. + + Unitary matrix: + + .. math:: \mathtt{GPi}(\phi) = \begin{bmatrix} + 0 & e^{-i \phi} \\ + e^{i \phi} & 0 + \end{bmatrix}. Args: angle (Union[FreeParameterExpression, float]): angle in radians. @@ -2476,7 +2994,7 @@ def to_matrix(self) -> np.ndarray: ] ) - def adjoint(self) -> List[Gate]: + def adjoint(self) -> list[Gate]: return [GPi(self.angle)] @staticmethod @@ -2496,7 +3014,12 @@ def gpi( control_state: Optional[BasisStateInput] = None, power: float = 1, ) -> Iterable[Instruction]: - """Registers this function into the circuit class. + r"""IonQ GPi gate. + + .. math:: \mathtt{GPi}(\phi) = \begin{bmatrix} + 0 & e^{-i \phi} \\ + e^{i \phi} & 0 + \end{bmatrix}. Args: target (QubitSetInput): Target qubit(s). @@ -2530,7 +3053,14 @@ def gpi( class GPi2(AngledGate): - """IonQ GPi2 gate. + r"""IonQ GPi2 gate. + + Unitary matrix: + + .. math:: \mathtt{GPi2}(\phi) = \begin{bmatrix} + 1 & -i e^{-i \phi} \\ + -i e^{i \phi} & 1 + \end{bmatrix}. Args: angle (Union[FreeParameterExpression, float]): angle in radians. @@ -2555,7 +3085,7 @@ def to_matrix(self) -> np.ndarray: ] ) / np.sqrt(2) - def adjoint(self) -> List[Gate]: + def adjoint(self) -> list[Gate]: return [GPi2(self.angle + np.pi)] @staticmethod @@ -2575,7 +3105,12 @@ def gpi2( control_state: Optional[BasisStateInput] = None, power: float = 1, ) -> Iterable[Instruction]: - """Registers this function into the circuit class. + r"""IonQ GPi2 gate. + + .. math:: \mathtt{GPi2}(\phi) = \begin{bmatrix} + 1 & -i e^{-i \phi} \\ + -i e^{i \phi} & 1 + \end{bmatrix}. Args: target (QubitSetInput): Target qubit(s). @@ -2609,12 +3144,26 @@ def gpi2( class MS(TripleAngledGate): - """IonQ Mølmer-Sørenson gate. + r"""IonQ Mølmer-Sørensen gate. + + Unitary matrix: + + .. math:: &\mathtt{MS}(\phi_0, \phi_1, \theta) =\\ &\begin{bmatrix} + \cos{\frac{\theta}{2}} & 0 & + 0 & -ie^{-i (\phi_0 + \phi_1)}\sin{\frac{\theta}{2}} \\ + 0 & \cos{\frac{\theta}{2}} & + -ie^{-i (\phi_0 - \phi_1)}\sin{\frac{\theta}{2}} & 0 \\ + 0 & -ie^{i (\phi_0 - \phi_1)}\sin{\frac{\theta}{2}} & + \cos{\frac{\theta}{2}} & 0 \\ + -ie^{i (\phi_0 + \phi_1)}\sin{\frac{\theta}{2}} & 0 + & 0 & \cos{\frac{\theta}{2}} + \end{bmatrix}. Args: angle_1 (Union[FreeParameterExpression, float]): angle in radians. angle_2 (Union[FreeParameterExpression, float]): angle in radians. angle_3 (Union[FreeParameterExpression, float]): angle in radians. + Default value is angle_3=pi/2. """ def __init__( @@ -2665,7 +3214,7 @@ def to_matrix(self) -> np.ndarray: ] ) - def adjoint(self) -> List[Gate]: + def adjoint(self) -> list[Gate]: return [MS(self.angle_1 + np.pi, self.angle_2, self.angle_3)] @staticmethod @@ -2688,7 +3237,18 @@ def ms( control_state: Optional[BasisStateInput] = None, power: float = 1, ) -> Iterable[Instruction]: - """Registers this function into the circuit class. + r"""IonQ Mølmer-Sørensen gate. + + .. math:: &\mathtt{MS}(\phi_0, \phi_1, \theta) =\\ &\begin{bmatrix} + \cos{\frac{\theta}{2}} & 0 & + 0 & -ie^{-i (\phi_0 + \phi_1)}\sin{\frac{\theta}{2}} \\ + 0 & \cos{\frac{\theta}{2}} & + -ie^{-i (\phi_0 - \phi_1)}\sin{\frac{\theta}{2}} & 0 \\ + 0 & -ie^{i (\phi_0 - \phi_1)}\sin{\frac{\theta}{2}} & + \cos{\frac{\theta}{2}} & 0 \\ + -ie^{i (\phi_0 + \phi_1)}\sin{\frac{\theta}{2}} & 0 + & 0 & \cos{\frac{\theta}{2}} + \end{bmatrix}. Args: target1 (QubitInput): Target qubit 1 index. @@ -2728,7 +3288,7 @@ def ms( class Unitary(Gate): - """Arbitrary unitary gate + """Arbitrary unitary gate. Args: matrix (numpy.ndarray): Unitary matrix which defines the gate. @@ -2754,7 +3314,7 @@ def __init__(self, matrix: np.ndarray, display_name: str = "U"): def to_matrix(self) -> np.ndarray: return np.array(self._matrix) - def adjoint(self) -> List[Gate]: + def adjoint(self) -> list[Gate]: return [Unitary(self._matrix.conj().T, display_name=f"({self.ascii_symbols})^†")] def _to_jaqcd(self, target: QubitSet) -> Any: @@ -2785,13 +3345,13 @@ def __hash__(self): return hash((self.name, str(self._matrix), self.qubit_count)) @staticmethod - def _transform_matrix_to_ir(matrix: np.ndarray) -> List: + def _transform_matrix_to_ir(matrix: np.ndarray) -> list: return [[[element.real, element.imag] for element in row] for row in matrix.tolist()] @staticmethod @circuit.subroutine(register=True) def unitary(targets: QubitSet, matrix: np.ndarray, display_name: str = "U") -> Instruction: - """Registers this function into the circuit class. + r"""Arbitrary unitary gate. Args: targets (QubitSet): Target qubits. @@ -2847,8 +3407,8 @@ def pulse_sequence(self) -> PulseSequence: return self._pulse_sequence @property - def parameters(self) -> List[FreeParameter]: - """Returns the list of `FreeParameter` s associated with the gate.""" + def parameters(self) -> list[FreeParameter]: + r"""Returns the list of `FreeParameter` s associated with the gate.""" return list(self._pulse_sequence.parameters) def bind_values(self, **kwargs) -> PulseGate: diff --git a/src/braket/circuits/instruction.py b/src/braket/circuits/instruction.py index 148711c79..0b2f90d01 100644 --- a/src/braket/circuits/instruction.py +++ b/src/braket/circuits/instruction.py @@ -13,16 +13,16 @@ from __future__ import annotations -from typing import Any, Dict, List, Optional, Tuple +from typing import Any, Optional from braket.circuits.basis_state import BasisState, BasisStateInput from braket.circuits.compiler_directive import CompilerDirective from braket.circuits.gate import Gate from braket.circuits.operator import Operator from braket.circuits.quantum_operator import QuantumOperator -from braket.circuits.qubit import QubitInput -from braket.circuits.qubit_set import QubitSet, QubitSetInput from braket.circuits.serialization import IRType, SerializationProperties +from braket.registers.qubit import QubitInput +from braket.registers.qubit_set import QubitSet, QubitSetInput # InstructionOperator is a type alias, and it can be expanded to include other operators InstructionOperator = Operator @@ -37,7 +37,7 @@ class Instruction: def __init__( self, operator: InstructionOperator, - target: QubitSetInput = None, + target: Optional[QubitSetInput] = None, *, control: Optional[QubitSetInput] = None, control_state: Optional[BasisStateInput] = None, @@ -48,7 +48,8 @@ def __init__( Args: operator (InstructionOperator): Operator for the instruction. - target (QubitSetInput): Target qubits that the operator is applied to. Default is None. + target (Optional[QubitSetInput]): Target qubits that the operator is applied to. + Default is None. control (Optional[QubitSetInput]): Target qubits that the operator is controlled on. Default is None. control_state (Optional[BasisStateInput]): Quantum state on which to control the @@ -135,13 +136,13 @@ def power(self) -> float: """ return self._power - def adjoint(self) -> List[Instruction]: + def adjoint(self) -> list[Instruction]: """Returns a list of Instructions implementing adjoint of this instruction's own operator This operation only works on Gate operators and compiler directives. Returns: - List[Instruction]: A list of new instructions that comprise the adjoint of this operator + list[Instruction]: A list of new instructions that comprise the adjoint of this operator Raises: NotImplementedError: If `operator` is not of type `Gate` or `CompilerDirective` @@ -165,7 +166,7 @@ def adjoint(self) -> List[Instruction]: def to_ir( self, ir_type: IRType = IRType.JAQCD, - serialization_properties: SerializationProperties = None, + serialization_properties: SerializationProperties | None = None, ) -> Any: """ Converts the operator into the canonical intermediate representation. @@ -174,9 +175,9 @@ def to_ir( Args: ir_type(IRType) : The IRType to use for converting the instruction object to its IR representation. - serialization_properties (SerializationProperties): The serialization properties to use - while serializing the object to the IR representation. The serialization properties - supplied must correspond to the supplied `ir_type`. Defaults to None. + serialization_properties (SerializationProperties | None): The serialization properties + to use while serializing the object to the IR representation. The serialization + properties supplied must correspond to the supplied `ir_type`. Defaults to None. Returns: Any: IR object of the instruction. @@ -195,16 +196,16 @@ def to_ir( ) @property - def ascii_symbols(self) -> Tuple[str, ...]: - """Tuple[str, ...]: Returns the ascii symbols for the instruction's operator.""" + def ascii_symbols(self) -> tuple[str, ...]: + """tuple[str, ...]: Returns the ascii symbols for the instruction's operator.""" return self._operator.ascii_symbols def copy( self, - target_mapping: Dict[QubitInput, QubitInput] = None, - target: QubitSetInput = None, - control_mapping: Dict[QubitInput, QubitInput] = None, - control: QubitSetInput = None, + target_mapping: Optional[dict[QubitInput, QubitInput]] = None, + target: Optional[QubitSetInput] = None, + control_mapping: Optional[dict[QubitInput, QubitInput]] = None, + control: Optional[QubitSetInput] = None, control_state: Optional[BasisStateInput] = None, power: float = 1, ) -> Instruction: @@ -217,14 +218,16 @@ def copy( Same relationship holds for `control_mapping`. Args: - target_mapping (Dict[QubitInput, QubitInput]): A dictionary of + target_mapping (Optional[dict[QubitInput, QubitInput]]): A dictionary of qubit mappings to apply to the target. Key is the qubit in this `target` and the value is what the key is changed to. Default = `None`. - target (QubitSetInput): Target qubits for the new instruction. Default is None. - control_mapping (Dict[QubitInput, QubitInput]): A dictionary of + target (Optional[QubitSetInput]): Target qubits for the new instruction. + Default is None. + control_mapping (Optional[dict[QubitInput, QubitInput]]): A dictionary of qubit mappings to apply to the control. Key is the qubit in this `control` and the value is what the key is changed to. Default = `None`. - control (QubitSetInput): Control qubits for the new instruction. Default is None. + control (Optional[QubitSetInput]): Control qubits for the new instruction. + Default is None. control_state (Optional[BasisStateInput]): Quantum state on which to control the operation. Must be a binary sequence of same length as number of qubits in `control`. Will be ignored if `control` is not present. May be represented as a diff --git a/src/braket/circuits/moments.py b/src/braket/circuits/moments.py index f1387d023..7cd8e0a9d 100644 --- a/src/braket/circuits/moments.py +++ b/src/braket/circuits/moments.py @@ -13,26 +13,16 @@ from __future__ import annotations +from collections import OrderedDict +from collections.abc import ItemsView, Iterable, KeysView, Mapping, ValuesView from enum import Enum -from typing import ( - Any, - Dict, - ItemsView, - Iterable, - KeysView, - List, - Mapping, - NamedTuple, - OrderedDict, - Union, - ValuesView, -) +from typing import Any, NamedTuple, Union from braket.circuits.compiler_directive import CompilerDirective from braket.circuits.instruction import Instruction from braket.circuits.noise import Noise -from braket.circuits.qubit import Qubit -from braket.circuits.qubit_set import QubitSet +from braket.registers.qubit import Qubit +from braket.registers.qubit_set import QubitSet class MomentType(str, Enum): @@ -110,9 +100,9 @@ class Moments(Mapping[MomentsKey, Instruction]): Value: Instruction('operator': H, 'target': QubitSet([Qubit(1)])) """ - def __init__(self, instructions: Iterable[Instruction] = None): + def __init__(self, instructions: Iterable[Instruction] | None = None): self._moments: OrderedDict[MomentsKey, Instruction] = OrderedDict() - self._max_times: Dict[Qubit, int] = {} + self._max_times: dict[Qubit, int] = {} self._qubits = QubitSet() self._depth = 0 self._time_all_qubits = -1 @@ -141,12 +131,12 @@ def qubits(self) -> QubitSet: """ return self._qubits - def time_slices(self) -> Dict[int, List[Instruction]]: + def time_slices(self) -> dict[int, list[Instruction]]: """ Get instructions keyed by time. Returns: - Dict[int, List[Instruction]]: Key is the time and value is a list of instructions that + dict[int, list[Instruction]]: Key is the time and value is a list of instructions that occur at that moment in time. The order of instructions is in no particular order. Note: @@ -293,13 +283,13 @@ def values(self) -> ValuesView[Instruction]: self.sort_moments() return self._moments.values() - def get(self, key: MomentsKey, default: Any = None) -> Instruction: + def get(self, key: MomentsKey, default: Any | None = None) -> Instruction: """ Get the instruction in self by key. Args: key (MomentsKey): Key of the instruction to fetch. - default (Any): Value to return if `key` is not in `moments`. Default = `None`. + default (Any | None): Value to return if `key` is not in `moments`. Default = `None`. Returns: Instruction: `moments[key]` if `key` in `moments`, else `default` is returned. diff --git a/src/braket/circuits/noise.py b/src/braket/circuits/noise.py index 5abf19f60..af1bb422f 100644 --- a/src/braket/circuits/noise.py +++ b/src/braket/circuits/noise.py @@ -13,7 +13,8 @@ from __future__ import annotations -from typing import Any, Dict, Iterable, List, Optional, Sequence, Set, Type, Union +from collections.abc import Iterable, Sequence +from typing import Any, Optional, Union import numpy as np @@ -21,12 +22,12 @@ from braket.circuits.free_parameter_expression import FreeParameterExpression from braket.circuits.parameterizable import Parameterizable from braket.circuits.quantum_operator import QuantumOperator -from braket.circuits.qubit_set import QubitSet from braket.circuits.serialization import ( IRType, OpenQASMSerializationProperties, SerializationProperties, ) +from braket.registers.qubit_set import QubitSet class Noise(QuantumOperator): @@ -67,7 +68,7 @@ def to_ir( self, target: QubitSet, ir_type: IRType = IRType.JAQCD, - serialization_properties: SerializationProperties = None, + serialization_properties: SerializationProperties | None = None, ) -> Any: """Returns IR object of quantum operator and target @@ -75,9 +76,9 @@ def to_ir( target (QubitSet): target qubit(s) ir_type(IRType) : The IRType to use for converting the noise object to its IR representation. Defaults to IRType.JAQCD. - serialization_properties (SerializationProperties): The serialization properties to use - while serializing the object to the IR representation. The serialization properties - supplied must correspond to the supplied `ir_type`. Defaults to None. + serialization_properties (SerializationProperties | None): The serialization properties + to use while serializing the object to the IR representation. The serialization + properties supplied must correspond to the supplied `ir_type`. Defaults to None. Returns: Any: IR object of the quantum operator and target @@ -163,10 +164,10 @@ def from_dict(cls, noise: dict) -> Noise: raise NotImplementedError @classmethod - def register_noise(cls, noise: Type[Noise]) -> None: + def register_noise(cls, noise: type[Noise]) -> None: """Register a noise implementation by adding it into the Noise class. Args: - noise (Type[Noise]): Noise class to register. + noise (type[Noise]): Noise class to register. """ setattr(cls, noise.__name__, noise) @@ -220,13 +221,13 @@ def __str__(self): return f"{self.name}({self.probability})" @property - def parameters(self) -> List[Union[FreeParameterExpression, float]]: + def parameters(self) -> list[Union[FreeParameterExpression, float]]: """ Returns the parameters associated with the object, either unbound free parameter expressions or bound values. Returns: - List[Union[FreeParameterExpression, float]]: The free parameter expressions + list[Union[FreeParameterExpression, float]]: The free parameter expressions or fixed values associated with the object. """ return [self._probability] @@ -343,14 +344,14 @@ class MultiQubitPauliNoise(Noise, Parameterizable): def __init__( self, - probabilities: Dict[str, Union[FreeParameterExpression, float]], + probabilities: dict[str, Union[FreeParameterExpression, float]], qubit_count: Optional[int], ascii_symbols: Sequence[str], ): """[summary] Args: - probabilities (Dict[str, Union[FreeParameterExpression, float]]): A dictionary with + probabilities (dict[str, Union[FreeParameterExpression, float]]): A dictionary with Pauli strings as keys and the probabilities as values, i.e. {"XX": 0.1. "IZ": 0.2}. qubit_count (Optional[int]): The number of qubits the Pauli noise acts on. ascii_symbols (Sequence[str]): ASCII string symbols for the noise. These are used when @@ -402,7 +403,7 @@ def __init__( @classmethod def _validate_pauli_string( - cls, pauli_str: str, qubit_count: int, allowed_substrings: Set[str] + cls, pauli_str: str, qubit_count: int, allowed_substrings: set[str] ) -> None: if not isinstance(pauli_str, str): raise TypeError(f"Type of {pauli_str} was not a string.") @@ -436,15 +437,15 @@ def __eq__(self, other): return False @property - def probabilities(self) -> Dict[str, float]: + def probabilities(self) -> dict[str, float]: """A map of a Pauli string to its corresponding probability. Returns: - Dict[str, float]: A map of a Pauli string to its corresponding probability. + dict[str, float]: A map of a Pauli string to its corresponding probability. """ return self._probabilities @property - def parameters(self) -> List[Union[FreeParameterExpression, float]]: + def parameters(self) -> list[Union[FreeParameterExpression, float]]: """ Returns the parameters associated with the object, either unbound free parameter expressions or bound values. @@ -452,7 +453,7 @@ def parameters(self) -> List[Union[FreeParameterExpression, float]]: Parameters are in alphabetical order of the Pauli strings in `probabilities`. Returns: - List[Union[FreeParameterExpression, float]]: The free parameter expressions + list[Union[FreeParameterExpression, float]]: The free parameter expressions or fixed values associated with the object. """ return [ @@ -596,7 +597,7 @@ def __eq__(self, other): return False @property - def parameters(self) -> List[Union[FreeParameterExpression, float]]: + def parameters(self) -> list[Union[FreeParameterExpression, float]]: """ Returns the parameters associated with the object, either unbound free parameter expressions or bound values. @@ -604,7 +605,7 @@ def parameters(self) -> List[Union[FreeParameterExpression, float]]: Parameters are in the order [probX, probY, probZ] Returns: - List[Union[FreeParameterExpression, float]]: The free parameter expressions + list[Union[FreeParameterExpression, float]]: The free parameter expressions or fixed values associated with the object. """ return self._parameters @@ -689,13 +690,13 @@ def __str__(self): return f"{self.name}({self.gamma})" @property - def parameters(self) -> List[Union[FreeParameterExpression, float]]: + def parameters(self) -> list[Union[FreeParameterExpression, float]]: """ Returns the parameters associated with the object, either unbound free parameter expressions or bound values. Returns: - List[Union[FreeParameterExpression, float]]: The free parameter expressions + list[Union[FreeParameterExpression, float]]: The free parameter expressions or fixed values associated with the object. """ return [self._gamma] @@ -791,7 +792,7 @@ def __str__(self): return f"{self.name}({self.gamma}, {self.probability})" @property - def parameters(self) -> List[Union[FreeParameterExpression, float]]: + def parameters(self) -> list[Union[FreeParameterExpression, float]]: """ Returns the parameters associated with the object, either unbound free parameter expressions or bound values. @@ -799,7 +800,7 @@ def parameters(self) -> List[Union[FreeParameterExpression, float]]: Parameters are in the order [gamma, probability] Returns: - List[Union[FreeParameterExpression, float]]: The free parameter expressions + list[Union[FreeParameterExpression, float]]: The free parameter expressions or fixed values associated with the object. """ return [self.gamma, self.probability] diff --git a/src/braket/circuits/noise_helpers.py b/src/braket/circuits/noise_helpers.py index 29ade8cf9..06c0b3620 100644 --- a/src/braket/circuits/noise_helpers.py +++ b/src/braket/circuits/noise_helpers.py @@ -14,7 +14,8 @@ from __future__ import annotations import warnings -from typing import TYPE_CHECKING, Any, Iterable, List, Optional, Tuple, Type, Union +from collections.abc import Iterable +from typing import TYPE_CHECKING, Any, Optional, Union import numpy as np @@ -23,7 +24,7 @@ from braket.circuits.moments import Moments from braket.circuits.noise import Noise from braket.circuits.quantum_operator_helpers import is_unitary -from braket.circuits.qubit_set import QubitSet, QubitSetInput +from braket.registers.qubit_set import QubitSet, QubitSetInput if TYPE_CHECKING: # pragma: no cover from braket.circuits.circuit import Circuit @@ -42,27 +43,27 @@ def no_noise_applied_warning(noise_applied: bool) -> None: ) -def wrap_with_list(an_item: Any) -> List[Any]: +def wrap_with_list(an_item: Any) -> list[Any]: """Helper function to make the input parameter a list. Args: an_item (Any): The item to wrap. Returns: - List[Any]: The item wrapped in a list. + list[Any]: The item wrapped in a list. """ if an_item is not None and not isinstance(an_item, list): an_item = [an_item] return an_item -def check_noise_target_gates(noise: Noise, target_gates: Iterable[Type[Gate]]) -> None: +def check_noise_target_gates(noise: Noise, target_gates: Iterable[type[Gate]]) -> None: """Helper function to check 1. whether all the elements in target_gates are a Gate type; 2. if `noise` is multi-qubit noise and `target_gates` contain gates with the number of qubits is the same as `noise.qubit_count`. Args: noise (Noise): A Noise class object to be applied to the circuit. - target_gates (Iterable[Type[Gate]]): Gate class or + target_gates (Iterable[type[Gate]]): Gate class or List of Gate classes which `noise` is applied to. """ if not all(isinstance(g, type) and issubclass(g, Gate) for g in target_gates): @@ -126,7 +127,7 @@ def check_noise_target_qubits( def apply_noise_to_moments( - circuit: Circuit, noise: Iterable[Type[Noise]], target_qubits: QubitSet, position: str + circuit: Circuit, noise: Iterable[type[Noise]], target_qubits: QubitSet, position: str ) -> Circuit: """ Apply initialization/readout noise to the circuit. @@ -138,7 +139,7 @@ def apply_noise_to_moments( Args: circuit (Circuit): A ciruit where `noise` is applied to. - noise (Iterable[Type[Noise]]): Noise channel(s) to be applied + noise (Iterable[type[Noise]]): Noise channel(s) to be applied to the circuit. target_qubits (QubitSet): Index or indices of qubits. `noise` is applied to. position (str): The position to add the noise to. May be 'initialization' or @@ -181,18 +182,18 @@ def apply_noise_to_moments( def _apply_noise_to_gates_helper( - noise: Iterable[Type[Noise]], + noise: Iterable[type[Noise]], target_qubits: QubitSet, instruction: Instruction, noise_index: int, intersection: QubitSet, noise_applied: bool, new_noise_instruction: Iterable, -) -> Tuple[Iterable[Instruction], int, bool]: +) -> tuple[Iterable[Instruction], int, bool]: """Helper function to work out the noise instructions to be attached to a gate. Args: - noise (Iterable[Type[Noise]]): Noise channel(s) to be applied + noise (Iterable[type[Noise]]): Noise channel(s) to be applied to the circuit. target_qubits (QubitSet): Index or indices of qubits which `noise` is applied to. instruction (Instruction): Instruction of the gate which `noise` is applied to. @@ -204,7 +205,7 @@ def _apply_noise_to_gates_helper( to the circuit. Returns: - Tuple[Iterable[Instruction], int, bool]: A tuple of three values: + tuple[Iterable[Instruction], int, bool]: A tuple of three values: new_noise_instruction: A list of noise intructions noise_index: The number of noise channels applied to the gate noise_applied: Whether noise is applied or not @@ -234,8 +235,8 @@ def _apply_noise_to_gates_helper( def apply_noise_to_gates( circuit: Circuit, - noise: Iterable[Type[Noise]], - target_gates: Union[Iterable[Type[Gate]], np.ndarray], + noise: Iterable[type[Noise]], + target_gates: Union[Iterable[type[Gate]], np.ndarray], target_qubits: QubitSet, ) -> Circuit: """Apply noise after target gates in target qubits. @@ -247,9 +248,9 @@ def apply_noise_to_gates( Args: circuit (Circuit): A ciruit where `noise` is applied to. - noise (Iterable[Type[Noise]]): Noise channel(s) to be applied + noise (Iterable[type[Noise]]): Noise channel(s) to be applied to the circuit. - target_gates (Union[Iterable[Type[Gate]], ndarray]): List of gates, or a unitary matrix + target_gates (Union[Iterable[type[Gate]], ndarray]): List of gates, or a unitary matrix which `noise` is applied to. target_qubits (QubitSet): Index or indices of qubits which `noise` is applied to. diff --git a/src/braket/circuits/noise_model/circuit_instruction_criteria.py b/src/braket/circuits/noise_model/circuit_instruction_criteria.py index 2e5928857..170d3e996 100644 --- a/src/braket/circuits/noise_model/circuit_instruction_criteria.py +++ b/src/braket/circuits/noise_model/circuit_instruction_criteria.py @@ -12,11 +12,11 @@ # language governing permissions and limitations under the License. from abc import abstractmethod -from typing import Optional, Set, Tuple, Union +from typing import Optional, Union from braket.circuits.instruction import Instruction from braket.circuits.noise_model.criteria import Criteria -from braket.circuits.qubit_set import QubitSetInput +from braket.registers.qubit_set import QubitSetInput class CircuitInstructionCriteria(Criteria): @@ -36,13 +36,13 @@ def instruction_matches(self, instruction: Instruction) -> bool: @staticmethod def _check_target_in_qubits( - qubits: Optional[Set[Union[int, Tuple[int]]]], target: QubitSetInput + qubits: Optional[set[Union[int, tuple[int]]]], target: QubitSetInput ) -> bool: """ Returns true if the given targets of an instruction match the given qubit input set. Args: - qubits (Optional[Set[Union[int, Tuple[int]]]]): The qubits provided to the criteria. + qubits (Optional[set[Union[int, tuple[int]]]]): The qubits provided to the criteria. target (QubitSetInput): Targets of an instruction. Returns: diff --git a/src/braket/circuits/noise_model/criteria.py b/src/braket/circuits/noise_model/criteria.py index 539bf3829..b9f0d2cc4 100644 --- a/src/braket/circuits/noise_model/criteria.py +++ b/src/braket/circuits/noise_model/criteria.py @@ -14,8 +14,9 @@ from __future__ import annotations from abc import ABC, abstractmethod +from collections.abc import Iterable from enum import Enum -from typing import Any, Iterable, Set, Type, Union +from typing import Any, Union class CriteriaKey(str, Enum): @@ -54,14 +55,14 @@ def applicable_key_types(self) -> Iterable[CriteriaKey]: raise NotImplementedError @abstractmethod - def get_keys(self, key_type: CriteriaKey) -> Union[CriteriaKeyResult, Set[Any]]: + def get_keys(self, key_type: CriteriaKey) -> Union[CriteriaKeyResult, set[Any]]: """Returns a set of key for a given key type. Args: key_type (CriteriaKey): The criteria key type. Returns: - Union[CriteriaKeyResult, Set[Any]]: Returns a set of keys for a key type. The + Union[CriteriaKeyResult, set[Any]]: Returns a set of keys for a key type. The actual returned keys will depend on the CriteriaKey. If the provided key type is not relevant the returned list will be empty. If the provided key type is relevant for all possible inputs, the string CriteriaKeyResult.ALL will be returned. @@ -105,10 +106,10 @@ def from_dict(cls, criteria: dict) -> Criteria: raise NotImplementedError @classmethod - def register_criteria(cls, criteria: Type[Criteria]) -> None: + def register_criteria(cls, criteria: type[Criteria]) -> None: """Register a criteria implementation by adding it into the Criteria class. Args: - criteria (Type[Criteria]): Criteria class to register. + criteria (type[Criteria]): Criteria class to register. """ setattr(cls, criteria.__name__, criteria) diff --git a/src/braket/circuits/noise_model/criteria_input_parsing.py b/src/braket/circuits/noise_model/criteria_input_parsing.py index 193525f3b..11f24619e 100644 --- a/src/braket/circuits/noise_model/criteria_input_parsing.py +++ b/src/braket/circuits/noise_model/criteria_input_parsing.py @@ -11,15 +11,16 @@ # ANY KIND, either express or implied. See the License for the specific # language governing permissions and limitations under the License. -from typing import Iterable, Optional, Set, Tuple, Union +from collections.abc import Iterable +from typing import Optional, Union from braket.circuits.quantum_operator import QuantumOperator -from braket.circuits.qubit_set import QubitSetInput +from braket.registers.qubit_set import QubitSetInput def parse_operator_input( operators: Union[QuantumOperator, Iterable[QuantumOperator]] -) -> Optional[Set[QuantumOperator]]: +) -> Optional[set[QuantumOperator]]: """ Processes the quantum operator input to __init__ to validate and return a set of QuantumOperators. @@ -28,7 +29,7 @@ def parse_operator_input( operators (Union[QuantumOperator, Iterable[QuantumOperator]]): QuantumOperator input. Returns: - Optional[Set[QuantumOperator]]: The set of relevant QuantumOperators or None if none + Optional[set[QuantumOperator]]: The set of relevant QuantumOperators or None if none is specified. Throws: @@ -47,7 +48,7 @@ def parse_operator_input( def parse_qubit_input( qubits: Optional[QubitSetInput], expected_qubit_count: Optional[int] = 0 -) -> Optional[Set[Union[int, Tuple[int]]]]: +) -> Optional[set[Union[int, tuple[int]]]]: """ Processes the qubit input to __init__ to validate and return a set of qubit targets. @@ -58,7 +59,7 @@ def parse_qubit_input( expected qubit count matches the actual qubit count. Default is 0. Returns: - Optional[Set[Union[int, Tuple[int]]]]: The set of qubit targets, or None if no qubits + Optional[set[Union[int, tuple[int]]]]: The set of qubit targets, or None if no qubits are specified. """ if qubits is None: diff --git a/src/braket/circuits/noise_model/gate_criteria.py b/src/braket/circuits/noise_model/gate_criteria.py index 3505391d8..623c02477 100644 --- a/src/braket/circuits/noise_model/gate_criteria.py +++ b/src/braket/circuits/noise_model/gate_criteria.py @@ -11,7 +11,8 @@ # ANY KIND, either express or implied. See the License for the specific # language governing permissions and limitations under the License. -from typing import Any, Iterable, Optional, Set, Union +from collections.abc import Iterable +from typing import Any, Optional, Union from braket.circuits.gate import Gate from braket.circuits.instruction import Instruction @@ -21,7 +22,7 @@ parse_operator_input, parse_qubit_input, ) -from braket.circuits.qubit_set import QubitSetInput +from braket.registers.qubit_set import QubitSetInput class GateCriteria(CircuitInstructionCriteria): @@ -65,14 +66,14 @@ def applicable_key_types(self) -> Iterable[CriteriaKey]: """ return [CriteriaKey.QUBIT, CriteriaKey.GATE] - def get_keys(self, key_type: CriteriaKey) -> Union[CriteriaKeyResult, Set[Any]]: + def get_keys(self, key_type: CriteriaKey) -> Union[CriteriaKeyResult, set[Any]]: """Gets the keys for a given CriteriaKey. Args: key_type (CriteriaKey): The relevant Criteria Key. Returns: - Union[CriteriaKeyResult, Set[Any]]: The return value is based on the key type: + Union[CriteriaKeyResult, set[Any]]: The return value is based on the key type: GATE will return a set of Gate classes that are relevant to this Criteria. QUBIT will return a set of qubit targets that are relevant to this Criteria, or CriteriaKeyResult.ALL if the Criteria is relevant for all (possible) qubits. diff --git a/src/braket/circuits/noise_model/initialization_criteria.py b/src/braket/circuits/noise_model/initialization_criteria.py index 6229573bc..e40d4e9de 100644 --- a/src/braket/circuits/noise_model/initialization_criteria.py +++ b/src/braket/circuits/noise_model/initialization_criteria.py @@ -14,7 +14,7 @@ from abc import abstractmethod from braket.circuits.noise_model.criteria import Criteria -from braket.circuits.qubit_set import QubitSetInput +from braket.registers.qubit_set import QubitSetInput class InitializationCriteria(Criteria): diff --git a/src/braket/circuits/noise_model/noise_model.py b/src/braket/circuits/noise_model/noise_model.py index 76cdffa2a..8922cda72 100644 --- a/src/braket/circuits/noise_model/noise_model.py +++ b/src/braket/circuits/noise_model/noise_model.py @@ -15,7 +15,7 @@ from collections import defaultdict from dataclasses import dataclass -from typing import List, Optional, Type +from typing import Optional from braket.circuits.circuit import Circuit from braket.circuits.gate import Gate @@ -25,8 +25,8 @@ from braket.circuits.noise_model.criteria import Criteria, CriteriaKey, CriteriaKeyResult from braket.circuits.noise_model.initialization_criteria import InitializationCriteria from braket.circuits.noise_model.result_type_criteria import ResultTypeCriteria -from braket.circuits.qubit_set import QubitSetInput from braket.circuits.result_types import ObservableResultType +from braket.registers.qubit_set import QubitSetInput @dataclass @@ -76,9 +76,9 @@ def from_dict(cls, noise_model_item: dict) -> NoiseModelInstruction: class NoiseModelInstructions: """Represents the instructions in a noise model, separated by type.""" - initialization_noise: List[NoiseModelInstruction] - gate_noise: List[NoiseModelInstruction] - readout_noise: List[NoiseModelInstruction] + initialization_noise: list[NoiseModelInstruction] + gate_noise: list[NoiseModelInstruction] + readout_noise: list[NoiseModelInstruction] class NoiseModel: @@ -90,7 +90,7 @@ class NoiseModel: a phase flip. """ - def __init__(self, instructions: List[NoiseModelInstruction] = None): + def __init__(self, instructions: list[NoiseModelInstruction] = None): self._instructions = instructions or [] def __repr__(self): @@ -109,12 +109,12 @@ def __str__(self): return "\n".join(all_strings) @property - def instructions(self) -> List[NoiseModelInstruction]: + def instructions(self) -> list[NoiseModelInstruction]: """ List all the noise in the NoiseModel. Returns: - List[NoiseModelInstruction]: The noise model instructions. + list[NoiseModelInstruction]: The noise model instructions. """ return self._instructions @@ -198,7 +198,7 @@ def from_filter( self, qubit: Optional[QubitSetInput] = None, gate: Optional[Gate] = None, - noise: Optional[Type[Noise]] = None, + noise: Optional[type[Noise]] = None, ) -> NoiseModel: """ Returns a new NoiseModel from this NoiseModel using a given filter. If no filters are @@ -211,7 +211,7 @@ def from_filter( gate (Optional[Gate]): The gate to filter. Default is None. If not None, the returned NoiseModel will only have Noise that might be applicable to the passed Gate. - noise (Optional[Type[Noise]]): The noise class to filter. Default is None. + noise (Optional[type[Noise]]): The noise class to filter. Default is None. If not None, the returned NoiseModel will only have noise that is of the same class as the given noise class. @@ -259,7 +259,7 @@ def to_dict(self) -> dict: def _apply_gate_noise( cls, circuit: Circuit, - gate_noise_instructions: List[NoiseModelInstruction], + gate_noise_instructions: list[NoiseModelInstruction], ) -> Circuit: """ Applies the gate noise to return a new circuit that's the `noisy` version of the given @@ -267,7 +267,7 @@ def _apply_gate_noise( Args: circuit (Circuit): a circuit to apply `noise` to. - gate_noise_instructions (List[NoiseModelInstruction]): a list of gate noise + gate_noise_instructions (list[NoiseModelInstruction]): a list of gate noise instructions to apply to the circuit. Returns: @@ -293,14 +293,14 @@ def _apply_gate_noise( def _apply_init_noise( cls, circuit: Circuit, - init_noise_instructions: List[NoiseModelInstruction], + init_noise_instructions: list[NoiseModelInstruction], ) -> Circuit: """ Applies the initialization noise of this noise model to a circuit and returns the circuit. Args: circuit (Circuit): A circuit to apply `noise` to. - init_noise_instructions (List[NoiseModelInstruction]): A list of initialization noise + init_noise_instructions (list[NoiseModelInstruction]): A list of initialization noise model instructions. Returns: @@ -318,14 +318,14 @@ def _apply_init_noise( def _apply_readout_noise( cls, circuit: Circuit, - readout_noise_instructions: List[NoiseModelInstruction], + readout_noise_instructions: list[NoiseModelInstruction], ) -> Circuit: """ Applies the readout noise of this noise model to a circuit and returns the circuit. Args: circuit (Circuit): A circuit to apply `noise` to. - readout_noise_instructions (List[NoiseModelInstruction]): The list of readout noise + readout_noise_instructions (list[NoiseModelInstruction]): The list of readout noise to apply. Returns: @@ -337,17 +337,17 @@ def _apply_readout_noise( @classmethod def _items_to_string( - cls, instructions_title: str, instructions: List[NoiseModelInstruction] - ) -> List[str]: + cls, instructions_title: str, instructions: list[NoiseModelInstruction] + ) -> list[str]: """ Creates a string representation of a list of instructions. Args: instructions_title (str): The title for this list of instructions. - instructions (List[NoiseModelInstruction]): A list of instructions. + instructions (list[NoiseModelInstruction]): A list of instructions. Returns: - List[str]: A list of string representations of the passed instructions. + list[str]: A list of string representations of the passed instructions. """ results = [] if len(instructions) > 0: @@ -376,14 +376,14 @@ def from_dict(cls, noise_dict: dict) -> NoiseModel: def _apply_noise_on_observable_result_types( - circuit: Circuit, readout_noise_instructions: List[NoiseModelInstruction] + circuit: Circuit, readout_noise_instructions: list[NoiseModelInstruction] ) -> Circuit: """Applies readout noise based on the observable result types in the circuit. Each applicable Noise will be applied only once to a target in the ObservableResultType. Args: circuit (Circuit): The circuit to apply the readout noise to. - readout_noise_instructions (List[NoiseModelInstruction]): The list of readout noise + readout_noise_instructions (list[NoiseModelInstruction]): The list of readout noise to apply. Returns: diff --git a/src/braket/circuits/noise_model/observable_criteria.py b/src/braket/circuits/noise_model/observable_criteria.py index e6affc94c..2275ccce7 100644 --- a/src/braket/circuits/noise_model/observable_criteria.py +++ b/src/braket/circuits/noise_model/observable_criteria.py @@ -11,7 +11,8 @@ # ANY KIND, either express or implied. See the License for the specific # language governing permissions and limitations under the License. -from typing import Any, Iterable, Optional, Set, Union +from collections.abc import Iterable +from typing import Any, Optional, Union from braket.circuits.noise_model.criteria import Criteria, CriteriaKey, CriteriaKeyResult from braket.circuits.noise_model.criteria_input_parsing import ( @@ -20,8 +21,8 @@ ) from braket.circuits.noise_model.result_type_criteria import ResultTypeCriteria from braket.circuits.observable import Observable -from braket.circuits.qubit_set import QubitSetInput from braket.circuits.result_type import ObservableResultType, ResultType +from braket.registers.qubit_set import QubitSetInput class ObservableCriteria(ResultTypeCriteria): @@ -71,14 +72,14 @@ def applicable_key_types(self) -> Iterable[CriteriaKey]: """ return [CriteriaKey.OBSERVABLE, CriteriaKey.QUBIT] - def get_keys(self, key_type: CriteriaKey) -> Union[CriteriaKeyResult, Set[Any]]: + def get_keys(self, key_type: CriteriaKey) -> Union[CriteriaKeyResult, set[Any]]: """Gets the keys for a given CriteriaKey. Args: key_type (CriteriaKey): The relevant Criteria Key. Returns: - Union[CriteriaKeyResult, Set[Any]]: The return value is based on the key type: + Union[CriteriaKeyResult, set[Any]]: The return value is based on the key type: OBSERVABLE will return a set of Observable classes that are relevant to this Criteria, or CriteriaKeyResult.ALL if the Criteria is relevant for all (possible) observables. QUBIT will return a set of qubit targets that are relevant to this Criteria, or diff --git a/src/braket/circuits/noise_model/qubit_initialization_criteria.py b/src/braket/circuits/noise_model/qubit_initialization_criteria.py index 0646845ce..abed13af8 100644 --- a/src/braket/circuits/noise_model/qubit_initialization_criteria.py +++ b/src/braket/circuits/noise_model/qubit_initialization_criteria.py @@ -11,12 +11,13 @@ # ANY KIND, either express or implied. See the License for the specific # language governing permissions and limitations under the License. -from typing import Any, Iterable, Optional, Set, Union +from collections.abc import Iterable +from typing import Any, Optional, Union from braket.circuits.noise_model.criteria import Criteria, CriteriaKey, CriteriaKeyResult from braket.circuits.noise_model.criteria_input_parsing import parse_qubit_input from braket.circuits.noise_model.initialization_criteria import InitializationCriteria -from braket.circuits.qubit_set import QubitSet, QubitSetInput +from braket.registers.qubit_set import QubitSet, QubitSetInput class QubitInitializationCriteria(InitializationCriteria): @@ -45,14 +46,14 @@ def applicable_key_types(self) -> Iterable[CriteriaKey]: """ return [CriteriaKey.QUBIT] - def get_keys(self, key_type: CriteriaKey) -> Union[CriteriaKeyResult, Set[Any]]: + def get_keys(self, key_type: CriteriaKey) -> Union[CriteriaKeyResult, set[Any]]: """Gets the keys for a given CriteriaKey. Args: key_type (CriteriaKey): The relevant Criteria Key. Returns: - Union[CriteriaKeyResult, Set[Any]]: The return value is based on the key type: + Union[CriteriaKeyResult, set[Any]]: The return value is based on the key type: QUBIT will return a set of qubit targets that are relevant to this Critera, or CriteriaKeyResult.ALL if the Criteria is relevant for all (possible) qubits. All other keys will return an empty set. diff --git a/src/braket/circuits/noise_model/unitary_gate_criteria.py b/src/braket/circuits/noise_model/unitary_gate_criteria.py index c87292980..229fc11ba 100644 --- a/src/braket/circuits/noise_model/unitary_gate_criteria.py +++ b/src/braket/circuits/noise_model/unitary_gate_criteria.py @@ -11,14 +11,15 @@ # ANY KIND, either express or implied. See the License for the specific # language governing permissions and limitations under the License. -from typing import Any, Iterable, Optional, Set, Union +from collections.abc import Iterable +from typing import Any, Optional, Union from braket.circuits.gates import Unitary from braket.circuits.instruction import Instruction from braket.circuits.noise_model.circuit_instruction_criteria import CircuitInstructionCriteria from braket.circuits.noise_model.criteria import Criteria, CriteriaKey, CriteriaKeyResult from braket.circuits.noise_model.criteria_input_parsing import parse_qubit_input -from braket.circuits.qubit_set import QubitSetInput +from braket.registers.qubit_set import QubitSetInput class UnitaryGateCriteria(CircuitInstructionCriteria): @@ -53,14 +54,14 @@ def applicable_key_types(self) -> Iterable[CriteriaKey]: """ return [CriteriaKey.QUBIT, CriteriaKey.UNITARY_GATE] - def get_keys(self, key_type: CriteriaKey) -> Union[CriteriaKeyResult, Set[Any]]: + def get_keys(self, key_type: CriteriaKey) -> Union[CriteriaKeyResult, set[Any]]: """Gets the keys for a given CriteriaKey. Args: key_type (CriteriaKey): The relevant Criteria Key. Returns: - Union[CriteriaKeyResult, Set[Any]]: The return value is based on the key type: + Union[CriteriaKeyResult, set[Any]]: The return value is based on the key type: UNITARY_GATE will return a set containing the bytes of the unitary matrix representing the unitary gate. QUBIT will return a set of qubit targets that are relevant to this Criteria, or diff --git a/src/braket/circuits/noises.py b/src/braket/circuits/noises.py index 75cec6c42..572489a9d 100644 --- a/src/braket/circuits/noises.py +++ b/src/braket/circuits/noises.py @@ -12,7 +12,8 @@ # language governing permissions and limitations under the License. import itertools -from typing import Any, Dict, Iterable, List, Union +from collections.abc import Iterable +from typing import Any, Union import numpy as np @@ -36,9 +37,9 @@ is_cptp, verify_quantum_operator_matrix_dimensions, ) -from braket.circuits.qubit import QubitInput -from braket.circuits.qubit_set import QubitSet, QubitSetInput from braket.circuits.serialization import OpenQASMSerializationProperties +from braket.registers.qubit import QubitInput +from braket.registers.qubit_set import QubitSet, QubitSetInput """ To add a new Noise implementation: @@ -357,7 +358,7 @@ def pauli_channel( Args: target (QubitSetInput): Target qubit(s) - probability List[float]: Probabilities for the Pauli X, Y and Z noise + probability list[float]: Probabilities for the Pauli X, Y and Z noise happening in the Kraus channel. probX (float): X rotation probability. probY (float): Y rotation probability. @@ -863,7 +864,7 @@ class TwoQubitPauliChannel(MultiQubitPauliNoise): _tensor_products_strings = itertools.product(_paulis.keys(), repeat=2) _names_list = ["".join(x) for x in _tensor_products_strings] - def __init__(self, probabilities: Dict[str, float]): + def __init__(self, probabilities: dict[str, float]): super().__init__( probabilities=probabilities, qubit_count=None, @@ -906,14 +907,14 @@ def fixed_qubit_count() -> int: @staticmethod @circuit.subroutine(register=True) def two_qubit_pauli_channel( - target1: QubitInput, target2: QubitInput, probabilities: Dict[str, float] + target1: QubitInput, target2: QubitInput, probabilities: dict[str, float] ) -> Iterable[Instruction]: """Registers this function into the circuit class. Args: target1 (QubitInput): Target qubit 1. target2 (QubitInput): Target qubit 2. - probabilities (Dict[str, float]): Probability of two-qubit Pauli channel. + probabilities (dict[str, float]): Probability of two-qubit Pauli channel. Returns: Iterable[Instruction]: `Iterable` of Depolarizing instructions. @@ -1374,7 +1375,7 @@ def _to_openqasm( return f"#pragma braket noise kraus({matrix_list}) {qubit_list}" @staticmethod - def _transform_matrix_to_ir(matrices: Iterable[np.ndarray]) -> List: + def _transform_matrix_to_ir(matrices: Iterable[np.ndarray]) -> list: serializable = [] for matrix in matrices: matrix_as_list = [ @@ -1439,14 +1440,14 @@ def from_dict(cls, noise: dict) -> Noise: def _ascii_representation( - noise: str, parameters: List[Union[FreeParameterExpression, float]] + noise: str, parameters: list[Union[FreeParameterExpression, float]] ) -> str: """ Generates a formatted ascii representation of a noise. Args: noise (str): The name of the noise. - parameters (List[Union[FreeParameterExpression, float]]): The parameters to the noise. + parameters (list[Union[FreeParameterExpression, float]]): The parameters to the noise. Returns: str: The ascii representation of the noise. diff --git a/src/braket/circuits/observable.py b/src/braket/circuits/observable.py index 3ac5477fd..1cc672fb9 100644 --- a/src/braket/circuits/observable.py +++ b/src/braket/circuits/observable.py @@ -14,20 +14,21 @@ from __future__ import annotations import numbers +from collections.abc import Sequence from copy import deepcopy -from typing import List, Sequence, Tuple, Union +from typing import Union import numpy as np from braket.circuits.gate import Gate from braket.circuits.quantum_operator import QuantumOperator -from braket.circuits.qubit_set import QubitSet from braket.circuits.serialization import ( IRType, OpenQASMSerializationProperties, SerializationProperties, ) from braket.pulse.pulse_sequence import PulseSequence +from braket.registers.qubit_set import QubitSet class Observable(QuantumOperator): @@ -47,22 +48,22 @@ def _unscaled(self) -> Observable: def to_ir( self, - target: QubitSet = None, + target: QubitSet | None = None, ir_type: IRType = IRType.JAQCD, - serialization_properties: SerializationProperties = None, - ) -> Union[str, List[Union[str, List[List[List[float]]]]]]: + serialization_properties: SerializationProperties | None = None, + ) -> Union[str, list[Union[str, list[list[list[float]]]]]]: """Returns the IR representation for the observable Args: - target (QubitSet): target qubit(s). Defaults to None. + target (QubitSet | None): target qubit(s). Defaults to None. ir_type(IRType) : The IRType to use for converting the result type object to its IR representation. Defaults to IRType.JAQCD. - serialization_properties (SerializationProperties): The serialization properties to use - while serializing the object to the IR representation. The serialization properties - supplied must correspond to the supplied `ir_type`. Defaults to None. + serialization_properties (SerializationProperties | None): The serialization properties + to use while serializing the object to the IR representation. The serialization + properties supplied must correspond to the supplied `ir_type`. Defaults to None. Returns: - Union[str, List[Union[str, List[List[List[float]]]]]]: The IR representation for + Union[str, list[Union[str, list[list[list[float]]]]]]: The IR representation for the observable. Raises: @@ -85,12 +86,14 @@ def to_ir( else: raise ValueError(f"Supplied ir_type {ir_type} is not supported.") - def _to_jaqcd(self) -> List[Union[str, List[List[List[float]]]]]: + def _to_jaqcd(self) -> list[Union[str, list[list[list[float]]]]]: """Returns the JAQCD representation of the observable.""" raise NotImplementedError("to_jaqcd has not been implemented yet.") def _to_openqasm( - self, serialization_properties: OpenQASMSerializationProperties, target: QubitSet = None + self, + serialization_properties: OpenQASMSerializationProperties, + target: QubitSet | None = None, ) -> str: """ Returns the openqasm string representation of the result type. @@ -98,7 +101,7 @@ def _to_openqasm( Args: serialization_properties (OpenQASMSerializationProperties): The serialization properties to use while serializing the object to the IR representation. - target (QubitSet): target qubit(s). Defaults to None. + target (QubitSet | None): target qubit(s). Defaults to None. Returns: str: Representing the openqasm representation of the result type. @@ -106,7 +109,9 @@ def _to_openqasm( raise NotImplementedError("to_openqasm has not been implemented yet.") def _to_pulse_sequence( - self, serialization_properties: OpenQASMSerializationProperties, target: QubitSet = None + self, + serialization_properties: OpenQASMSerializationProperties, + target: QubitSet | None = None, ) -> PulseSequence: """ Returns the openqasm string representation of the result type. @@ -114,10 +119,10 @@ def _to_pulse_sequence( Args: serialization_properties (OpenQASMSerializationProperties): The serialization properties to use while serializing the object to the IR representation. - target (QubitSet): target qubit(s). Defaults to None. + target (QubitSet | None): target qubit(s). Defaults to None. Returns: - str: Representing the openqasm representation of the result type. + PulseSequence: A PulseSequence corresponding to the full circuit. """ raise NotImplementedError("to_pulse_sequence has not been implemented yet.") @@ -130,10 +135,10 @@ def coefficient(self) -> int: return self._coef @property - def basis_rotation_gates(self) -> Tuple[Gate, ...]: + def basis_rotation_gates(self) -> tuple[Gate, ...]: """Returns the basis rotation gates for this observable. Returns: - Tuple[Gate, ...]: The basis rotation gates for this observable. + tuple[Gate, ...]: The basis rotation gates for this observable. """ raise NotImplementedError @@ -227,7 +232,7 @@ def eigenvalue(self, index: int) -> float: return self.coefficient * self._eigenvalues[index] @property - def ascii_symbols(self) -> Tuple[str, ...]: + def ascii_symbols(self) -> tuple[str, ...]: return tuple( f"{self.coefficient if self.coefficient != 1 else ''}{ascii_symbol}" for ascii_symbol in self._ascii_symbols diff --git a/src/braket/circuits/observables.py b/src/braket/circuits/observables.py index 1472fe104..f26293f1f 100644 --- a/src/braket/circuits/observables.py +++ b/src/braket/circuits/observables.py @@ -18,7 +18,7 @@ import math import numbers from copy import deepcopy -from typing import Dict, List, Tuple, Union +from typing import Union import numpy as np @@ -29,9 +29,9 @@ is_hermitian, verify_quantum_operator_matrix_dimensions, ) -from braket.circuits.qubit_set import QubitSet from braket.circuits.serialization import IRType, OpenQASMSerializationProperties from braket.pulse.pulse_sequence import PulseSequence +from braket.registers.qubit_set import QubitSet class H(StandardObservable): @@ -47,7 +47,7 @@ def __init__(self): def _unscaled(self) -> StandardObservable: return H() - def _to_jaqcd(self) -> List[str]: + def _to_jaqcd(self) -> list[str]: if self.coefficient != 1: raise ValueError("Observable coefficients not supported with Jaqcd") return ["h"] @@ -68,7 +68,7 @@ def to_matrix(self) -> np.ndarray: ) @property - def basis_rotation_gates(self) -> Tuple[Gate, ...]: + def basis_rotation_gates(self) -> tuple[Gate, ...]: return tuple([Gate.Ry(-math.pi / 4)]) @@ -88,7 +88,7 @@ def __init__(self): def _unscaled(self) -> Observable: return I() - def _to_jaqcd(self) -> List[str]: + def _to_jaqcd(self) -> list[str]: if self.coefficient != 1: raise ValueError("Observable coefficients not supported with Jaqcd") return ["i"] @@ -107,7 +107,7 @@ def to_matrix(self) -> np.ndarray: return self.coefficient * np.eye(2, dtype=complex) @property - def basis_rotation_gates(self) -> Tuple[Gate, ...]: + def basis_rotation_gates(self) -> tuple[Gate, ...]: return () @property @@ -138,7 +138,7 @@ def __init__(self): def _unscaled(self) -> StandardObservable: return X() - def _to_jaqcd(self) -> List[str]: + def _to_jaqcd(self) -> list[str]: if self.coefficient != 1: raise ValueError("Observable coefficients not supported with Jaqcd") return ["x"] @@ -165,7 +165,7 @@ def to_matrix(self) -> np.ndarray: return self.coefficient * np.array([[0.0, 1.0], [1.0, 0.0]], dtype=complex) @property - def basis_rotation_gates(self) -> Tuple[Gate, ...]: + def basis_rotation_gates(self) -> tuple[Gate, ...]: return tuple([Gate.H()]) @@ -185,7 +185,7 @@ def __init__(self): def _unscaled(self) -> StandardObservable: return Y() - def _to_jaqcd(self) -> List[str]: + def _to_jaqcd(self) -> list[str]: if self.coefficient != 1: raise ValueError("Observable coefficients not supported with Jaqcd") return ["y"] @@ -204,7 +204,7 @@ def to_matrix(self) -> np.ndarray: return self.coefficient * np.array([[0.0, -1.0j], [1.0j, 0.0]], dtype=complex) @property - def basis_rotation_gates(self) -> Tuple[Gate, ...]: + def basis_rotation_gates(self) -> tuple[Gate, ...]: return tuple([Gate.Z(), Gate.S(), Gate.H()]) @@ -224,7 +224,7 @@ def __init__(self): def _unscaled(self) -> StandardObservable: return Z() - def _to_jaqcd(self) -> List[str]: + def _to_jaqcd(self) -> list[str]: if self.coefficient != 1: raise ValueError("Observable coefficients not supported with Jaqcd") return ["z"] @@ -243,7 +243,7 @@ def to_matrix(self) -> np.ndarray: return self.coefficient * np.array([[1.0, 0.0], [0.0, -1.0]], dtype=complex) @property - def basis_rotation_gates(self) -> Tuple[Gate, ...]: + def basis_rotation_gates(self) -> tuple[Gate, ...]: return () @@ -253,10 +253,10 @@ def basis_rotation_gates(self) -> Tuple[Gate, ...]: class TensorProduct(Observable): """Tensor product of observables""" - def __init__(self, observables: List[Observable]): + def __init__(self, observables: list[Observable]): """ Args: - observables (List[Observable]): List of observables for tensor product + observables (list[Observable]): List of observables for tensor product Examples: >>> t1 = Observable.Y() @ Observable.X() @@ -304,7 +304,7 @@ def __init__(self, observables: List[Observable]): self._all_eigenvalues = None @property - def ascii_symbols(self) -> Tuple[str, ...]: + def ascii_symbols(self) -> tuple[str, ...]: return tuple( f"{self.coefficient if self.coefficient != 1 else ''}" f"{'@'.join([obs.ascii_symbols[0] for obs in self.factors])}" @@ -316,7 +316,7 @@ def _unscaled(self) -> Observable: copied._coef = 1 return copied - def _to_jaqcd(self) -> List[str]: + def _to_jaqcd(self) -> list[str]: if self.coefficient != 1: raise ValueError("Observable coefficients not supported with Jaqcd") ir = [] @@ -345,8 +345,8 @@ def _to_openqasm( return f"{coef_prefix}{' @ '.join(factors)}" @property - def factors(self) -> Tuple[Observable, ...]: - """Tuple[Observable]: The observables that comprise this tensor product.""" + def factors(self) -> tuple[Observable, ...]: + """tuple[Observable]: The observables that comprise this tensor product.""" return self._factors def to_matrix(self) -> np.ndarray: @@ -355,10 +355,10 @@ def to_matrix(self) -> np.ndarray: ) @property - def basis_rotation_gates(self) -> Tuple[Gate, ...]: + def basis_rotation_gates(self) -> tuple[Gate, ...]: """Returns the basis rotation gates for this observable. Returns: - Tuple[Gate, ...]: The basis rotation gates for this observable. + tuple[Gate, ...]: The basis rotation gates for this observable. """ gates = [] for obs in self.factors: @@ -413,7 +413,7 @@ def __eq__(self, other): return self.matrix_equivalence(other) @staticmethod - def _compute_eigenvalues(observables: Tuple[Observable], num_qubits: int) -> np.ndarray: + def _compute_eigenvalues(observables: tuple[Observable], num_qubits: int) -> np.ndarray: if False in [isinstance(observable, StandardObservable) for observable in observables]: # Tensor product of observables contains a mixture # of standard and non-standard observables @@ -440,10 +440,10 @@ def _compute_eigenvalues(observables: Tuple[Observable], num_qubits: int) -> np. class Sum(Observable): """Sum of observables""" - def __init__(self, observables: List[Observable], display_name: str = "Hamiltonian"): + def __init__(self, observables: list[Observable], display_name: str = "Hamiltonian"): """ Args: - observables (List[Observable]): List of observables for Sum + observables (list[Observable]): List of observables for Sum display_name (str): Name to use for an instance of this Sum observable for circuit diagrams. Defaults to `Hamiltonian`. @@ -474,13 +474,13 @@ def __mul__(self, other) -> Observable: return sum_copy raise TypeError("Observable coefficients must be numbers.") - def _to_jaqcd(self) -> List[str]: + def _to_jaqcd(self) -> list[str]: raise NotImplementedError("Sum Observable is not supported in Jaqcd") def _to_openqasm( self, serialization_properties: OpenQASMSerializationProperties, - target: List[QubitSet] = None, + target: list[QubitSet] = None, ) -> str: if len(self.summands) != len(target): raise ValueError( @@ -502,15 +502,15 @@ def _to_openqasm( ).replace("+ -", "- ") @property - def summands(self) -> Tuple[Observable, ...]: - """Tuple[Observable]: The observables that comprise this sum.""" + def summands(self) -> tuple[Observable, ...]: + """tuple[Observable]: The observables that comprise this sum.""" return self._summands def to_matrix(self) -> np.ndarray: raise NotImplementedError("Matrix operation is not supported for Sum") @property - def basis_rotation_gates(self) -> Tuple[Gate, ...]: + def basis_rotation_gates(self) -> tuple[Gate, ...]: raise NotImplementedError("Basis rotation calculation not supported for Sum") @property @@ -527,7 +527,7 @@ def __eq__(self, other): return repr(self) == repr(other) @staticmethod - def _compute_eigenvalues(observables: Tuple[Observable], num_qubits: int) -> np.ndarray: + def _compute_eigenvalues(observables: tuple[Observable], num_qubits: int) -> np.ndarray: raise NotImplementedError("Eigenvalue calculation not supported for Sum") @@ -572,7 +572,7 @@ def __init__(self, matrix: np.ndarray, display_name: str = "Hermitian"): def _unscaled(self) -> Observable: return Hermitian(matrix=self._matrix, display_name=self.ascii_symbols[0]) - def _to_jaqcd(self) -> List[List[List[List[float]]]]: + def _to_jaqcd(self) -> list[list[list[list[float]]]]: if self.coefficient != 1: raise ValueError("Observable coefficients not supported with Jaqcd") return [ @@ -607,7 +607,7 @@ def __eq__(self, other) -> bool: return self.matrix_equivalence(other) @property - def basis_rotation_gates(self) -> Tuple[Gate, ...]: + def basis_rotation_gates(self) -> tuple[Gate, ...]: return self._diagonalizing_gates @property @@ -622,7 +622,7 @@ def eigenvalue(self, index: int) -> float: return self._eigenvalues[index] @staticmethod - def _get_eigendecomposition(matrix: np.ndarray) -> Dict[str, np.ndarray]: + def _get_eigendecomposition(matrix: np.ndarray) -> dict[str, np.ndarray]: """ Decomposes the Hermitian matrix into its eigenvectors and associated eigenvalues. The eigendecomposition is cached so that if another Hermitian observable @@ -633,7 +633,7 @@ def _get_eigendecomposition(matrix: np.ndarray) -> Dict[str, np.ndarray]: matrix (ndarray): The Hermitian matrix. Returns: - Dict[str, ndarray]: The keys are "eigenvectors_conj_t", mapping to the + dict[str, ndarray]: The keys are "eigenvectors_conj_t", mapping to the conjugate transpose of a matrix whose columns are the eigenvectors of the matrix, and "eigenvalues", a list of associated eigenvalues in the order of their corresponding eigenvectors in the "eigenvectors" matrix. These cached values @@ -657,13 +657,13 @@ def __repr__(self): Observable.register_observable(Hermitian) -def observable_from_ir(ir_observable: List[Union[str, List[List[List[float]]]]]) -> Observable: +def observable_from_ir(ir_observable: list[Union[str, list[list[list[float]]]]]) -> Observable: """ Create an observable from the IR observable list. This can be a tensor product of observables or a single observable. Args: - ir_observable (List[Union[str, List[List[List[float]]]]]): observable as defined in IR + ir_observable (list[Union[str, list[list[list[float]]]]]): observable as defined in IR Returns: Observable: observable object @@ -675,7 +675,7 @@ def observable_from_ir(ir_observable: List[Union[str, List[List[List[float]]]]]) return observable -def _observable_from_ir_list_item(observable: Union[str, List[List[List[float]]]]) -> Observable: +def _observable_from_ir_list_item(observable: Union[str, list[list[list[float]]]]) -> Observable: if observable == "i": return I() elif observable == "h": diff --git a/src/braket/circuits/quantum_operator.py b/src/braket/circuits/quantum_operator.py index 846573c67..df67bf199 100644 --- a/src/braket/circuits/quantum_operator.py +++ b/src/braket/circuits/quantum_operator.py @@ -13,7 +13,8 @@ from __future__ import annotations -from typing import Any, Optional, Sequence, Tuple +from collections.abc import Sequence +from typing import Any, Optional import numpy as np @@ -96,8 +97,8 @@ def qubit_count(self) -> int: return self._qubit_count @property - def ascii_symbols(self) -> Tuple[str, ...]: - """Tuple[str, ...]: Returns the ascii symbols for the quantum operator.""" + def ascii_symbols(self) -> tuple[str, ...]: + """tuple[str, ...]: Returns the ascii symbols for the quantum operator.""" return self._ascii_symbols @property diff --git a/src/braket/circuits/quantum_operator_helpers.py b/src/braket/circuits/quantum_operator_helpers.py index cb6da9e80..a264d0b38 100644 --- a/src/braket/circuits/quantum_operator_helpers.py +++ b/src/braket/circuits/quantum_operator_helpers.py @@ -11,8 +11,8 @@ # ANY KIND, either express or implied. See the License for the specific # language governing permissions and limitations under the License. +from collections.abc import Iterable from functools import lru_cache -from typing import Iterable import numpy as np diff --git a/src/braket/circuits/qubit.py b/src/braket/circuits/qubit.py index 479a453e3..f82e91fa0 100644 --- a/src/braket/circuits/qubit.py +++ b/src/braket/circuits/qubit.py @@ -11,58 +11,4 @@ # ANY KIND, either express or implied. See the License for the specific # language governing permissions and limitations under the License. -from __future__ import annotations - -import numbers -from typing import Union - -QubitInput = Union["Qubit", int] - - -class Qubit(int): - """ - A quantum bit index. The index of this qubit is locally scoped towards the contained - circuit. This may not be the exact qubit index on the quantum device. - """ - - def __new__(cls, index: int): - """ - Args: - index (int): Index of the qubit. - - Raises: - ValueError: If `index` is less than zero. - - Examples: - >>> Qubit(0) - >>> Qubit(1) - """ - if not isinstance(index, numbers.Integral): - raise TypeError(f"Supplied qubit index, {index}, must be an integer.") - if index < 0: - raise ValueError(f"Supplied qubit index, {index}, cannot be less than zero.") - return super().__new__(cls, index) - - def __repr__(self): - return f"Qubit({super().__repr__()})" - - def __str__(self): - return self.__repr__() - - @staticmethod - def new(qubit: QubitInput) -> Qubit: - """ - Helper constructor - if input is a `Qubit` it returns the same value, - else a new `Qubit` is constructed. - - Args: - qubit (QubitInput): `Qubit` index. If `type == Qubit` then the `qubit` is returned. - - Returns: - Qubit: The qubit. - """ - - if isinstance(qubit, Qubit): - return qubit - else: - return Qubit(qubit) +from braket.registers import Qubit, QubitInput # noqa: F401 diff --git a/src/braket/circuits/qubit_set.py b/src/braket/circuits/qubit_set.py index d4571e91a..b2c1bbfc3 100644 --- a/src/braket/circuits/qubit_set.py +++ b/src/braket/circuits/qubit_set.py @@ -11,83 +11,4 @@ # ANY KIND, either express or implied. See the License for the specific # language governing permissions and limitations under the License. -from __future__ import annotations - -from typing import Any, Dict, Iterable, Union - -from boltons.setutils import IndexedSet - -from braket.circuits.qubit import Qubit, QubitInput - -QubitSetInput = Union[QubitInput, Iterable[QubitInput]] - - -def _flatten(other: Any) -> Any: - if isinstance(other, Iterable) and not isinstance(other, str): - for item in other: - yield from _flatten(item) - else: - yield other - - -class QubitSet(IndexedSet): - """ - An ordered, unique set of quantum bits. - - Note: - QubitSet implements `__hash__()` but is a mutable object, therefore be careful when - mutating this object. - """ - - def __init__(self, qubits: QubitSetInput = None): - """ - Args: - qubits (QubitSetInput): Qubits to be included in the `QubitSet`. Default is `None`. - - Examples: - >>> qubits = QubitSet([0, 1]) - >>> for qubit in qubits: - ... print(qubit) - ... - Qubit(0) - Qubit(1) - - >>> qubits = QubitSet([0, 1, [2, 3]]) - >>> for qubit in qubits: - ... print(qubit) - ... - Qubit(0) - Qubit(1) - Qubit(2) - Qubit(3) - """ - - _qubits = [Qubit.new(qubit) for qubit in _flatten(qubits)] if qubits is not None else None - super().__init__(_qubits) - - def map(self, mapping: Dict[QubitInput, QubitInput]) -> QubitSet: - """ - Creates a new `QubitSet` where this instance's qubits are mapped to the values in `mapping`. - If this instance contains a qubit that is not in the `mapping` that qubit is not modified. - - Args: - mapping (Dict[QubitInput, QubitInput]): A dictionary of qubit mappings to - apply. Key is the qubit in this instance to target, and the value is what - the key will be changed to. - - Returns: - QubitSet: A new QubitSet with the `mapping` applied. - - Examples: - >>> qubits = QubitSet([0, 1]) - >>> mapping = {0: 10, Qubit(1): Qubit(11)} - >>> qubits.map(mapping) - QubitSet([Qubit(10), Qubit(11)]) - """ - - new_qubits = [mapping.get(qubit, qubit) for qubit in self] - - return QubitSet(new_qubits) - - def __hash__(self): - return hash(tuple(self)) +from braket.registers.qubit_set import QubitSet, QubitSetInput # noqa: F401 diff --git a/src/braket/circuits/result_type.py b/src/braket/circuits/result_type.py index d66eb1f7b..6a9440e04 100644 --- a/src/braket/circuits/result_type.py +++ b/src/braket/circuits/result_type.py @@ -13,19 +13,19 @@ from __future__ import annotations -from typing import Any, Dict, List, Type, Union +from typing import Any, Union from braket.circuits.free_parameter import FreeParameter from braket.circuits.observable import Observable from braket.circuits.observables import Sum -from braket.circuits.qubit import QubitInput -from braket.circuits.qubit_set import QubitSet, QubitSetInput from braket.circuits.serialization import ( IRType, OpenQASMSerializationProperties, SerializationProperties, ) from braket.pulse.pulse_sequence import PulseSequence +from braket.registers.qubit import QubitInput +from braket.registers.qubit_set import QubitSet, QubitSetInput class ResultType: @@ -35,10 +35,10 @@ class ResultType: the metadata that defines what a requested result type is and what it does. """ - def __init__(self, ascii_symbols: List[str]): + def __init__(self, ascii_symbols: list[str]): """ Args: - ascii_symbols (List[str]): ASCII string symbols for the result type. This is used when + ascii_symbols (list[str]): ASCII string symbols for the result type. This is used when printing a diagram of circuits. Raises: @@ -51,8 +51,8 @@ def __init__(self, ascii_symbols: List[str]): self._ascii_symbols = ascii_symbols @property - def ascii_symbols(self) -> List[str]: - """List[str]: Returns the ascii symbols for the requested result type.""" + def ascii_symbols(self) -> list[str]: + """list[str]: Returns the ascii symbols for the requested result type.""" return self._ascii_symbols @property @@ -68,7 +68,7 @@ def name(self) -> str: def to_ir( self, ir_type: IRType = IRType.JAQCD, - serialization_properties: SerializationProperties = None, + serialization_properties: SerializationProperties | None = None, **kwargs, ) -> Any: """Returns IR object of the result type @@ -76,9 +76,9 @@ def to_ir( Args: ir_type(IRType) : The IRType to use for converting the result type object to its IR representation. Defaults to IRType.JAQCD. - serialization_properties (SerializationProperties): The serialization properties to use - while serializing the object to the IR representation. The serialization properties - supplied must correspond to the supplied `ir_type`. Defaults to None. + serialization_properties (SerializationProperties | None): The serialization properties + to use while serializing the object to the IR representation. The serialization + properties supplied must correspond to the supplied `ir_type`. Defaults to None. Returns: Any: IR object of the result type @@ -129,12 +129,14 @@ def to_pulse_sequence( to use while serializing the object to the IR representation. Returns: - str: Representing the openqasm representation of the result type. + PulseSequence: A PulseSequence corresponding to the full circuit. """ raise NotImplementedError("to_pulse_sequence has not been implemented yet.") def copy( - self, target_mapping: Dict[QubitInput, QubitInput] = None, target: QubitSetInput = None + self, + target_mapping: dict[QubitInput, QubitInput] | None = None, + target: QubitSetInput | None = None, ) -> ResultType: """ Return a shallow copy of the result type. @@ -144,10 +146,10 @@ def copy( qubits. This is useful apply an instruction to a circuit and change the target qubits. Args: - target_mapping (Dict[QubitInput, QubitInput]): A dictionary of + target_mapping (dict[QubitInput, QubitInput] | None): A dictionary of qubit mappings to apply to the target. Key is the qubit in this `target` and the value is what the key is changed to. Default = `None`. - target (QubitSetInput): Target qubits for the new instruction. + target (QubitSetInput | None): Target qubits for the new instruction. Returns: ResultType: A shallow copy of the result type. @@ -178,11 +180,11 @@ def copy( return copy @classmethod - def register_result_type(cls, result_type: Type[ResultType]) -> None: + def register_result_type(cls, result_type: type[ResultType]) -> None: """Register a result type implementation by adding it into the `ResultType` class. Args: - result_type (Type[ResultType]): `ResultType` class to register. + result_type (type[ResultType]): `ResultType` class to register. """ setattr(cls, result_type.__name__, result_type) @@ -204,14 +206,14 @@ class ObservableResultType(ResultType): """ def __init__( - self, ascii_symbols: List[str], observable: Observable, target: QubitSetInput = None + self, ascii_symbols: list[str], observable: Observable, target: QubitSetInput | None = None ): """ Args: - ascii_symbols (List[str]): ASCII string symbols for the result type. This is used when + ascii_symbols (list[str]): ASCII string symbols for the result type. This is used when printing a diagram of circuits. observable (Observable): the observable for the result type - target (QubitSetInput): Target qubits that the + target (QubitSetInput | None): Target qubits that the result type is requested for. Default is `None`, which means the observable must only operate on 1 qubit and it will be applied to all qubits in parallel @@ -301,10 +303,10 @@ class ObservableParameterResultType(ObservableResultType): def __init__( self, - ascii_symbols: List[str], + ascii_symbols: list[str], observable: Observable, - target: QubitSetInput = None, - parameters: List[Union[str, FreeParameter]] = None, + target: QubitSetInput | None = None, + parameters: list[Union[str, FreeParameter]] | None = None, ): super().__init__(ascii_symbols, observable, target) @@ -316,13 +318,13 @@ def __init__( """ Args: - ascii_symbols (List[str]): ASCII string symbols for the result type. This is used when + ascii_symbols (list[str]): ASCII string symbols for the result type. This is used when printing a diagram of circuits. observable (Observable): the observable for the result type. - target (QubitSetInput): Target qubits that the result type is requested for. + target (QubitSetInput | None): Target qubits that the result type is requested for. Default is `None`, which means the observable must only operate on 1 qubit and it will be applied to all qubits in parallel. - parameters (List[Union[str, FreeParameter]]): List of string inputs or + parameters (list[Union[str, FreeParameter]] | None): List of string inputs or FreeParameter objects. These inputs will be used as parameters for gradient calculation. Default: `all`. @@ -334,7 +336,7 @@ def __init__( """ @property - def parameters(self) -> List[str]: + def parameters(self) -> list[str]: return self._parameters def __repr__(self) -> str: diff --git a/src/braket/circuits/result_types.py b/src/braket/circuits/result_types.py index 31ff76323..e21dd39b1 100644 --- a/src/braket/circuits/result_types.py +++ b/src/braket/circuits/result_types.py @@ -15,20 +15,20 @@ import re from functools import reduce -from typing import List, Union +from typing import Union import braket.ir.jaqcd as ir from braket.circuits import circuit from braket.circuits.free_parameter import FreeParameter from braket.circuits.observable import Observable from braket.circuits.observables import Sum -from braket.circuits.qubit_set import QubitSet, QubitSetInput from braket.circuits.result_type import ( ObservableParameterResultType, ObservableResultType, ResultType, ) from braket.circuits.serialization import IRType, OpenQASMSerializationProperties +from braket.registers.qubit_set import QubitSet, QubitSetInput """ To add a new result type: @@ -91,10 +91,10 @@ class DensityMatrix(ResultType): This is available on simulators only when `shots=0`. """ - def __init__(self, target: QubitSetInput = None): + def __init__(self, target: QubitSetInput | None = None): """ Args: - target (QubitSetInput): The target qubits + target (QubitSetInput | None): The target qubits of the reduced density matrix. Default is `None`, and the full density matrix is returned. @@ -134,10 +134,10 @@ def _to_openqasm(self, serialization_properties: OpenQASMSerializationProperties @staticmethod @circuit.subroutine(register=True) - def density_matrix(target: QubitSetInput = None) -> ResultType: + def density_matrix(target: QubitSetInput | None = None) -> ResultType: """Registers this function into the circuit class. Args: - target (QubitSetInput): The target qubits + target (QubitSetInput | None): The target qubits of the reduced density matrix. Default is `None`, and the full density matrix is returned. @@ -178,20 +178,20 @@ class AdjointGradient(ObservableParameterResultType): def __init__( self, observable: Observable, - target: List[QubitSetInput] = None, - parameters: List[Union[str, FreeParameter]] = None, + target: list[QubitSetInput] | None = None, + parameters: list[Union[str, FreeParameter]] | None = None, ): """ Args: observable (Observable): The expectation value of this observable is the function against which parameters in the gradient are differentiated. - target (List[QubitSetInput]): Target qubits that the result type is requested for. - Each term in the target list should have the same number of qubits as the + target (list[QubitSetInput] | None): Target qubits that the result type is requested + for. Each term in the target list should have the same number of qubits as the corresponding term in the observable. Default is `None`, which means the observable must operate only on 1 qubit and it is applied to all qubits in parallel. - parameters (List[Union[str, FreeParameter]]): The free parameters in the circuit to - differentiate with respect to. Default: `all`. + parameters (list[Union[str, FreeParameter]] | None): The free parameters in the circuit + to differentiate with respect to. Default: `all`. Raises: ValueError: If the observable's qubit count does not equal the number of target @@ -240,21 +240,21 @@ def _to_openqasm(self, serialization_properties: OpenQASMSerializationProperties @circuit.subroutine(register=True) def adjoint_gradient( observable: Observable, - target: List[QubitSetInput] = None, - parameters: List[Union[str, FreeParameter]] = None, + target: list[QubitSetInput] | None = None, + parameters: list[Union[str, FreeParameter]] | None = None, ) -> ResultType: """Registers this function into the circuit class. Args: observable (Observable): The expectation value of this observable is the function against which parameters in the gradient are differentiated. - target (List[QubitSetInput]): Target qubits that the result type is requested for. - Each term in the target list should have the same number of qubits as the + target (list[QubitSetInput] | None): Target qubits that the result type is requested + for. Each term in the target list should have the same number of qubits as the corresponding term in the observable. Default is `None`, which means the observable must operate only on 1 qubit and it is applied to all qubits in parallel. - parameters (List[Union[str, FreeParameter]]): The free parameters in the circuit to - differentiate with respect to. Default: `all`. + parameters (list[Union[str, FreeParameter]] | None): The free parameters in the circuit + to differentiate with respect to. Default: `all`. Returns: ResultType: gradient computed via adjoint differentiation as a requested result type @@ -279,10 +279,10 @@ class Amplitude(ResultType): This is available on simulators only when `shots=0`. """ - def __init__(self, state: List[str]): + def __init__(self, state: list[str]): """ Args: - state (List[str]): list of quantum states as strings with "0" and "1" + state (list[str]): list of quantum states as strings with "0" and "1" Raises: ValueError: If state is `None` or an empty list, or @@ -293,7 +293,7 @@ def __init__(self, state: List[str]): """ if ( not state - or not isinstance(state, List) + or not isinstance(state, list) or not all( isinstance(amplitude, str) and re.fullmatch("^[01]+$", amplitude) for amplitude in state @@ -306,7 +306,7 @@ def __init__(self, state: List[str]): self._state = state @property - def state(self) -> List[str]: + def state(self) -> list[str]: return self._state def _to_jaqcd(self) -> ir.Amplitude: @@ -318,11 +318,11 @@ def _to_openqasm(self, serialization_properties: OpenQASMSerializationProperties @staticmethod @circuit.subroutine(register=True) - def amplitude(state: List[str]) -> ResultType: + def amplitude(state: list[str]) -> ResultType: """Registers this function into the circuit class. Args: - state (List[str]): list of quantum states as strings with "0" and "1" + state (list[str]): list of quantum states as strings with "0" and "1" Returns: ResultType: state vector as a requested result type @@ -361,10 +361,10 @@ class Probability(ResultType): only on simulators and represents the exact result. """ - def __init__(self, target: QubitSetInput = None): + def __init__(self, target: QubitSetInput | None = None): """ Args: - target (QubitSetInput): The target qubits that the + target (QubitSetInput | None): The target qubits that the result type is requested for. Default is `None`, which means all qubits for the circuit. @@ -404,11 +404,11 @@ def _to_openqasm(self, serialization_properties: OpenQASMSerializationProperties @staticmethod @circuit.subroutine(register=True) - def probability(target: QubitSetInput = None) -> ResultType: + def probability(target: QubitSetInput | None = None) -> ResultType: """Registers this function into the circuit class. Args: - target (QubitSetInput): The target qubits that the + target (QubitSetInput | None): The target qubits that the result type is requested for. Default is `None`, which means all qubits for the circuit. @@ -451,11 +451,11 @@ class Expectation(ObservableResultType): See :mod:`braket.circuits.observables` module for all of the supported observables. """ - def __init__(self, observable: Observable, target: QubitSetInput = None): + def __init__(self, observable: Observable, target: QubitSetInput | None = None): """ Args: observable (Observable): the observable for the result type - target (QubitSetInput): Target qubits that the + target (QubitSetInput | None): Target qubits that the result type is requested for. Default is `None`, which means the observable must operate only on 1 qubit and it is applied to all qubits in parallel. @@ -501,12 +501,12 @@ def to_pulse_sequence(self, serialization_properties: OpenQASMSerializationPrope @staticmethod @circuit.subroutine(register=True) - def expectation(observable: Observable, target: QubitSetInput = None) -> ResultType: + def expectation(observable: Observable, target: QubitSetInput | None = None) -> ResultType: """Registers this function into the circuit class. Args: observable (Observable): the observable for the result type - target (QubitSetInput): Target qubits that the + target (QubitSetInput | None): Target qubits that the result type is requested for. Default is `None`, which means the observable must operate only on 1 qubit and it is applied to all qubits in parallel. @@ -534,11 +534,11 @@ class Sample(ObservableResultType): See :mod:`braket.circuits.observables` module for all of the supported observables. """ - def __init__(self, observable: Observable, target: QubitSetInput = None): + def __init__(self, observable: Observable, target: QubitSetInput | None = None): """ Args: observable (Observable): the observable for the result type - target (QubitSetInput): Target qubits that the + target (QubitSetInput | None): Target qubits that the result type is requested for. Default is `None`, which means the observable must operate only on 1 qubit and it is applied to all qubits in parallel. @@ -576,12 +576,12 @@ def _to_openqasm(self, serialization_properties: OpenQASMSerializationProperties @staticmethod @circuit.subroutine(register=True) - def sample(observable: Observable, target: QubitSetInput = None) -> ResultType: + def sample(observable: Observable, target: QubitSetInput | None = None) -> ResultType: """Registers this function into the circuit class. Args: observable (Observable): the observable for the result type - target (QubitSetInput): Target qubits that the + target (QubitSetInput | None): Target qubits that the result type is requested for. Default is `None`, which means the observable must operate only on 1 qubit and it is applied to all qubits in parallel. @@ -610,11 +610,11 @@ class Variance(ObservableResultType): See :mod:`braket.circuits.observables` module for all of the supported observables. """ - def __init__(self, observable: Observable, target: QubitSetInput = None): + def __init__(self, observable: Observable, target: QubitSetInput | None = None): """ Args: observable (Observable): the observable for the result type - target (QubitSetInput): Target qubits that the + target (QubitSetInput | None): Target qubits that the result type is requested for. Default is `None`, which means the observable must operate only on 1 qubit and it is applied to all qubits in parallel. @@ -652,12 +652,12 @@ def _to_openqasm(self, serialization_properties: OpenQASMSerializationProperties @staticmethod @circuit.subroutine(register=True) - def variance(observable: Observable, target: QubitSetInput = None) -> ResultType: + def variance(observable: Observable, target: QubitSetInput | None = None) -> ResultType: """Registers this function into the circuit class. Args: observable (Observable): the observable for the result type - target (QubitSetInput): Target qubits that the + target (QubitSetInput | None): Target qubits that the result type is requested for. Default is `None`, which means the observable must only operate on 1 qubit and it will be applied to all qubits in parallel diff --git a/src/braket/circuits/serialization.py b/src/braket/circuits/serialization.py index 596a1f364..1e0826e80 100644 --- a/src/braket/circuits/serialization.py +++ b/src/braket/circuits/serialization.py @@ -39,7 +39,7 @@ class OpenQASMSerializationProperties: Properties for serializing a circuit to OpenQASM. qubit_reference_type (QubitReferenceType): determines whether to use - logical qubits or physical qubits (__qubits__[i] vs $i). + logical qubits or physical qubits (q[i] vs $i). """ qubit_reference_type: QubitReferenceType = QubitReferenceType.VIRTUAL @@ -53,7 +53,7 @@ def format_target(self, target: int) -> str: str: The OpenQASM representation of the target qubit. """ qubit_reference_format = ( - "__qubits__[{}]" if self.qubit_reference_type == QubitReferenceType.VIRTUAL else "${}" + "q[{}]" if self.qubit_reference_type == QubitReferenceType.VIRTUAL else "${}" ) return qubit_reference_format.format(target) diff --git a/src/braket/circuits/unitary_calculation.py b/src/braket/circuits/unitary_calculation.py index 628d230e9..ebc3c7878 100644 --- a/src/braket/circuits/unitary_calculation.py +++ b/src/braket/circuits/unitary_calculation.py @@ -11,7 +11,7 @@ # ANY KIND, either express or implied. See the License for the specific # language governing permissions and limitations under the License. -from typing import Iterable +from collections.abc import Iterable import numpy as np from scipy.linalg import fractional_matrix_power @@ -19,79 +19,8 @@ from braket.circuits.compiler_directive import CompilerDirective from braket.circuits.gate import Gate from braket.circuits.instruction import Instruction -from braket.circuits.qubit_set import QubitSet from braket.default_simulator.linalg_utils import multiply_matrix - - -def _einsum_subscripts(targets: QubitSet, qubit_count: int) -> str: - target_count = len(targets) - - gate_left_indexes = list(range(target_count)) - un_left_indexes = list(range(target_count, target_count + qubit_count)) - un_right_indexes = list(range(target_count + qubit_count, target_count + 2 * qubit_count)) - - gate_right_indexes = [un_left_indexes[-1 - target] for target in targets] - - result_left_indexes = un_left_indexes.copy() - for pos, target in enumerate(targets): - result_left_indexes[-1 - target] = gate_left_indexes[pos] - - return ( - gate_left_indexes + gate_right_indexes, - un_left_indexes + un_right_indexes, - result_left_indexes + un_right_indexes, - ) - - -def calculate_unitary(qubit_count: int, instructions: Iterable[Instruction]) -> np.ndarray: - """ - Returns the unitary matrix representation for all the `instructions` with a given - `qubit_count`. - *Note*: The performance of this method degrades with qubit count. It might be slow for - qubit count > 10. - - Args: - qubit_count (int): Total number of qubits, enough for all the `instructions`. - instructions (Iterable[Instruction]): The instructions for which the unitary matrix - will be calculated. - - Returns: - np.ndarray: A numpy array with shape (2^qubit_count, 2^qubit_count) representing the - `instructions` as a unitary. - - Raises: - TypeError: If `instructions` is not composed only of `Gate` instances, - i.e. a circuit with `Noise` operators will raise this error. - Any `CompilerDirective` instructions will be ignored, as these should - not affect the unitary representation of the circuit. - """ - unitary = np.eye(2**qubit_count, dtype=complex) - un_tensor = np.reshape(unitary, qubit_count * [2, 2]) - - for instr in instructions: - if isinstance(instr.operator, CompilerDirective): - continue - - if not isinstance(instr.operator, Gate): - raise TypeError("Only Gate operators are supported to build the unitary") - - matrix = instr.operator.to_matrix() - targets = instr.target - - gate_indexes, un_indexes, result_indexes = _einsum_subscripts(targets, qubit_count) - gate_matrix = np.reshape(matrix, len(targets) * [2, 2]) - - un_tensor = np.einsum( - gate_matrix, - gate_indexes, - un_tensor, - un_indexes, - result_indexes, - dtype=complex, - casting="no", - ) - - return np.reshape(un_tensor, 2 * [2**qubit_count]) +from braket.registers.qubit_set import QubitSet def calculate_unitary_big_endian( diff --git a/src/braket/devices/device.py b/src/braket/devices/device.py index 6ecb2ecc2..49f510edf 100644 --- a/src/braket/devices/device.py +++ b/src/braket/devices/device.py @@ -12,7 +12,7 @@ # language governing permissions and limitations under the License. from abc import ABC, abstractmethod -from typing import Dict, List, Optional, Union +from typing import Optional, Union from braket.annealing.problem import Problem from braket.circuits import Circuit @@ -37,7 +37,7 @@ def run( self, task_specification: Union[Circuit, Problem], shots: Optional[int], - inputs: Optional[Dict[str, float]], + inputs: Optional[dict[str, float]], *args, **kwargs ) -> QuantumTask: @@ -49,7 +49,7 @@ def run( to run on device. shots (Optional[int]): The number of times to run the quantum task on the device. Default is `None`. - inputs (Optional[Dict[str, float]]): Inputs to be passed along with the + inputs (Optional[dict[str, float]]): Inputs to be passed along with the IR. If IR is an OpenQASM Program, the inputs will be updated with this value. Not all devices and IR formats support inputs. Default: {}. @@ -62,24 +62,24 @@ def run_batch( self, task_specifications: Union[ Union[Circuit, Problem], - List[Union[Circuit, Problem]], + list[Union[Circuit, Problem]], ], shots: Optional[int], max_parallel: Optional[int], - inputs: Optional[Union[Dict[str, float], List[Dict[str, float]]]], + inputs: Optional[Union[dict[str, float], list[dict[str, float]]]], *args, **kwargs ) -> QuantumTaskBatch: """Executes a batch of quantum tasks in parallel Args: - task_specifications (Union[Union[Circuit, Problem], List[Union[Circuit, Problem]]]): + task_specifications (Union[Union[Circuit, Problem], list[Union[Circuit, Problem]]]): Single instance or list of circuits or problems to run on device. shots (Optional[int]): The number of times to run the circuit or annealing problem. max_parallel (Optional[int]): The maximum number of quantum tasks to run in parallel. Batch creation will fail if this value is greater than the maximum allowed concurrent quantum tasks on the device. - inputs (Optional[Union[Dict[str, float], List[Dict[str, float]]]]): Inputs to be + inputs (Optional[Union[dict[str, float], list[dict[str, float]]]]): Inputs to be passed along with the IR. If the IR supports inputs, the inputs will be updated with this value. diff --git a/src/braket/devices/local_simulator.py b/src/braket/devices/local_simulator.py index a4462ab65..c719978ac 100644 --- a/src/braket/devices/local_simulator.py +++ b/src/braket/devices/local_simulator.py @@ -17,7 +17,7 @@ from itertools import repeat from multiprocessing import Pool from os import cpu_count -from typing import Dict, List, Optional, Set, Union +from typing import Optional, Union import pkg_resources @@ -68,7 +68,7 @@ def run( self, task_specification: Union[Circuit, Problem, Program, AnalogHamiltonianSimulation], shots: int = 0, - inputs: Optional[Dict[str, float]] = None, + inputs: Optional[dict[str, float]] = None, *args, **kwargs, ) -> LocalQuantumTask: @@ -81,7 +81,7 @@ def run( Default is 0, which means that the simulator will compute the exact results based on the quantum task specification. Sampling is not supported for shots=0. - inputs (Optional[Dict[str, float]]): Inputs to be passed along with the + inputs (Optional[dict[str, float]]): Inputs to be passed along with the IR. If the IR supports inputs, the inputs will be updated with this value. Default: {}. @@ -105,24 +105,24 @@ def run_batch( self, task_specifications: Union[ Union[Circuit, Problem, Program, AnalogHamiltonianSimulation], - List[Union[Circuit, Problem, Program, AnalogHamiltonianSimulation]], + list[Union[Circuit, Problem, Program, AnalogHamiltonianSimulation]], ], shots: Optional[int] = 0, max_parallel: Optional[int] = None, - inputs: Optional[Union[Dict[str, float], List[Dict[str, float]]]] = None, + inputs: Optional[Union[dict[str, float], list[dict[str, float]]]] = None, *args, **kwargs, ) -> LocalQuantumTaskBatch: """Executes a batch of quantum tasks in parallel Args: - task_specifications (Union[Union[Circuit, Problem, Program, AnalogHamiltonianSimulation], List[Union[Circuit, Problem, Program, AnalogHamiltonianSimulation]]]): # noqa + task_specifications (Union[Union[Circuit, Problem, Program, AnalogHamiltonianSimulation], list[Union[Circuit, Problem, Program, AnalogHamiltonianSimulation]]]): # noqa Single instance or list of quantum task specification. shots (Optional[int]): The number of times to run the quantum task. Default: 0. max_parallel (Optional[int]): The maximum number of quantum tasks to run in parallel. Default is the number of CPU. - inputs (Optional[Union[Dict[str, float], List[Dict[str, float]]]]): Inputs to be passed + inputs (Optional[Union[dict[str, float], list[dict[str, float]]]]): Inputs to be passed along with the IR. If the IR supports inputs, the inputs will be updated with this value. Default: {}. @@ -188,11 +188,11 @@ def properties(self) -> DeviceCapabilities: return self._delegate.properties @staticmethod - def registered_backends() -> Set[str]: + def registered_backends() -> set[str]: """Gets the backends that have been registered as entry points Returns: - Set[str]: The names of the available backends that can be passed + set[str]: The names of the available backends that can be passed into LocalSimulator's constructor """ return set(_simulator_devices.keys()) @@ -201,7 +201,7 @@ def _run_internal_wrap( self, task_specification: Union[Circuit, Problem, Program, AnalogHamiltonianSimulation], shots: Optional[int] = None, - inputs: Optional[Dict[str, float]] = None, + inputs: Optional[dict[str, float]] = None, *args, **kwargs, ) -> Union[GateModelQuantumTaskResult, AnnealingQuantumTaskResult]: # pragma: no cover @@ -243,7 +243,7 @@ def _( self, circuit: Circuit, shots: Optional[int] = None, - inputs: Optional[Dict[str, float]] = None, + inputs: Optional[dict[str, float]] = None, *args, **kwargs, ): @@ -278,7 +278,7 @@ def _( self, program: Program, shots: Optional[int] = None, - inputs: Optional[Dict[str, float]] = None, + inputs: Optional[dict[str, float]] = None, *args, **kwargs, ): diff --git a/src/braket/error_mitigation/debias.py b/src/braket/error_mitigation/debias.py index 154094a06..305bf7b78 100644 --- a/src/braket/error_mitigation/debias.py +++ b/src/braket/error_mitigation/debias.py @@ -11,8 +11,6 @@ # ANY KIND, either express or implied. See the License for the specific # language governing permissions and limitations under the License. -from typing import List - from braket.device_schema import error_mitigation from braket.error_mitigation.error_mitigation import ErrorMitigation @@ -22,5 +20,5 @@ class Debias(ErrorMitigation): The debias error mitigation scheme. This scheme takes no parameters. """ - def serialize(self) -> List[error_mitigation.Debias]: + def serialize(self) -> list[error_mitigation.Debias]: return [error_mitigation.Debias()] diff --git a/src/braket/error_mitigation/error_mitigation.py b/src/braket/error_mitigation/error_mitigation.py index c03fbf6df..79b1f3e30 100644 --- a/src/braket/error_mitigation/error_mitigation.py +++ b/src/braket/error_mitigation/error_mitigation.py @@ -11,16 +11,14 @@ # ANY KIND, either express or implied. See the License for the specific # language governing permissions and limitations under the License. -from typing import List - from braket.device_schema import error_mitigation class ErrorMitigation: - def serialize(self) -> List[error_mitigation.ErrorMitigationScheme]: + def serialize(self) -> list[error_mitigation.ErrorMitigationScheme]: """ Returns: - List[ErrorMitigationScheme]: A list of service-readable error + list[ErrorMitigationScheme]: A list of service-readable error mitigation scheme descriptions. """ raise NotImplementedError("serialize is not implemented.") diff --git a/src/braket/jobs/__init__.py b/src/braket/jobs/__init__.py index e58ba16eb..0a6fb520f 100644 --- a/src/braket/jobs/__init__.py +++ b/src/braket/jobs/__init__.py @@ -20,7 +20,17 @@ ) from braket.jobs.data_persistence import ( # noqa: F401 load_job_checkpoint, + load_job_result, save_job_checkpoint, save_job_result, ) +from braket.jobs.environment_variables import ( # noqa: F401 + get_checkpoint_dir, + get_hyperparameters, + get_input_data_dir, + get_job_device_arn, + get_job_name, + get_results_dir, +) +from braket.jobs.hybrid_job import hybrid_job # noqa: F401 from braket.jobs.image_uris import Framework, retrieve_image # noqa: F401 diff --git a/src/braket/jobs/_entry_point_template.py b/src/braket/jobs/_entry_point_template.py new file mode 100644 index 000000000..dceea98d1 --- /dev/null +++ b/src/braket/jobs/_entry_point_template.py @@ -0,0 +1,79 @@ +run_entry_point = """ +import cloudpickle +import os +from braket.jobs import get_results_dir, save_job_result +from braket.jobs_data import PersistedJobDataFormat + + +# load and run serialized entry point function +recovered = cloudpickle.loads({serialized}) +def {function_name}(): + try: + # set working directory to results dir + os.chdir(get_results_dir()) + + # create symlinks to input data + links = link_input() + + result = recovered() + finally: + clean_links(links) + if result is not None: + save_job_result(result, data_format=PersistedJobDataFormat.PICKLED_V4) + return result +""" + +symlink_input_data = ''' +from pathlib import Path +from braket.jobs import get_input_data_dir + + +def make_link(input_link_path, input_data_path, links): + """ Create symlink from input_link_path to input_data_path. """ + input_link_path.parent.mkdir(parents=True, exist_ok=True) + input_link_path.symlink_to(input_data_path) + print(input_link_path, '->', input_data_path) + links[input_link_path] = input_data_path + + +def link_input(): + links = {{}} + dirs = set() + # map of data sources to lists of matched local files + prefix_matches = {prefix_matches} + + for channel, data in {input_data_items}: + + if channel in {prefix_channels}: + # link all matched files + for input_link_name in prefix_matches[channel]: + input_link_path = Path(input_link_name) + input_data_path = Path(get_input_data_dir(channel)) / input_link_path.name + make_link(input_link_path, input_data_path, links) + + else: + input_link_path = Path(data) + if channel in {directory_channels}: + # link directory source directly to input channel directory + input_data_path = Path(get_input_data_dir(channel)) + else: + # link file source to file within input channel directory + input_data_path = Path(get_input_data_dir(channel), Path(data).name) + make_link(input_link_path, input_data_path, links) + + return links + + +def clean_links(links): + for link, target in links.items(): + if link.is_symlink and link.readlink() == target: + link.unlink() + + if link.is_relative_to(Path()): + for dir in link.parents[:-1]: + try: + dir.rmdir() + except: + # directory not empty + pass +''' diff --git a/src/braket/jobs/config.py b/src/braket/jobs/config.py index 73467dd3a..a598388e4 100644 --- a/src/braket/jobs/config.py +++ b/src/braket/jobs/config.py @@ -11,8 +11,9 @@ # ANY KIND, either express or implied. See the License for the specific # language governing permissions and limitations under the License. +from __future__ import annotations + from dataclasses import dataclass -from typing import Optional @dataclass @@ -20,12 +21,12 @@ class CheckpointConfig: """Configuration that specifies the location where checkpoint data is stored.""" localPath: str = "/opt/jobs/checkpoints" - s3Uri: Optional[str] = None + s3Uri: str | None = None @dataclass class InstanceConfig: - """Configuration of the instances used to execute the hybrid job.""" + """Configuration of the instance(s) used to run the hybrid job.""" instanceType: str = "ml.m5.large" volumeSizeInGb: int = 30 @@ -36,8 +37,8 @@ class InstanceConfig: class OutputDataConfig: """Configuration that specifies the location for the output of the hybrid job.""" - s3Path: Optional[str] = None - kmsKeyId: Optional[str] = None + s3Path: str | None = None + kmsKeyId: str | None = None @dataclass @@ -61,14 +62,14 @@ class S3DataSourceConfig: def __init__( self, - s3_data, - content_type=None, + s3_data: str, + content_type: str | None = None, ): """Create a definition for input data used by a Braket Hybrid job. Args: s3_data (str): Defines the location of s3 data to train on. - content_type (str): MIME type of the input data (default: None). + content_type (str | None): MIME type of the input data (default: None). """ self.config = { "dataSource": { diff --git a/src/braket/jobs/data_persistence.py b/src/braket/jobs/data_persistence.py index 198a7e225..f2ec9b6fa 100644 --- a/src/braket/jobs/data_persistence.py +++ b/src/braket/jobs/data_persistence.py @@ -11,15 +11,18 @@ # ANY KIND, either express or implied. See the License for the specific # language governing permissions and limitations under the License. -import os -from typing import Any, Dict +from __future__ import annotations +from pathlib import Path +from typing import Any + +from braket.jobs.environment_variables import get_checkpoint_dir, get_job_name, get_results_dir from braket.jobs.serialization import deserialize_values, serialize_values from braket.jobs_data import PersistedJobData, PersistedJobDataFormat def save_job_checkpoint( - checkpoint_data: Dict[str, Any], + checkpoint_data: dict[str, Any], checkpoint_file_suffix: str = "", data_format: PersistedJobDataFormat = PersistedJobDataFormat.PLAINTEXT, ) -> None: @@ -35,7 +38,7 @@ def save_job_checkpoint( Args: - checkpoint_data (Dict[str, Any]): Dict that specifies the checkpoint data to be persisted. + checkpoint_data (dict[str, Any]): Dict that specifies the checkpoint data to be persisted. checkpoint_file_suffix (str): str that specifies the file suffix to be used for the checkpoint filename. The resulting filename `f"{job_name}(_{checkpoint_file_suffix}).json"` is used to save the checkpoints. @@ -49,8 +52,8 @@ def save_job_checkpoint( """ if not checkpoint_data: raise ValueError("The checkpoint_data argument cannot be empty.") - checkpoint_directory = os.environ["AMZN_BRAKET_CHECKPOINT_DIR"] - job_name = os.environ["AMZN_BRAKET_JOB_NAME"] + checkpoint_directory = get_checkpoint_dir() + job_name = get_job_name() checkpoint_file_path = ( f"{checkpoint_directory}/{job_name}_{checkpoint_file_suffix}.json" if checkpoint_file_suffix @@ -62,27 +65,31 @@ def save_job_checkpoint( f.write(persisted_data.json()) -def load_job_checkpoint(job_name: str, checkpoint_file_suffix: str = "") -> Dict[str, Any]: +def load_job_checkpoint( + job_name: str | None = None, checkpoint_file_suffix: str = "" +) -> dict[str, Any]: """ - Loads the hybrid job checkpoint data stored for the job named 'job_name', with the checkpoint - file that ends with the `checkpoint_file_suffix`. The `job_name` can refer to any hybrid job - whose checkpoint data you expect to be available in the file path specified by the - `CHECKPOINT_DIR` container environment variable. + Loads the job checkpoint data stored for the job named 'job_name', with the checkpoint + file that ends with the `checkpoint_file_suffix`. The `job_name` can refer to any job whose + checkpoint data you expect to be available in the file path specified by the `CHECKPOINT_DIR` + container environment variable. If not provided, this function will use the currently running + job's name. Note: This function for loading hybrid job checkpoints is only for use inside the job container as it writes data to directories and references env variables set in the containers. Args: - job_name (str): str that specifies the name of the hybrid job whose checkpoints - are to be loaded. + job_name (str | None): str that specifies the name of the job whose checkpoints + are to be loaded. Default: current job name. + checkpoint_file_suffix (str): str specifying the file suffix that is used to locate the checkpoint file to load. The resulting file name `f"{job_name}(_{checkpoint_file_suffix}).json"` is used to locate the checkpoint file. Default: "" Returns: - Dict[str, Any]: Dict that contains the checkpoint data persisted in the checkpoint file. + dict[str, Any]: Dict that contains the checkpoint data persisted in the checkpoint file. Raises: FileNotFoundError: If the file `f"{job_name}(_{checkpoint_file_suffix})"` could not be found @@ -90,7 +97,8 @@ def load_job_checkpoint(job_name: str, checkpoint_file_suffix: str = "") -> Dict ValueError: If the data stored in the checkpoint file can't be deserialized (possibly due to corruption). """ - checkpoint_directory = os.environ["AMZN_BRAKET_CHECKPOINT_DIR"] + job_name = job_name or get_job_name() + checkpoint_directory = get_checkpoint_dir() checkpoint_file_path = ( f"{checkpoint_directory}/{job_name}_{checkpoint_file_suffix}.json" if checkpoint_file_suffix @@ -104,33 +112,82 @@ def load_job_checkpoint(job_name: str, checkpoint_file_suffix: str = "") -> Dict return deserialized_data +def _load_persisted_data(filename: str | Path | None = None) -> PersistedJobData: + filename = filename or Path(get_results_dir()) / "results.json" + try: + with open(filename, mode="r") as f: + return PersistedJobData.parse_raw(f.read()) + except FileNotFoundError: + return PersistedJobData( + dataDictionary={}, + dataFormat=PersistedJobDataFormat.PLAINTEXT, + ) + + +def load_job_result(filename: str | Path | None = None) -> dict[str, Any]: + """ + Loads job result of currently running job. + + Args: + filename (str | Path | None): Location of job results. Default `results.json` in job + results directory in a job instance or in working directory locally. This file + must be in the format used by `save_job_result`. + + Returns: + dict[str, Any]: Job result data of current job + """ + persisted_data = _load_persisted_data(filename) + deserialized_data = deserialize_values(persisted_data.dataDictionary, persisted_data.dataFormat) + return deserialized_data + + def save_job_result( - result_data: Dict[str, Any], - data_format: PersistedJobDataFormat = PersistedJobDataFormat.PLAINTEXT, + result_data: dict[str, Any] | Any, + data_format: PersistedJobDataFormat | None = None, ) -> None: """ Saves the `result_data` to the local output directory that is specified by the container environment variable `AMZN_BRAKET_JOB_RESULTS_DIR`, with the filename 'results.json'. The `result_data` values are serialized to the specified `data_format`. - Note: This function for storing the results is only for use inside the hybrid job container + Note: This function for storing the results is only for use inside the job container as it writes data to directories and references env variables set in the containers. - Args: - result_data (Dict[str, Any]): Dict that specifies the result data to be persisted. - data_format (PersistedJobDataFormat): The data format used to serialize the + result_data (dict[str, Any] | Any): Dict that specifies the result data to be + persisted. If result data is not a dict, then it will be wrapped as + `{"result": result_data}`. + data_format (PersistedJobDataFormat | None): The data format used to serialize the values. Note that for `PICKLED` data formats, the values are base64 encoded after serialization. Default: PersistedJobDataFormat.PLAINTEXT. - - Raises: - ValueError: If the supplied `result_data` is `None` or empty. """ - if not result_data: - raise ValueError("The result_data argument cannot be empty.") - result_directory = os.environ["AMZN_BRAKET_JOB_RESULTS_DIR"] - result_path = f"{result_directory}/results.json" - with open(result_path, "w") as f: - serialized_data = serialize_values(result_data or {}, data_format) + if not isinstance(result_data, dict): + result_data = {"result": result_data} + + current_persisted_data = _load_persisted_data() + + if current_persisted_data.dataFormat == PersistedJobDataFormat.PICKLED_V4: + # if results are already pickled, maintain pickled format + # if user explicitly specifies plaintext, raise error + if data_format == PersistedJobDataFormat.PLAINTEXT: + raise TypeError( + "Cannot update results object serialized with " + f"{current_persisted_data.dataFormat.value} using data format " + f"{data_format.value}." + ) + + data_format = PersistedJobDataFormat.PICKLED_V4 + + # if not specified or already pickled, default to plaintext + data_format = data_format or PersistedJobDataFormat.PLAINTEXT + + current_results = deserialize_values( + current_persisted_data.dataDictionary, + current_persisted_data.dataFormat, + ) + updated_results = current_results | result_data + + with open(Path(get_results_dir()) / "results.json", "w") as f: + serialized_data = serialize_values(updated_results or {}, data_format) persisted_data = PersistedJobData(dataDictionary=serialized_data, dataFormat=data_format) f.write(persisted_data.json()) diff --git a/src/braket/jobs/environment_variables.py b/src/braket/jobs/environment_variables.py new file mode 100644 index 000000000..4fba9315c --- /dev/null +++ b/src/braket/jobs/environment_variables.py @@ -0,0 +1,85 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"). You +# may not use this file except in compliance with the License. A copy of +# the License is located at +# +# http://aws.amazon.com/apache2.0/ +# +# or in the "license" file accompanying this file. This file is +# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF +# ANY KIND, either express or implied. See the License for the specific +# language governing permissions and limitations under the License. + +import json +import os + + +def get_job_name() -> str: + """ + Get the name of the current job. + + Returns: + str: The name of the job if in a job, else an empty string. + """ + return os.getenv("AMZN_BRAKET_JOB_NAME", "") + + +def get_job_device_arn() -> str: + """ + Get the device ARN of the current job. If not in a job, default to "local:none/none". + + Returns: + str: The device ARN of the current job or "local:none/none". + """ + return os.getenv("AMZN_BRAKET_DEVICE_ARN", "local:none/none") + + +def get_input_data_dir(channel: str = "input") -> str: + """ + Get the job input data directory. + + Args: + channel (str): The name of the input channel. Default value + corresponds to the default input channel name, `input`. + + Returns: + str: The input directory, defaulting to current working directory. + """ + input_dir = os.getenv("AMZN_BRAKET_INPUT_DIR", ".") + if input_dir != ".": + return f"{input_dir}/{channel}" + return input_dir + + +def get_results_dir() -> str: + """ + Get the job result directory. + + Returns: + str: The results directory, defaulting to current working directory. + """ + return os.getenv("AMZN_BRAKET_JOB_RESULTS_DIR", ".") + + +def get_checkpoint_dir() -> str: + """ + Get the job checkpoint directory. + + Returns: + str: The checkpoint directory, defaulting to current working directory. + """ + return os.getenv("AMZN_BRAKET_CHECKPOINT_DIR", ".") + + +def get_hyperparameters() -> dict[str, str]: + """ + Get the job hyperparameters as a dict, with the values stringified. + + Returns: + dict[str, str]: The hyperparameters of the job. + """ + if "AMZN_BRAKET_HP_FILE" in os.environ: + with open(os.getenv("AMZN_BRAKET_HP_FILE"), "r") as f: + return json.load(f) + return {} diff --git a/src/braket/jobs/hybrid_job.py b/src/braket/jobs/hybrid_job.py new file mode 100644 index 000000000..ae17c2715 --- /dev/null +++ b/src/braket/jobs/hybrid_job.py @@ -0,0 +1,415 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"). You +# may not use this file except in compliance with the License. A copy of +# the License is located at +# +# http://aws.amazon.com/apache2.0/ +# +# or in the "license" file accompanying this file. This file is +# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF +# ANY KIND, either express or implied. See the License for the specific +# language governing permissions and limitations under the License. + +from __future__ import annotations + +import functools +import importlib.util +import inspect +import re +import shutil +import sys +import tempfile +import warnings +from collections.abc import Callable, Iterable +from logging import Logger, getLogger +from pathlib import Path +from types import ModuleType +from typing import Any + +import cloudpickle + +from braket.aws.aws_session import AwsSession +from braket.jobs._entry_point_template import run_entry_point, symlink_input_data +from braket.jobs.config import ( + CheckpointConfig, + InstanceConfig, + OutputDataConfig, + S3DataSourceConfig, + StoppingCondition, +) +from braket.jobs.image_uris import Framework, built_in_images, retrieve_image +from braket.jobs.quantum_job import QuantumJob +from braket.jobs.quantum_job_creation import _generate_default_job_name + + +def hybrid_job( + *, + device: str | None, + include_modules: str | ModuleType | Iterable[str | ModuleType] | None = None, + dependencies: str | Path | list[str] | None = None, + local: bool = False, + job_name: str | None = None, + image_uri: str | None = None, + input_data: str | dict | S3DataSourceConfig | None = None, + wait_until_complete: bool = False, + instance_config: InstanceConfig | None = None, + distribution: str | None = None, + copy_checkpoints_from_job: str | None = None, + checkpoint_config: CheckpointConfig | None = None, + role_arn: str | None = None, + stopping_condition: StoppingCondition | None = None, + output_data_config: OutputDataConfig | None = None, + aws_session: AwsSession | None = None, + tags: dict[str, str] | None = None, + logger: Logger = getLogger(__name__), +) -> Callable: + """Defines a hybrid job by decorating the entry point function. The job will be created + when the decorated function is called. + + The job created will be a `LocalQuantumJob` when `local` is set to `True`, otherwise an + `AwsQuantumJob`. The following parameters will be ignored when running a job with + `local` set to `True`: `wait_until_complete`, `instance_config`, `distribution`, + `copy_checkpoints_from_job`, `stopping_condition`, `tags`, and `logger`. + + Args: + device (str | None): Device ARN of the QPU device that receives priority quantum + task queueing once the hybrid job begins running. Each QPU has a separate hybrid jobs + queue so that only one hybrid job is running at a time. The device string is accessible + in the hybrid job instance as the environment variable "AMZN_BRAKET_DEVICE_ARN". + When using embedded simulators, you may provide the device argument as string of the + form: "local:/" or `None`. + + include_modules (str | ModuleType | Iterable[str | ModuleType] | None): Either a + single module or module name or a list of module or module names referring to local + modules to be included. Any references to members of these modules in the hybrid job + algorithm code will be serialized as part of the algorithm code. Default: `[]` + + dependencies (str | Path | list[str] | None): Path (absolute or relative) to a + requirements.txt file, or alternatively a list of strings, with each string being a + `requirement specifier `_, to be used for the hybrid job. + + local (bool): Whether to use local mode for the hybrid job. Default: `False` + + job_name (str | None): A string that specifies the name with which the job is created. + Allowed pattern for job name: `^[a-zA-Z0-9](-*[a-zA-Z0-9]){0,50}$`. Defaults to + f'{decorated-function-name}-{timestamp}'. + + image_uri (str | None): A str that specifies the ECR image to use for executing the job. + `retrieve_image()` function may be used for retrieving the ECR image URIs + for the containers supported by Braket. Default: ``. + + input_data (str | dict | S3DataSourceConfig | None): Information about the training + data. Dictionary maps channel names to local paths or S3 URIs. Contents found + at any local paths will be uploaded to S3 at + f's3://{default_bucket_name}/jobs/{job_name}/data/{channel_name}'. If a local + path, S3 URI, or S3DataSourceConfig is provided, it will be given a default + channel name "input". + Default: {}. + + wait_until_complete (bool): `True` if we should wait until the job completes. + This would tail the job logs as it waits. Otherwise `False`. Ignored if using + local mode. Default: `False`. + + instance_config (InstanceConfig | None): Configuration of the instance(s) for running the + classical code for the hybrid job. Default: + `InstanceConfig(instanceType='ml.m5.large', instanceCount=1, volumeSizeInGB=30)`. + + distribution (str | None): A str that specifies how the job should be distributed. + If set to "data_parallel", the hyperparameters for the job will be set to use data + parallelism features for PyTorch or TensorFlow. Default: `None`. + + copy_checkpoints_from_job (str | None): A str that specifies the job ARN whose + checkpoint you want to use in the current job. Specifying this value will copy + over the checkpoint data from `use_checkpoints_from_job`'s checkpoint_config + s3Uri to the current job's checkpoint_config s3Uri, making it available at + checkpoint_config.localPath during the job execution. Default: `None` + + checkpoint_config (CheckpointConfig | None): Configuration that specifies the + location where checkpoint data is stored. + Default: `CheckpointConfig(localPath='/opt/jobs/checkpoints', + s3Uri=f's3://{default_bucket_name}/jobs/{job_name}/checkpoints')`. + + role_arn (str | None): A str providing the IAM role ARN used to execute the + script. Default: IAM role returned by AwsSession's `get_default_jobs_role()`. + + stopping_condition (StoppingCondition | None): The maximum length of time, in seconds, + and the maximum number of tasks that a job can run before being forcefully stopped. + Default: StoppingCondition(maxRuntimeInSeconds=5 * 24 * 60 * 60). + + output_data_config (OutputDataConfig | None): Specifies the location for the output of + the job. + Default: `OutputDataConfig(s3Path=f's3://{default_bucket_name}/jobs/{job_name}/data', + kmsKeyId=None)`. + + aws_session (AwsSession | None): AwsSession for connecting to AWS Services. + Default: AwsSession() + + tags (dict[str, str] | None): Dict specifying the key-value pairs for tagging this job. + Default: {}. + + logger (Logger): Logger object with which to write logs, such as task statuses + while waiting for task to be in a terminal state. Default: `getLogger(__name__)` + + Returns: + Callable: the callable for creating a Hybrid Job. + """ + _validate_python_version(image_uri, aws_session) + + def _hybrid_job(entry_point: Callable) -> Callable: + @functools.wraps(entry_point) + def job_wrapper(*args, **kwargs) -> Callable: + """ + The job wrapper. + Returns: + Callable: the callable for creating a Hybrid Job. + """ + with _IncludeModules(include_modules), tempfile.TemporaryDirectory( + dir="", prefix="decorator_job_" + ) as temp_dir: + temp_dir_path = Path(temp_dir) + entry_point_file_path = Path("entry_point.py") + with open(temp_dir_path / entry_point_file_path, "w") as entry_point_file: + template = "\n".join( + [ + _process_input_data(input_data), + _serialize_entry_point(entry_point, args, kwargs), + ] + ) + entry_point_file.write(template) + + if dependencies: + _process_dependencies(dependencies, temp_dir_path) + + job_args = { + "device": device or "local:none/none", + "source_module": temp_dir, + "entry_point": ( + f"{temp_dir}.{entry_point_file_path.stem}:{entry_point.__name__}" + ), + "wait_until_complete": wait_until_complete, + "job_name": job_name or _generate_default_job_name(func=entry_point), + "hyperparameters": _log_hyperparameters(entry_point, args, kwargs), + "logger": logger, + } + optional_args = { + "image_uri": image_uri, + "input_data": input_data, + "instance_config": instance_config, + "distribution": distribution, + "checkpoint_config": checkpoint_config, + "copy_checkpoints_from_job": copy_checkpoints_from_job, + "role_arn": role_arn, + "stopping_condition": stopping_condition, + "output_data_config": output_data_config, + "aws_session": aws_session, + "tags": tags, + } + for key, value in optional_args.items(): + if value is not None: + job_args[key] = value + + job = _create_job(job_args, local) + return job + + return job_wrapper + + return _hybrid_job + + +def _validate_python_version(image_uri: str | None, aws_session: AwsSession | None = None) -> None: + """Validate python version at job definition time""" + aws_session = aws_session or AwsSession() + # user provides a custom image_uri + if image_uri and image_uri not in built_in_images(aws_session.region): + print( + "Skipping python version validation, make sure versions match " + "between local environment and container." + ) + else: + # set default image_uri to base + image_uri = image_uri or retrieve_image(Framework.BASE, aws_session.region) + tag = aws_session.get_full_image_tag(image_uri) + major_version, minor_version = re.search(r"-py(\d)(\d+)-", tag).groups() + if not (sys.version_info.major, sys.version_info.minor) == ( + int(major_version), + int(minor_version), + ): + raise RuntimeError( + "Python version must match between local environment and container. " + f"Client is running Python {sys.version_info.major}.{sys.version_info.minor} " + f"locally, but container uses Python {major_version}.{minor_version}." + ) + + +def _process_dependencies(dependencies: str | Path | list[str], temp_dir: Path) -> None: + if isinstance(dependencies, (str, Path)): + # requirements file + shutil.copy(Path(dependencies).resolve(), temp_dir / "requirements.txt") + else: + # list of packages + with open(temp_dir / "requirements.txt", "w") as f: + f.write("\n".join(dependencies)) + + +class _IncludeModules: + def __init__(self, modules: str | ModuleType | Iterable[str | ModuleType] = None): + modules = modules or [] + if isinstance(modules, (str, ModuleType)): + modules = [modules] + self._modules = [ + (importlib.import_module(module) if isinstance(module, str) else module) + for module in modules + ] + + def __enter__(self): + """Register included modules with cloudpickle to be pickled by value""" + for module in self._modules: + cloudpickle.register_pickle_by_value(module) + + def __exit__(self, exc_type, exc_val, exc_tb): + """Unregister included modules with cloudpickle to be pickled by value""" + for module in self._modules: + cloudpickle.unregister_pickle_by_value(module) + + +def _serialize_entry_point(entry_point: Callable, args: tuple, kwargs: dict) -> str: + """Create an entry point from a function""" + wrapped_entry_point = functools.partial(entry_point, *args, **kwargs) + + try: + serialized = cloudpickle.dumps(wrapped_entry_point) + except Exception as e: + raise RuntimeError( + "Serialization failed for decorator hybrid job. If you are referencing " + "an object from outside the function scope, either directly or through " + "function parameters, try instantiating the object inside the decorated " + "function instead." + ) from e + + return run_entry_point.format( + serialized=serialized, + function_name=entry_point.__name__, + ) + + +def _log_hyperparameters(entry_point: Callable, args: tuple, kwargs: dict) -> dict: + """Capture function arguments as hyperparameters""" + signature = inspect.signature(entry_point) + bound_args = signature.bind(*args, **kwargs) + bound_args.apply_defaults() + hyperparameters = {} + for param, value in bound_args.arguments.items(): + param_kind = signature.parameters[param].kind + if param_kind in [ + inspect.Parameter.POSITIONAL_OR_KEYWORD, + inspect.Parameter.KEYWORD_ONLY, + ]: + hyperparameters[param] = value + elif param_kind == inspect.Parameter.VAR_KEYWORD: + hyperparameters.update(**value) + else: + warnings.warn( + "Positional only arguments will not be logged to the hyperparameters file." + ) + return {name: _sanitize(value) for name, value in hyperparameters.items()} + + +def _sanitize(hyperparameter: Any) -> str: + """Sanitize forbidden characters from hp strings""" + string_hp = str(hyperparameter) + + sanitized = ( + string_hp + # replace forbidden characters with close matches + .replace("\n", " ") + .replace("$", "?") + .replace("(", "{") + .replace("&", "+") + .replace("`", "'") + # not technically forbidden, but to avoid mismatched parens + .replace(")", "}") + ) + + # max allowed length for a hyperparameter is 2500 + if len(sanitized) > 2500: + # show as much as possible, including the final 20 characters + return f"{sanitized[:2500 - 23]}...{sanitized[-20:]}" + return sanitized + + +def _process_input_data(input_data: dict) -> list[str]: + """ + Create symlinks to data + + Logic chart for how the service moves files into the data directory on the instance: + input data matches exactly one file: cwd/filename -> channel/filename + input data matches exactly one directory: cwd/dirname/* -> channel/* + else (multiple matches, possibly including exact): + cwd/prefix_match -> channel/prefix_match, for each match + """ + input_data = input_data or {} + if not isinstance(input_data, dict): + input_data = {"input": input_data} + + def matches(prefix: str) -> list[str]: + return [ + str(path) for path in Path(prefix).parent.iterdir() if str(path).startswith(str(prefix)) + ] + + def is_prefix(path: str) -> bool: + return len(matches(path)) > 1 or not Path(path).exists() + + prefix_channels = set() + directory_channels = set() + file_channels = set() + + for channel, data in input_data.items(): + if AwsSession.is_s3_uri(str(data)) or isinstance(data, S3DataSourceConfig): + channel_arg = f'channel="{channel}"' if channel != "input" else "" + print( + "Input data channels mapped to an S3 source will not be available in " + f"the working directory. Use `get_input_data_dir({channel_arg})` to read " + f"input data from S3 source inside the job container." + ) + elif is_prefix(data): + prefix_channels.add(channel) + elif Path(data).is_dir(): + directory_channels.add(channel) + else: + file_channels.add(channel) + + return symlink_input_data.format( + prefix_matches={channel: matches(input_data[channel]) for channel in prefix_channels}, + input_data_items=[ + (channel, data) + for channel, data in input_data.items() + if channel in prefix_channels | directory_channels | file_channels + ], + prefix_channels=prefix_channels, + directory_channels=directory_channels, + ) + + +def _create_job(job_args: dict[str, Any], local: bool = False) -> QuantumJob: + """Create an AWS or Local hybrid job""" + if local: + from braket.jobs.local import LocalQuantumJob + + for aws_only_arg in [ + "wait_until_complete", + "copy_checkpoints_from_job", + "instance_config", + "distribution", + "stopping_condition", + "tags", + "logger", + ]: + if aws_only_arg in job_args: + del job_args[aws_only_arg] + return LocalQuantumJob.create(**job_args) + else: + from braket.aws import AwsQuantumJob + + return AwsQuantumJob.create(**job_args) diff --git a/src/braket/jobs/image_uris.py b/src/braket/jobs/image_uris.py index 0a29f2ce2..3a3346abe 100644 --- a/src/braket/jobs/image_uris.py +++ b/src/braket/jobs/image_uris.py @@ -14,7 +14,8 @@ import json import os from enum import Enum -from typing import Dict +from functools import cache +from typing import Dict, Set class Framework(str, Enum): @@ -25,6 +26,11 @@ class Framework(str, Enum): PL_PYTORCH = "PL_PYTORCH" +def built_in_images(region: str) -> Set[str]: + return {retrieve_image(framework, region) for framework in Framework} + + +@cache def retrieve_image(framework: Framework, region: str) -> str: """Retrieves the ECR URI for the Docker image matching the specified arguments. diff --git a/src/braket/jobs/local/local_job.py b/src/braket/jobs/local/local_job.py index a6fc8d279..f516d9693 100644 --- a/src/braket/jobs/local/local_job.py +++ b/src/braket/jobs/local/local_job.py @@ -15,7 +15,7 @@ import os import time -from typing import Any, Dict, List, Union +from typing import Any from braket.aws.aws_session import AwsSession from braket.jobs.config import CheckpointConfig, OutputDataConfig, S3DataSourceConfig @@ -38,58 +38,61 @@ def create( cls, device: str, source_module: str, - entry_point: str = None, - image_uri: str = None, - job_name: str = None, - code_location: str = None, - role_arn: str = None, - hyperparameters: Dict[str, Any] = None, - input_data: Union[str, Dict, S3DataSourceConfig] = None, - output_data_config: OutputDataConfig = None, - checkpoint_config: CheckpointConfig = None, - aws_session: AwsSession = None, + entry_point: str | None = None, + image_uri: str | None = None, + job_name: str | None = None, + code_location: str | None = None, + role_arn: str | None = None, + hyperparameters: dict[str, Any] | None = None, + input_data: str | dict | S3DataSourceConfig | None = None, + output_data_config: OutputDataConfig | None = None, + checkpoint_config: CheckpointConfig | None = None, + aws_session: AwsSession | None = None, local_container_update: bool = True, ) -> LocalQuantumJob: """Creates and runs hybrid job by setting up and running the customer script in a local docker container. Args: - device (str): ARN for the AWS device which is primarily accessed for the execution - of this hybrid job. Alternatively, a string of the format - "local:/" for using a local simulator for the hybrid job. This - string will be available as the environment variable `AMZN_BRAKET_DEVICE_ARN` inside - the hybrid job container when using a Braket container. + device (str): Device ARN of the QPU device that receives priority quantum + task queueing once the hybrid job begins running. Each QPU has a separate hybrid + jobs queue so that only one hybrid job is running at a time. The device string is + accessible in the hybrid job instance as the environment variable + "AMZN_BRAKET_DEVICE_ARN". When using embedded simulators, you may provide the device + argument as a string of the form: "local:/". source_module (str): Path (absolute, relative or an S3 URI) to a python module to be tarred and uploaded. If `source_module` is an S3 URI, it must point to a tar.gz file. Otherwise, source_module may be a file or directory. - entry_point (str): A str that specifies the entry point of the hybrid job, relative to - the source module. The entry point must be in the format + entry_point (str | None): A str that specifies the entry point of the hybrid job, + relative to the source module. The entry point must be in the format `importable.module` or `importable.module:callable`. For example, `source_module.submodule:start_here` indicates the `start_here` function contained in `source_module.submodule`. If source_module is an S3 URI, entry point must be given. Default: source_module's name - image_uri (str): A str that specifies the ECR image to use for executing the hybrid job. - `image_uris.retrieve_image()` function may be used for retrieving the ECR image URIs - for the containers supported by Braket. Default = ``. + image_uri (str | None): A str that specifies the ECR image to use for executing the + hybrid job. `image_uris.retrieve_image()` function may be used for retrieving the + ECR image URIs for the containers supported by Braket. + Default = ``. - job_name (str): A str that specifies the name with which the hybrid job is created. + job_name (str | None): A str that specifies the name with which the hybrid job is + created. Default: f'{image_uri_type}-{timestamp}'. - code_location (str): The S3 prefix URI where custom code will be uploaded. + code_location (str | None): The S3 prefix URI where custom code will be uploaded. Default: f's3://{default_bucket_name}/jobs/{job_name}/script'. - role_arn (str): This field is currently not used for local hybrid jobs. Local hybrid - jobs will use the current role's credentials. This may be subject to change. + role_arn (str | None): This field is currently not used for local hybrid jobs. Local + hybrid jobs will use the current role's credentials. This may be subject to change. - hyperparameters (Dict[str, Any]): Hyperparameters accessible to the hybrid job. + hyperparameters (dict[str, Any] | None): Hyperparameters accessible to the hybrid job. The hyperparameters are made accessible as a Dict[str, str] to the hybrid job. For convenience, this accepts other types for keys and values, but `str()` is called to convert them before being passed on. Default: None. - input_data (Union[str, Dict, S3DataSourceConfig]): Information about the training + input_data (str | dict | S3DataSourceConfig | None): Information about the training data. Dictionary maps channel names to local paths or S3 URIs. Contents found at any local paths will be uploaded to S3 at f's3://{default_bucket_name}/jobs/{job_name}/data/{channel_name}. If a local @@ -97,17 +100,17 @@ def create( channel name "input". Default: {}. - output_data_config (OutputDataConfig): Specifies the location for the output of the - hybrid job. + output_data_config (OutputDataConfig | None): Specifies the location for the output of + the hybrid job. Default: OutputDataConfig(s3Path=f's3://{default_bucket_name}/jobs/{job_name}/data', kmsKeyId=None). - checkpoint_config (CheckpointConfig): Configuration that specifies the location where - checkpoint data is stored. + checkpoint_config (CheckpointConfig | None): Configuration that specifies the location + where checkpoint data is stored. Default: CheckpointConfig(localPath='/opt/jobs/checkpoints', s3Uri=f's3://{default_bucket_name}/jobs/{job_name}/checkpoints'). - aws_session (AwsSession): AwsSession for connecting to AWS Services. + aws_session (AwsSession | None): AwsSession for connecting to AWS Services. Default: AwsSession() local_container_update (bool): Perform an update, if available, from ECR to the local @@ -162,11 +165,12 @@ def create( run_log = container.run_log return LocalQuantumJob(f"local:job/{job_name}", run_log) - def __init__(self, arn: str, run_log: str = None): + def __init__(self, arn: str, run_log: str | None = None): """ Args: arn (str): The ARN of the hybrid job. - run_log (str): The container output log of running the hybrid job with the given arn. + run_log (str | None): The container output log of running the hybrid job with the + given arn. """ if not arn.startswith("local:job/"): raise ValueError(f"Arn {arn} is not a valid local job arn") @@ -213,7 +217,7 @@ def state(self, use_cached_value: bool = False) -> str: """ return "COMPLETED" - def metadata(self, use_cached_value: bool = False) -> Dict[str, Any]: + def metadata(self, use_cached_value: bool = False) -> dict[str, Any]: """When running the hybrid job in local mode, the metadata is not available. Args: use_cached_value (bool): If `True`, uses the value most recently retrieved @@ -221,7 +225,7 @@ def metadata(self, use_cached_value: bool = False) -> Dict[str, Any]: `GetJob` is called to retrieve the metadata. If `False`, always calls `GetJob`, which also updates the cached value. Default: `False`. Returns: - Dict[str, Any]: None + dict[str, Any]: None """ pass @@ -234,14 +238,14 @@ def cancel(self) -> str: def download_result( self, - extract_to: str = None, + extract_to: str | None = None, poll_timeout_seconds: float = QuantumJob.DEFAULT_RESULTS_POLL_TIMEOUT, poll_interval_seconds: float = QuantumJob.DEFAULT_RESULTS_POLL_INTERVAL, ) -> None: """When running the hybrid job in local mode, results are automatically stored locally. Args: - extract_to (str): The directory to which the results are extracted. The results + extract_to (str | None): The directory to which the results are extracted. The results are extracted to a folder titled with the hybrid job name within this directory. Default= `Current working directory`. poll_timeout_seconds (float): The polling timeout, in seconds, for `result()`. @@ -255,7 +259,7 @@ def result( self, poll_timeout_seconds: float = QuantumJob.DEFAULT_RESULTS_POLL_TIMEOUT, poll_interval_seconds: float = QuantumJob.DEFAULT_RESULTS_POLL_INTERVAL, - ) -> Dict[str, Any]: + ) -> dict[str, Any]: """Retrieves the hybrid job result persisted using save_job_result() function. Args: @@ -265,7 +269,7 @@ def result( Default: 5 seconds. Returns: - Dict[str, Any]: Dict specifying the hybrid job results. + dict[str, Any]: Dict specifying the hybrid job results. """ try: with open(os.path.join(self.name, "results.json"), "r") as f: @@ -281,7 +285,7 @@ def metrics( self, metric_type: MetricType = MetricType.TIMESTAMP, statistic: MetricStatistic = MetricStatistic.MAX, - ) -> Dict[str, List[Any]]: + ) -> dict[str, list[Any]]: """Gets all the metrics data, where the keys are the column names, and the values are a list containing the values in each row. @@ -299,7 +303,7 @@ def metrics( values may be integers, floats, strings or None. Returns: - Dict[str, List[Any]]: The metrics data. + dict[str, list[Any]]: The metrics data. """ parser = LogMetricsParser() current_time = str(time.time()) diff --git a/src/braket/jobs/local/local_job_container.py b/src/braket/jobs/local/local_job_container.py index f924db47f..ea5625623 100644 --- a/src/braket/jobs/local/local_job_container.py +++ b/src/braket/jobs/local/local_job_container.py @@ -10,6 +10,8 @@ # distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF # ANY KIND, either express or implied. See the License for the specific # language governing permissions and limitations under the License. +from __future__ import annotations + import base64 import re import subprocess @@ -29,7 +31,7 @@ class _LocalJobContainer(object): def __init__( self, image_uri: str, - aws_session: AwsSession = None, + aws_session: AwsSession | None = None, logger: Logger = getLogger(__name__), force_update: bool = False, ): @@ -39,7 +41,7 @@ def __init__( The function "end_session" must be called when the container is no longer needed. Args: image_uri (str): The URI of the container image to run. - aws_session (AwsSession): AwsSession for connecting to AWS Services. + aws_session (AwsSession | None): AwsSession for connecting to AWS Services. Default: AwsSession() logger (Logger): Logger object with which to write logs. Default: `getLogger(__name__)` diff --git a/src/braket/jobs/metrics_data/cwl_insights_metrics_fetcher.py b/src/braket/jobs/metrics_data/cwl_insights_metrics_fetcher.py index 94ed5499d..b32cd6b9c 100644 --- a/src/braket/jobs/metrics_data/cwl_insights_metrics_fetcher.py +++ b/src/braket/jobs/metrics_data/cwl_insights_metrics_fetcher.py @@ -11,6 +11,8 @@ # ANY KIND, either express or implied. See the License for the specific # language governing permissions and limitations under the License. +from __future__ import annotations + import time from logging import Logger, getLogger from typing import Any, Dict, List, Optional, Union @@ -133,8 +135,8 @@ def get_metrics_for_job( job_name: str, metric_type: MetricType = MetricType.TIMESTAMP, statistic: MetricStatistic = MetricStatistic.MAX, - job_start_time: int = None, - job_end_time: int = None, + job_start_time: int | None = None, + job_end_time: int | None = None, ) -> Dict[str, List[Union[str, float, int]]]: """ Synchronously retrieves all the algorithm metrics logged by a given Hybrid Job. @@ -145,10 +147,10 @@ def get_metrics_for_job( metric_type (MetricType): The type of metrics to get. Default is MetricType.TIMESTAMP. statistic (MetricStatistic): The statistic to determine which metric value to use when there is a conflict. Default is MetricStatistic.MAX. - job_start_time (int): The time when the hybrid job started. + job_start_time (int | None): The time when the hybrid job started. Default: 3 hours before job_end_time. - job_end_time (int): If the hybrid job is complete, this should be the time at which the - hybrid job finished. Default: current time. + job_end_time (int | None): If the hybrid job is complete, this should be the time at + which the hybrid job finished. Default: current time. Returns: Dict[str, List[Union[str, float, int]]] : The metrics data, where the keys diff --git a/src/braket/jobs/quantum_job.py b/src/braket/jobs/quantum_job.py index f022d28a3..a84118991 100644 --- a/src/braket/jobs/quantum_job.py +++ b/src/braket/jobs/quantum_job.py @@ -10,6 +10,8 @@ # distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF # ANY KIND, either express or implied. See the License for the specific # language governing permissions and limitations under the License. +from __future__ import annotations + from abc import ABC, abstractmethod from typing import Any, Dict, List @@ -170,7 +172,7 @@ def result( @abstractmethod def download_result( self, - extract_to: str = None, + extract_to: str | None = None, poll_timeout_seconds: float = DEFAULT_RESULTS_POLL_TIMEOUT, poll_interval_seconds: float = DEFAULT_RESULTS_POLL_INTERVAL, ) -> None: @@ -179,7 +181,7 @@ def download_result( the results are extracted to the current directory. Args: - extract_to (str): The directory to which the results are extracted. The results + extract_to (str | None): The directory to which the results are extracted. The results are extracted to a folder titled with the hybrid job name within this directory. Default= `Current working directory`. diff --git a/src/braket/jobs/quantum_job_creation.py b/src/braket/jobs/quantum_job_creation.py index 1dbc7dc4c..b935de7eb 100644 --- a/src/braket/jobs/quantum_job_creation.py +++ b/src/braket/jobs/quantum_job_creation.py @@ -19,12 +19,13 @@ import tarfile import tempfile import time +import warnings +from collections.abc import Callable from dataclasses import asdict from pathlib import Path -from typing import Any, Dict, List, Optional, Tuple, Union +from typing import Any from braket.aws.aws_session import AwsSession -from braket.jobs import Framework, image_uris from braket.jobs.config import ( CheckpointConfig, DeviceConfig, @@ -33,65 +34,70 @@ S3DataSourceConfig, StoppingCondition, ) +from braket.jobs.image_uris import Framework, retrieve_image def prepare_quantum_job( device: str, source_module: str, - entry_point: str = None, - image_uri: str = None, - job_name: str = None, - code_location: str = None, - role_arn: str = None, - hyperparameters: Dict[str, Any] = None, - input_data: Union[str, Dict, S3DataSourceConfig] = None, - instance_config: InstanceConfig = None, - distribution: str = None, - stopping_condition: StoppingCondition = None, - output_data_config: OutputDataConfig = None, - copy_checkpoints_from_job: str = None, - checkpoint_config: CheckpointConfig = None, - aws_session: AwsSession = None, - tags: Dict[str, str] = None, -) -> Dict: + entry_point: str | None = None, + image_uri: str | None = None, + job_name: str | None = None, + code_location: str | None = None, + role_arn: str | None = None, + hyperparameters: dict[str, Any] | None = None, + input_data: str | dict | S3DataSourceConfig | None = None, + instance_config: InstanceConfig | None = None, + distribution: str | None = None, + stopping_condition: StoppingCondition | None = None, + output_data_config: OutputDataConfig | None = None, + copy_checkpoints_from_job: str | None = None, + checkpoint_config: CheckpointConfig | None = None, + aws_session: AwsSession | None = None, + tags: dict[str, str] | None = None, +) -> dict: """Creates a hybrid job by invoking the Braket CreateJob API. Args: - device (str): ARN for the AWS device which is primarily - accessed for the execution of this hybrid job. + device (str): Device ARN of the QPU device that receives priority quantum + task queueing once the hybrid job begins running. Each QPU has a separate hybrid jobs + queue so that only one hybrid job is running at a time. The device string is accessible + in the hybrid job instance as the environment variable "AMZN_BRAKET_DEVICE_ARN". + When using embedded simulators, you may provide the device argument as string of the + form: "local:/". source_module (str): Path (absolute, relative or an S3 URI) to a python module to be tarred and uploaded. If `source_module` is an S3 URI, it must point to a tar.gz file. Otherwise, source_module may be a file or directory. - entry_point (str): A str that specifies the entry point of the hybrid job, relative to - the source module. The entry point must be in the format + entry_point (str | None): A str that specifies the entry point of the hybrid job, relative + to the source module. The entry point must be in the format `importable.module` or `importable.module:callable`. For example, `source_module.submodule:start_here` indicates the `start_here` function contained in `source_module.submodule`. If source_module is an S3 URI, entry point must be given. Default: source_module's name - image_uri (str): A str that specifies the ECR image to use for executing the hybrid job. - `image_uris.retrieve_image()` function may be used for retrieving the ECR image URIs + image_uri (str | None): A str that specifies the ECR image to use for executing the hybrid + job.`image_uris.retrieve_image()` function may be used for retrieving the ECR image URIs for the containers supported by Braket. Default = ``. - job_name (str): A str that specifies the name with which the hybrid job is created. The - hybrid job - name must be between 0 and 50 characters long and cannot contain underscores. + job_name (str | None): A str that specifies the name with which the hybrid job is created. + The hybrid job name must be between 0 and 50 characters long and cannot contain + underscores. Default: f'{image_uri_type}-{timestamp}'. - code_location (str): The S3 prefix URI where custom code will be uploaded. + code_location (str | None): The S3 prefix URI where custom code will be uploaded. Default: f's3://{default_bucket_name}/jobs/{job_name}/script'. - role_arn (str): A str providing the IAM role ARN used to execute the + role_arn (str | None): A str providing the IAM role ARN used to execute the script. Default: IAM role returned by AwsSession's `get_default_jobs_role()`. - hyperparameters (Dict[str, Any]): Hyperparameters accessible to the hybrid job. + hyperparameters (dict[str, Any] | None): Hyperparameters accessible to the hybrid job. The hyperparameters are made accessible as a Dict[str, str] to the hybrid job. For convenience, this accepts other types for keys and values, but `str()` is called to convert them before being passed on. Default: None. - input_data (Union[str, Dict, S3DataSourceConfig]): Information about the training + input_data (str | dict | S3DataSourceConfig | None): Information about the training data. Dictionary maps channel names to local paths or S3 URIs. Contents found at any local paths will be uploaded to S3 at f's3://{default_bucket_name}/jobs/{job_name}/data/{channel_name}. If a local @@ -99,42 +105,43 @@ def prepare_quantum_job( channel name "input". Default: {}. - instance_config (InstanceConfig): Configuration of the instances to be used - to execute the hybrid job. Default: InstanceConfig(instanceType='ml.m5.large', - instanceCount=1, volumeSizeInGB=30, volumeKmsKey=None). + instance_config (InstanceConfig | None): Configuration of the instance(s) for running the + classical code for the hybrid job. Defaults to + `InstanceConfig(instanceType='ml.m5.large', instanceCount=1, volumeSizeInGB=30)`. - distribution (str): A str that specifies how the hybrid job should be distributed. If set to - "data_parallel", the hyperparameters for the hybrid job will be set to use data - parallelism features for PyTorch or TensorFlow. Default: None. + distribution (str | None): A str that specifies how the hybrid job should be distributed. + If set to "data_parallel", the hyperparameters for the hybrid job will be set to use + data parallelism features for PyTorch or TensorFlow. Default: None. - stopping_condition (StoppingCondition): The maximum length of time, in seconds, + stopping_condition (StoppingCondition | None): The maximum length of time, in seconds, and the maximum number of quantum tasks that a hybrid job can run before being forcefully stopped. Default: StoppingCondition(maxRuntimeInSeconds=5 * 24 * 60 * 60). - output_data_config (OutputDataConfig): Specifies the location for the output of the hybrid - job. + output_data_config (OutputDataConfig | None): Specifies the location for the output of the + hybrid job. Default: OutputDataConfig(s3Path=f's3://{default_bucket_name}/jobs/{job_name}/data', kmsKeyId=None). - copy_checkpoints_from_job (str): A str that specifies the hybrid job ARN whose checkpoint - you want to use in the current hybrid job. Specifying this value will copy over the - checkpoint data from `use_checkpoints_from_job`'s checkpoint_config s3Uri to the current - hybrid job's checkpoint_config s3Uri, making it available at checkpoint_config.localPath - during the hybrid job execution. Default: None + copy_checkpoints_from_job (str | None): A str that specifies the hybrid job ARN whose + checkpoint you want to use in the current hybrid job. Specifying this value will copy + over the checkpoint data from `use_checkpoints_from_job`'s checkpoint_config s3Uri to + the current hybrid job's checkpoint_config s3Uri, making it available at + checkpoint_config.localPath during the hybrid job execution. Default: None - checkpoint_config (CheckpointConfig): Configuration that specifies the location where + checkpoint_config (CheckpointConfig | None): Configuration that specifies the location where checkpoint data is stored. Default: CheckpointConfig(localPath='/opt/jobs/checkpoints', s3Uri=f's3://{default_bucket_name}/jobs/{job_name}/checkpoints'). - aws_session (AwsSession): AwsSession for connecting to AWS Services. + aws_session (AwsSession | None): AwsSession for connecting to AWS Services. Default: AwsSession() - tags (Dict[str, str]): Dict specifying the key-value pairs for tagging this hybrid job. + tags (dict[str, str] | None): Dict specifying the key-value pairs for tagging this + hybrid job. Default: {}. Returns: - Dict: Hybrid job tracking the execution on Amazon Braket. + dict: Hybrid job tracking the execution on Amazon Braket. Raises: ValueError: Raises ValueError if the parameters are not valid. @@ -149,7 +156,7 @@ def prepare_quantum_job( _validate_params(param_datatype_map) aws_session = aws_session or AwsSession() device_config = DeviceConfig(device) - job_name = job_name or _generate_default_job_name(image_uri) + job_name = job_name or _generate_default_job_name(image_uri=image_uri) role_arn = role_arn or os.getenv("BRAKET_JOBS_ROLE_ARN", aws_session.get_default_jobs_role()) hyperparameters = hyperparameters or {} hyperparameters = {str(key): str(value) for key, value in hyperparameters.items()} @@ -181,7 +188,7 @@ def prepare_quantum_job( "compressionType": "GZIP", } } - image_uri = image_uri or image_uris.retrieve_image(Framework.BASE, aws_session.region) + image_uri = image_uri or retrieve_image(Framework.BASE, aws_session.region) algorithm_specification["containerImage"] = {"uri": image_uri} if not output_data_config.s3Path: output_data_config.s3Path = AwsSession.construct_s3_uri( @@ -226,24 +233,37 @@ def prepare_quantum_job( return create_job_kwargs -def _generate_default_job_name(image_uri: Optional[str]) -> str: +def _generate_default_job_name(image_uri: str | None = None, func: Callable | None = None) -> str: """ - Generate default hybrid job name using the image uri and a timestamp + Generate default job name using the image uri and entrypoint function. + Args: - image_uri (Optional[str]): URI for the image container. + image_uri (str | None): URI for the image container. + func (Callable | None): The entry point function. Returns: str: Hybrid job name. """ - if not image_uri: - job_type = "-default" + max_length = 50 + timestamp = str(int(time.time() * 1000)) + + if func: + name = func.__name__.replace("_", "-") + if len(name) + len(timestamp) > max_length: + name = name[: max_length - len(timestamp) - 1] + warnings.warn( + f"Job name exceeded {max_length} characters. Truncating name to {name}-{timestamp}." + ) else: - job_type_match = re.search("/amazon-braket-(.*)-jobs:", image_uri) or re.search( - "/amazon-braket-([^:/]*)", image_uri - ) - job_type = f"-{job_type_match.groups()[0]}" if job_type_match else "" - - return f"braket-job{job_type}-{time.time() * 1000:.0f}" + if not image_uri: + name = "braket-job-default" + else: + job_type_match = re.search("/amazon-braket-(.*)-jobs:", image_uri) or re.search( + "/amazon-braket-([^:/]*)", image_uri + ) + container = f"-{job_type_match.groups()[0]}" if job_type_match else "" + name = f"braket-job{container}" + return f"{name}-{timestamp}" def _process_s3_source_module( @@ -310,6 +330,7 @@ def _validate_entry_point(source_module_path: Path, entry_point: str) -> None: sys.path.append(str(source_module_path.parent)) try: # second argument allows relative imports + importlib.invalidate_caches() module = importlib.util.find_spec(importable, source_module_path.stem) assert module is not None # if entry point is nested (ie contains '.'), parent modules are imported @@ -337,12 +358,12 @@ def _tar_and_upload_to_code_location( aws_session.upload_to_s3(f"{temp_dir}/source.tar.gz", f"{code_location}/source.tar.gz") -def _validate_params(dict_arr: Dict[str, Tuple[any, any]]) -> None: +def _validate_params(dict_arr: dict[str, tuple[any, any]]) -> None: """ Validate that config parameters are of the right type. Args: - dict_arr (Dict[str, Tuple[any, any]]): dict mapping parameter names to + dict_arr (dict[str, tuple[any, any]]): dict mapping parameter names to a tuple containing the provided value and expected type. """ for parameter_name, value_tuple in dict_arr.items(): @@ -356,19 +377,19 @@ def _validate_params(dict_arr: Dict[str, Tuple[any, any]]) -> None: def _process_input_data( - input_data: Union[str, Dict, S3DataSourceConfig], job_name: str, aws_session: AwsSession -) -> List[Dict[str, Any]]: + input_data: str | dict | S3DataSourceConfig, job_name: str, aws_session: AwsSession +) -> list[dict[str, Any]]: """ Convert input data into a list of dicts compatible with the Braket API. Args: - input_data (Union[str, Dict, S3DataSourceConfig]): Either a channel definition or a + input_data (str | dict | S3DataSourceConfig): Either a channel definition or a dictionary mapping channel names to channel definitions, where a channel definition can be an S3DataSourceConfig or a str corresponding to a local prefix or S3 prefix. job_name (str): Hybrid job name. aws_session (AwsSession): AwsSession for possibly uploading local data. Returns: - List[Dict[str, Any]]: A list of channel configs. + list[dict[str, Any]]: A list of channel configs. """ if not isinstance(input_data, dict): input_data = {"input": input_data} @@ -405,17 +426,17 @@ def _process_channel( return S3DataSourceConfig(s3_prefix) -def _convert_input_to_config(input_data: Dict[str, S3DataSourceConfig]) -> List[Dict[str, Any]]: +def _convert_input_to_config(input_data: dict[str, S3DataSourceConfig]) -> list[dict[str, Any]]: """ Convert a dictionary mapping channel names to S3DataSourceConfigs into a list of channel configs compatible with the Braket API. Args: - input_data (Dict[str, S3DataSourceConfig]): A dictionary mapping channel names to + input_data (dict[str, S3DataSourceConfig]): A dictionary mapping channel names to S3DataSourceConfig objects. Returns: - List[Dict[str, Any]]: A list of channel configs. + list[dict[str, Any]]: A list of channel configs. """ return [ { @@ -426,5 +447,5 @@ def _convert_input_to_config(input_data: Dict[str, S3DataSourceConfig]) -> List[ ] -def _exclude_nones_factory(items: List[Tuple]) -> Dict: +def _exclude_nones_factory(items: list[tuple]) -> dict: return {k: v for k, v in items if v is not None} diff --git a/src/braket/parametric/free_parameter.py b/src/braket/parametric/free_parameter.py index 27e588d79..db22f5f60 100644 --- a/src/braket/parametric/free_parameter.py +++ b/src/braket/parametric/free_parameter.py @@ -14,7 +14,7 @@ from __future__ import annotations from numbers import Number -from typing import Dict, Union +from typing import Union from sympy import Symbol @@ -44,13 +44,12 @@ def __init__(self, name: str): Args: name (str): Name of the :class:'FreeParameter'. Can be a unicode value. - Must not start with '__'. Examples: >>> param1 = FreeParameter("theta") >>> param1 = FreeParameter("\u03B8") """ - self._set_name(name) + self._name = Symbol(name) super().__init__(expression=self._name) @property @@ -60,12 +59,12 @@ def name(self) -> str: """ return self._name.name - def subs(self, parameter_values: Dict[str, Number]) -> Union[FreeParameter, Number]: + def subs(self, parameter_values: dict[str, Number]) -> Union[FreeParameter, Number]: """ Substitutes a value in if the parameter exists within the mapping. Args: - parameter_values (Dict[str, Number]): A mapping of parameter to its + parameter_values (dict[str, Number]): A mapping of parameter to its corresponding value. Returns: @@ -100,19 +99,6 @@ def to_dict(self) -> dict: "name": self.name, } - def _set_name(self, name: str) -> None: - FreeParameter._validate_name(name) - self._name = Symbol(name) - - @staticmethod - def _validate_name(name: str) -> None: - if not name: - raise ValueError("FreeParameter names must be non empty") - if not isinstance(name, str): - raise TypeError("FreeParameter names must be strings") - if name.startswith("__"): - raise ValueError("FreeParameter names must not start with two underscores '__'") - @classmethod def from_dict(cls, parameter: dict) -> FreeParameter: return FreeParameter(parameter["name"]) diff --git a/src/braket/parametric/free_parameter_expression.py b/src/braket/parametric/free_parameter_expression.py index 9eada787f..5c4487f68 100644 --- a/src/braket/parametric/free_parameter_expression.py +++ b/src/braket/parametric/free_parameter_expression.py @@ -15,7 +15,7 @@ import ast from numbers import Number -from typing import Any, Dict, Optional, Union +from typing import Any, Optional, Union from openpulse.ast import ( ClassicalType, @@ -87,14 +87,14 @@ def expression(self) -> Union[Number, Expr]: return self._expression def subs( - self, parameter_values: Dict[str, Number] + self, parameter_values: dict[str, Number] ) -> Union[FreeParameterExpression, Number, Expr]: """ Similar to a substitution in Sympy. Parameters are swapped for corresponding values or expressions from the dictionary. Args: - parameter_values (Dict[str, Number]): A mapping of parameters to their corresponding + parameter_values (dict[str, Number]): A mapping of parameters to their corresponding values to be assigned. Returns: diff --git a/src/braket/parametric/parameterizable.py b/src/braket/parametric/parameterizable.py index 45c7561ff..bd5dbc5a7 100644 --- a/src/braket/parametric/parameterizable.py +++ b/src/braket/parametric/parameterizable.py @@ -14,7 +14,7 @@ from __future__ import annotations from abc import ABC, abstractmethod -from typing import Any, List, Union +from typing import Any, Union from braket.parametric.free_parameter import FreeParameter from braket.parametric.free_parameter_expression import FreeParameterExpression @@ -28,11 +28,11 @@ class Parameterizable(ABC): @property @abstractmethod - def parameters(self) -> List[Union[FreeParameterExpression, FreeParameter, float]]: + def parameters(self) -> list[Union[FreeParameterExpression, FreeParameter, float]]: """Get the parameters. Returns: - List[Union[FreeParameterExpression, FreeParameter, float]]: The parameters associated + list[Union[FreeParameterExpression, FreeParameter, float]]: The parameters associated with the object, either unbound free parameter expressions or bound values. The order of the parameters is determined by the subclass. """ diff --git a/src/braket/pulse/ast/approximation_parser.py b/src/braket/pulse/ast/approximation_parser.py index d8096bf83..851eb6750 100644 --- a/src/braket/pulse/ast/approximation_parser.py +++ b/src/braket/pulse/ast/approximation_parser.py @@ -10,10 +10,12 @@ # distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF # ANY KIND, either express or implied. See the License for the specific # language governing permissions and limitations under the License. + import re from collections import defaultdict +from collections.abc import KeysView from dataclasses import dataclass -from typing import Any, Dict, KeysView, List, Optional, Union +from typing import Any, Optional, Union import numpy as np from openpulse import ast @@ -43,7 +45,7 @@ class _FrameState: @dataclass class _ParseState: variables: dict - frame_data: Dict[str, _FrameState] + frame_data: dict[str, _FrameState] class _ApproximationParser(QASMVisitor[_ParseState]): @@ -52,12 +54,12 @@ class _ApproximationParser(QASMVisitor[_ParseState]): TIME_UNIT_TO_EXP = {"dt": 4, "ns": 3, "us": 2, "ms": 1, "s": 0} - def __init__(self, program: Program, frames: Dict[str, Frame]): + def __init__(self, program: Program, frames: dict[str, Frame]): self.amplitudes = defaultdict(TimeSeries) self.frequencies = defaultdict(TimeSeries) self.phases = defaultdict(TimeSeries) context = _ParseState(variables={"pi": np.pi}, frame_data=_init_frame_data(frames)) - self._qubit_frames_mapping: Dict[str, List[str]] = _init_qubit_frame_mapping(frames) + self._qubit_frames_mapping: dict[str, list[str]] = _init_qubit_frame_mapping(frames) self.visit(program.to_ast(include_externs=False), context) def visit( @@ -73,8 +75,8 @@ def visit( return super().visit(node, context) def _get_frame_parameters( - self, parameters: List[ast.Expression], context: _ParseState - ) -> Union[KeysView, List[str]]: + self, parameters: list[ast.Expression], context: _ParseState + ) -> Union[KeysView, list[str]]: frame_ids = set() for expression in parameters: identifier_name = self.visit(expression, context) @@ -466,7 +468,7 @@ def drag_gaussian(self, node: ast.FunctionCall, context: _ParseState) -> Wavefor return DragGaussianWaveform(*args) -def _init_frame_data(frames: Dict[str, Frame]) -> Dict[str, _FrameState]: +def _init_frame_data(frames: dict[str, Frame]) -> dict[str, _FrameState]: frame_states = dict() for frameId, frame in frames.items(): frame_states[frameId] = _FrameState( @@ -475,7 +477,7 @@ def _init_frame_data(frames: Dict[str, Frame]) -> Dict[str, _FrameState]: return frame_states -def _init_qubit_frame_mapping(frames: Dict[str, Frame]) -> Dict[str, List[str]]: +def _init_qubit_frame_mapping(frames: dict[str, Frame]) -> dict[str, list[str]]: mapping = {} for frameId in frames.keys(): if m := ( @@ -489,7 +491,7 @@ def _init_qubit_frame_mapping(frames: Dict[str, Frame]) -> Dict[str, List[str]]: return mapping -def _lcm_floats(*dts: List[float]) -> float: +def _lcm_floats(*dts: list[float]) -> float: """Return the least common multiple of time increments of a list of frames A time increment is the inverse of the corresponding sample rate which is considered an integer. @@ -497,7 +499,7 @@ def _lcm_floats(*dts: List[float]) -> float: Hence the LCM of dts is 1/gcd([sample rates]) Args: - *dts (List[float]): list of time resolutions + *dts (list[float]): list of time resolutions """ sample_rates = [round(1 / dt) for dt in dts] diff --git a/src/braket/pulse/ast/free_parameters.py b/src/braket/pulse/ast/free_parameters.py index 1d8545933..639645b27 100644 --- a/src/braket/pulse/ast/free_parameters.py +++ b/src/braket/pulse/ast/free_parameters.py @@ -10,7 +10,8 @@ # distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF # ANY KIND, either express or implied. See the License for the specific # language governing permissions and limitations under the License. -from typing import Dict, Union + +from typing import Union from openpulse import ast from openqasm3.visitor import QASMTransformer @@ -26,7 +27,7 @@ class _FreeParameterTransformer(QASMTransformer): """Walk the AST and evaluate FreeParameterExpressions.""" - def __init__(self, param_values: Dict[str, float], program: Program): + def __init__(self, param_values: dict[str, float], program: Program): self.param_values = param_values self.program = program super().__init__() diff --git a/src/braket/pulse/ast/qasm_parser.py b/src/braket/pulse/ast/qasm_parser.py index f43fb7e7c..1f5c48cce 100644 --- a/src/braket/pulse/ast/qasm_parser.py +++ b/src/braket/pulse/ast/qasm_parser.py @@ -10,6 +10,7 @@ # distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF # ANY KIND, either express or implied. See the License for the specific # language governing permissions and limitations under the License. + import io from openpulse import ast diff --git a/src/braket/pulse/ast/qasm_transformer.py b/src/braket/pulse/ast/qasm_transformer.py index 57a9018ea..f5e350883 100644 --- a/src/braket/pulse/ast/qasm_transformer.py +++ b/src/braket/pulse/ast/qasm_transformer.py @@ -10,6 +10,7 @@ # distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF # ANY KIND, either express or implied. See the License for the specific # language governing permissions and limitations under the License. + from typing import Any, Optional from openpulse import ast @@ -43,8 +44,8 @@ def visit_ExpressionStatement(self, expression_statement: ast.ExpressionStatemen ): # For capture_v0 nodes, it replaces it with classical assignment statements # of the form: - # __bits__[0] = capture_v0(...) - # __bits__[1] = capture_v0(...) + # b[0] = capture_v0(...) + # b[1] = capture_v0(...) new_val = ast.ClassicalAssignment( # Ideally should use IndexedIdentifier here, but this works since it is just # for printing. diff --git a/src/braket/pulse/frame.py b/src/braket/pulse/frame.py index aab243a41..b84e4a440 100644 --- a/src/braket/pulse/frame.py +++ b/src/braket/pulse/frame.py @@ -12,7 +12,7 @@ # language governing permissions and limitations under the License. import math -from typing import Any, Dict, Optional +from typing import Any, Optional from oqpy import FrameVar as OQFrame from oqpy.base import OQPyExpression @@ -33,7 +33,7 @@ def __init__( frequency: float, phase: float = 0, is_predefined: bool = False, - properties: Optional[Dict[str, Any]] = None, + properties: Optional[dict[str, Any]] = None, ): """ Args: @@ -43,7 +43,7 @@ def __init__( phase (float): phase to which this frame should be initialized. Defaults to 0. is_predefined (bool): bool indicating whether this is a predefined frame on the device. Defaults to False. - properties (Optional[Dict[str, Any]]): Dict containing properties of this frame. + properties (Optional[dict[str, Any]]): Dict containing properties of this frame. Defaults to None. """ self._frame_id = frame_id diff --git a/src/braket/pulse/port.py b/src/braket/pulse/port.py index 373836875..2b1760415 100644 --- a/src/braket/pulse/port.py +++ b/src/braket/pulse/port.py @@ -11,7 +11,7 @@ # ANY KIND, either express or implied. See the License for the specific # language governing permissions and limitations under the License. -from typing import Any, Dict, Optional +from typing import Any, Optional from oqpy import PortVar from oqpy.base import OQPyExpression @@ -23,12 +23,12 @@ class Port: a device. See https://openqasm.com/language/openpulse.html#ports for more details. """ - def __init__(self, port_id: str, dt: float, properties: Optional[Dict[str, Any]] = None): + def __init__(self, port_id: str, dt: float, properties: Optional[dict[str, Any]] = None): """ Args: port_id (str): str identifying a unique port on the device. dt (float): The smallest time step that may be used on the control hardware. - properties (Optional[Dict[str, Any]]): Dict containing properties of + properties (Optional[dict[str, Any]]): Dict containing properties of this port. Defaults to None. """ self._port_id = port_id diff --git a/src/braket/pulse/pulse_sequence.py b/src/braket/pulse/pulse_sequence.py index 3f63bb4df..0dacec41a 100644 --- a/src/braket/pulse/pulse_sequence.py +++ b/src/braket/pulse/pulse_sequence.py @@ -16,13 +16,12 @@ import builtins from copy import deepcopy from inspect import signature -from typing import Any, Dict, List, Set, Union +from typing import Any, Union from openpulse import ast from oqpy import BitVar, PhysicalQubits, Program from oqpy.timing import OQDurationLiteral -from braket.circuits.qubit_set import QubitSet from braket.parametric.free_parameter import FreeParameter from braket.parametric.free_parameter_expression import FreeParameterExpression from braket.parametric.parameterizable import Parameterizable @@ -36,6 +35,7 @@ from braket.pulse.frame import Frame from braket.pulse.pulse_sequence_trace import PulseSequenceTrace from braket.pulse.waveforms import Waveform +from braket.registers.qubit_set import QubitSet class PulseSequence: @@ -67,7 +67,7 @@ def to_time_trace(self) -> PulseSequenceTrace: ) @property - def parameters(self) -> Set[FreeParameter]: + def parameters(self) -> set[FreeParameter]: """Returns the set of `FreeParameter` s in the PulseSequence.""" return self._free_parameters.copy() @@ -169,14 +169,14 @@ def set_scale( def delay( self, - qubits_or_frames: Union[Frame, List[Frame], QubitSet], + qubits_or_frames: Union[Frame, list[Frame], QubitSet], duration: Union[float, FreeParameterExpression], ) -> PulseSequence: """ Adds an instruction to advance the frame clock by the specified `duration` value. Args: - qubits_or_frames (Union[Frame, List[Frame], QubitSet]): Qubits or frame(s) on which + qubits_or_frames (Union[Frame, list[Frame], QubitSet]): Qubits or frame(s) on which the delay needs to be introduced. duration (Union[float, FreeParameterExpression]): value (in seconds) defining the duration of the delay. @@ -196,13 +196,13 @@ def delay( self._program.delay(time=duration, qubits_or_frames=physical_qubits) return self - def barrier(self, qubits_or_frames: Union[List[Frame], QubitSet]) -> PulseSequence: + def barrier(self, qubits_or_frames: Union[list[Frame], QubitSet]) -> PulseSequence: """ Adds an instruction to align the frame clocks to the latest time across all the specified frames. Args: - qubits_or_frames (Union[List[Frame], QubitSet]): Qubits or frames which the delay + qubits_or_frames (Union[list[Frame], QubitSet]): Qubits or frames which the delay needs to be introduced. Returns: @@ -258,13 +258,13 @@ def capture_v0(self, frame: Frame) -> PulseSequence: self._frames[frame.id] = frame return self - def make_bound_pulse_sequence(self, param_values: Dict[str, float]) -> PulseSequence: + def make_bound_pulse_sequence(self, param_values: dict[str, float]) -> PulseSequence: """ Binds FreeParameters based upon their name and values passed in. If parameters share the same name, all the parameters of that name will be set to the mapped value. Args: - param_values (Dict[str, float]): A mapping of FreeParameter names + param_values (dict[str, float]): A mapping of FreeParameter names to a value to assign to them. Returns: @@ -337,7 +337,7 @@ def _format_parameter_ast( return OQDurationLiteral(parameter) if isinstance(_type, ast.DurationType) else parameter def _parse_arg_from_calibration_schema( - self, argument: Dict, waveforms: Dict[Waveform], frames: Dict[Frame] + self, argument: dict, waveforms: dict[Waveform], frames: dict[Frame] ) -> Any: nonprimitive_arg_type = { "frame": getattr(frames, "get"), @@ -351,15 +351,15 @@ def _parse_arg_from_calibration_schema( @classmethod def _parse_from_calibration_schema( - cls, calibration: Dict, waveforms: Dict[Waveform], frames: Dict[Frame] + cls, calibration: dict, waveforms: dict[Waveform], frames: dict[Frame] ) -> PulseSequence: """ Parsing a JSON input based on https://github.com/aws/amazon-braket-schemas-python/blob/main/src/braket/device_schema/pulse/native_gate_calibrations_v1.py#L26. Args: - calibration (Dict): The pulse instruction to parse - waveforms (Dict[Waveform]): The waveforms supplied for the pulse sequences. - frames (Dict[Frame]): A dictionary of frame objects to use. + calibration (dict): The pulse instruction to parse + waveforms (dict[Waveform]): The waveforms supplied for the pulse sequences. + frames (dict[Frame]): A dictionary of frame objects to use. Returns: PulseSequence: The parse sequence obtain from parsing a pulse instruction. @@ -402,12 +402,12 @@ def _parse_from_calibration_schema( raise ValueError(f"The {instr['name']} instruction has not been implemented") return calibration_sequence - def __call__(self, arg: Any = None, **kwargs) -> PulseSequence: + def __call__(self, arg: Any | None = None, **kwargs) -> PulseSequence: """ Implements the call function to easily make a bound PulseSequence. Args: - arg (Any): A value to bind to all parameters. Defaults to None and + arg (Any | None): A value to bind to all parameters. Defaults to None and can be overridden if the parameter is in kwargs. Returns: @@ -461,7 +461,7 @@ def __eq__(self, other): def _validate_uniqueness( - mapping: Dict[str, Any], values: Union[Frame, Waveform, List[Frame], List[Waveform]] + mapping: dict[str, Any], values: Union[Frame, Waveform, list[Frame], list[Waveform]] ) -> None: if not isinstance(values, list): values = [values] diff --git a/src/braket/pulse/pulse_sequence_trace.py b/src/braket/pulse/pulse_sequence_trace.py index 9b4f55933..dba4762b2 100644 --- a/src/braket/pulse/pulse_sequence_trace.py +++ b/src/braket/pulse/pulse_sequence_trace.py @@ -14,7 +14,6 @@ from __future__ import annotations from dataclasses import dataclass -from typing import Dict from braket.timings.time_series import TimeSeries @@ -34,6 +33,6 @@ class PulseSequenceTrace: the waveform phase. """ - amplitudes: Dict[str, TimeSeries] - frequencies: Dict[str, TimeSeries] - phases: Dict[str, TimeSeries] + amplitudes: dict[str, TimeSeries] + frequencies: dict[str, TimeSeries] + phases: dict[str, TimeSeries] diff --git a/src/braket/pulse/waveforms.py b/src/braket/pulse/waveforms.py index 72d2120bc..b7dafac59 100644 --- a/src/braket/pulse/waveforms.py +++ b/src/braket/pulse/waveforms.py @@ -16,7 +16,7 @@ import random import string from abc import ABC, abstractmethod -from typing import Dict, List, Optional, Union +from typing import Optional, Union import numpy as np from oqpy import WaveformVar, bool_, complex128, declare_waveform_generator, duration, float64 @@ -55,12 +55,12 @@ def sample(self, dt: float) -> np.ndarray: @staticmethod @abstractmethod - def _from_calibration_schema(waveform_json: Dict) -> Waveform: + def _from_calibration_schema(waveform_json: dict) -> Waveform: """ Parses a JSON input and returns the BDK waveform. See https://github.com/aws/amazon-braket-schemas-python/blob/main/src/braket/device_schema/pulse/native_gate_calibrations_v1.py#L104 Args: - waveform_json (Dict): A JSON object with the needed parameters for making the Waveform. + waveform_json (dict): A JSON object with the needed parameters for making the Waveform. Returns: Waveform: A Waveform object parsed from the supplied JSON. @@ -71,10 +71,10 @@ class ArbitraryWaveform(Waveform): """An arbitrary waveform with amplitudes at each timestep explicitly specified using an array.""" - def __init__(self, amplitudes: List[complex], id: Optional[str] = None): + def __init__(self, amplitudes: list[complex], id: Optional[str] = None): """ Args: - amplitudes (List[complex]): Array of complex values specifying the + amplitudes (list[complex]): Array of complex values specifying the waveform amplitude at each timestep. The timestep is determined by the sampling rate of the frame to which waveform is applied to. id (Optional[str]): The identifier used for declaring this waveform. A random string of @@ -106,7 +106,7 @@ def sample(self, dt: float) -> np.ndarray: raise NotImplementedError @staticmethod - def _from_calibration_schema(waveform_json: Dict) -> ArbitraryWaveform: + def _from_calibration_schema(waveform_json: dict) -> ArbitraryWaveform: wave_id = waveform_json["waveformId"] complex_amplitudes = [complex(i[0], i[1]) for i in waveform_json["amplitudes"]] return ArbitraryWaveform(complex_amplitudes, wave_id) @@ -132,7 +132,7 @@ def __init__( self.id = id or _make_identifier_name() @property - def parameters(self) -> List[Union[FreeParameterExpression, FreeParameter, float]]: + def parameters(self) -> list[Union[FreeParameterExpression, FreeParameter, float]]: """Returns the parameters associated with the object, either unbound free parameter expressions or bound values.""" return [self.length] @@ -184,7 +184,7 @@ def sample(self, dt: float) -> np.ndarray: return samples @staticmethod - def _from_calibration_schema(waveform_json: Dict) -> ConstantWaveform: + def _from_calibration_schema(waveform_json: dict) -> ConstantWaveform: wave_id = waveform_json["waveformId"] length = iq = None for val in waveform_json["arguments"]: @@ -237,7 +237,7 @@ def __init__( self.id = id or _make_identifier_name() @property - def parameters(self) -> List[Union[FreeParameterExpression, FreeParameter, float]]: + def parameters(self) -> list[Union[FreeParameterExpression, FreeParameter, float]]: """Returns the parameters associated with the object, either unbound free parameter expressions or bound values.""" return [self.length, self.sigma, self.beta, self.amplitude] @@ -319,7 +319,7 @@ def sample(self, dt: float) -> np.ndarray: return samples @staticmethod - def _from_calibration_schema(waveform_json: Dict) -> DragGaussianWaveform: + def _from_calibration_schema(waveform_json: dict) -> DragGaussianWaveform: waveform_parameters = {"id": waveform_json["waveformId"]} for val in waveform_json["arguments"]: waveform_parameters[val["name"]] = ( @@ -361,7 +361,7 @@ def __init__( self.id = id or _make_identifier_name() @property - def parameters(self) -> List[Union[FreeParameterExpression, FreeParameter, float]]: + def parameters(self) -> list[Union[FreeParameterExpression, FreeParameter, float]]: """Returns the parameters associated with the object, either unbound free parameter expressions or bound values.""" return [self.length, self.sigma, self.amplitude] @@ -435,7 +435,7 @@ def sample(self, dt: float) -> np.ndarray: return samples @staticmethod - def _from_calibration_schema(waveform_json: Dict) -> GaussianWaveform: + def _from_calibration_schema(waveform_json: dict) -> GaussianWaveform: waveform_parameters = {"id": waveform_json["waveformId"]} for val in waveform_json["arguments"]: waveform_parameters[val["name"]] = ( @@ -460,7 +460,7 @@ def _map_to_oqpy_type( ) -def _parse_waveform_from_calibration_schema(waveform: Dict) -> Waveform: +def _parse_waveform_from_calibration_schema(waveform: dict) -> Waveform: waveform_names = { "arbitrary": ArbitraryWaveform._from_calibration_schema, "drag_gaussian": DragGaussianWaveform._from_calibration_schema, diff --git a/src/braket/quantum_information/pauli_string.py b/src/braket/quantum_information/pauli_string.py index 1f1454492..8dca06625 100644 --- a/src/braket/quantum_information/pauli_string.py +++ b/src/braket/quantum_information/pauli_string.py @@ -14,7 +14,7 @@ from __future__ import annotations import itertools -from typing import List, Optional, Tuple, Union +from typing import Optional, Union from braket.circuits.circuit import Circuit from braket.circuits.observables import TensorProduct, X, Y, Z @@ -86,7 +86,7 @@ def to_unsigned_observable(self) -> TensorProduct: [_PAULI_OBSERVABLES[self._nontrivial[qubit]] for qubit in sorted(self._nontrivial)] ) - def weight_n_substrings(self, weight: int) -> Tuple[PauliString, ...]: + def weight_n_substrings(self, weight: int) -> tuple[PauliString, ...]: r"""Returns every substring of this Pauli string with exactly `weight` nontrivial factors. The number of substrings is equal to :math:`\binom{n}{w}`, where :math`n` is the number of @@ -96,7 +96,7 @@ def weight_n_substrings(self, weight: int) -> Tuple[PauliString, ...]: weight (int): The number of non-identity factors in the substrings. Returns: - Tuple[PauliString, ...]: A tuple of weight-n Pauli substrings. + tuple[PauliString, ...]: A tuple of weight-n Pauli substrings. """ substrings = [] for indices in itertools.combinations(self._nontrivial, weight): @@ -111,7 +111,7 @@ def weight_n_substrings(self, weight: int) -> Tuple[PauliString, ...]: ) return tuple(substrings) - def eigenstate(self, signs: Optional[Union[str, List[int], Tuple[int, ...]]] = None) -> Circuit: + def eigenstate(self, signs: Optional[Union[str, list[int], tuple[int, ...]]] = None) -> Circuit: """Returns the eigenstate of this Pauli string with the given factor signs. The resulting eigenstate has each qubit in the +1 eigenstate of its corresponding signed @@ -120,7 +120,7 @@ def eigenstate(self, signs: Optional[Union[str, List[int], Tuple[int, ...]]] = N phase of the Pauli string is ignored). Args: - signs (Optional[Union[str, List[int], Tuple[int, ...]]]): The sign of each factor of the + signs (Optional[Union[str, list[int], tuple[int, ...]]]): The sign of each factor of the eigenstate, specified either as a string of "+" and "_", or as a list or tuple of +/-1. The length of signs must be equal to the length of the Pauli string. If not specified, it is assumed to be all +. Default: None. @@ -350,7 +350,7 @@ def __repr__(self): return f"{PauliString._phase_to_str(self._phase)}{''.join(factors)}" @staticmethod - def _split(pauli_word: str) -> Tuple[int, str]: + def _split(pauli_word: str) -> tuple[int, str]: index = 0 phase = 1 if pauli_word[index] in {"+", "-"}: @@ -367,7 +367,7 @@ def _split(pauli_word: str) -> Tuple[int, str]: def _phase_to_str(phase: int) -> str: return "+" if phase > 0 else "-" - def _generate_eigenstate_circuit(self, signs: Tuple[int, ...]) -> Circuit: + def _generate_eigenstate_circuit(self, signs: tuple[int, ...]) -> Circuit: circ = Circuit() for qubit in range(len(signs)): state = signs[qubit] * self[qubit] diff --git a/src/braket/registers/__init__.py b/src/braket/registers/__init__.py new file mode 100644 index 000000000..0f433333b --- /dev/null +++ b/src/braket/registers/__init__.py @@ -0,0 +1,15 @@ +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"). You +# may not use this file except in compliance with the License. A copy of +# the License is located at +# +# http://aws.amazon.com/apache2.0/ +# +# or in the "license" file accompanying this file. This file is +# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF +# ANY KIND, either express or implied. See the License for the specific +# language governing permissions and limitations under the License. + +from braket.registers.qubit import Qubit, QubitInput # noqa: F401 +from braket.registers.qubit_set import QubitSet, QubitSetInput # noqa: F401 diff --git a/src/braket/registers/qubit.py b/src/braket/registers/qubit.py new file mode 100644 index 000000000..479a453e3 --- /dev/null +++ b/src/braket/registers/qubit.py @@ -0,0 +1,68 @@ +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"). You +# may not use this file except in compliance with the License. A copy of +# the License is located at +# +# http://aws.amazon.com/apache2.0/ +# +# or in the "license" file accompanying this file. This file is +# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF +# ANY KIND, either express or implied. See the License for the specific +# language governing permissions and limitations under the License. + +from __future__ import annotations + +import numbers +from typing import Union + +QubitInput = Union["Qubit", int] + + +class Qubit(int): + """ + A quantum bit index. The index of this qubit is locally scoped towards the contained + circuit. This may not be the exact qubit index on the quantum device. + """ + + def __new__(cls, index: int): + """ + Args: + index (int): Index of the qubit. + + Raises: + ValueError: If `index` is less than zero. + + Examples: + >>> Qubit(0) + >>> Qubit(1) + """ + if not isinstance(index, numbers.Integral): + raise TypeError(f"Supplied qubit index, {index}, must be an integer.") + if index < 0: + raise ValueError(f"Supplied qubit index, {index}, cannot be less than zero.") + return super().__new__(cls, index) + + def __repr__(self): + return f"Qubit({super().__repr__()})" + + def __str__(self): + return self.__repr__() + + @staticmethod + def new(qubit: QubitInput) -> Qubit: + """ + Helper constructor - if input is a `Qubit` it returns the same value, + else a new `Qubit` is constructed. + + Args: + qubit (QubitInput): `Qubit` index. If `type == Qubit` then the `qubit` is returned. + + Returns: + Qubit: The qubit. + """ + + if isinstance(qubit, Qubit): + return qubit + else: + return Qubit(qubit) diff --git a/src/braket/registers/qubit_set.py b/src/braket/registers/qubit_set.py new file mode 100644 index 000000000..0f9d0a7ac --- /dev/null +++ b/src/braket/registers/qubit_set.py @@ -0,0 +1,95 @@ +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"). You +# may not use this file except in compliance with the License. A copy of +# the License is located at +# +# http://aws.amazon.com/apache2.0/ +# +# or in the "license" file accompanying this file. This file is +# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF +# ANY KIND, either express or implied. See the License for the specific +# language governing permissions and limitations under the License. + +from __future__ import annotations + +from collections.abc import Iterable +from typing import Any, Union + +from boltons.setutils import IndexedSet + +from braket.registers.qubit import Qubit, QubitInput + +QubitSetInput = Union[QubitInput, Iterable[QubitInput]] + + +def _flatten(other: Any) -> Any: + if isinstance(other, Iterable) and not isinstance(other, str): + for item in other: + yield from _flatten(item) + else: + yield other + + +class QubitSet(IndexedSet): + """ + An ordered, unique set of quantum bits. + + Note: + QubitSet implements `__hash__()` but is a mutable object, therefore be careful when + mutating this object. + """ + + def __init__(self, qubits: QubitSetInput | None = None): + """ + Args: + qubits (QubitSetInput | None): Qubits to be included in the `QubitSet`. + Default is `None`. + + Examples: + >>> qubits = QubitSet([0, 1]) + >>> for qubit in qubits: + ... print(qubit) + ... + Qubit(0) + Qubit(1) + + >>> qubits = QubitSet([0, 1, [2, 3]]) + >>> for qubit in qubits: + ... print(qubit) + ... + Qubit(0) + Qubit(1) + Qubit(2) + Qubit(3) + """ + + _qubits = [Qubit.new(qubit) for qubit in _flatten(qubits)] if qubits is not None else None + super().__init__(_qubits) + + def map(self, mapping: dict[QubitInput, QubitInput]) -> QubitSet: + """ + Creates a new `QubitSet` where this instance's qubits are mapped to the values in `mapping`. + If this instance contains a qubit that is not in the `mapping` that qubit is not modified. + + Args: + mapping (dict[QubitInput, QubitInput]): A dictionary of qubit mappings to + apply. Key is the qubit in this instance to target, and the value is what + the key will be changed to. + + Returns: + QubitSet: A new QubitSet with the `mapping` applied. + + Examples: + >>> qubits = QubitSet([0, 1]) + >>> mapping = {0: 10, Qubit(1): Qubit(11)} + >>> qubits.map(mapping) + QubitSet([Qubit(10), Qubit(11)]) + """ + + new_qubits = [mapping.get(qubit, qubit) for qubit in self] + + return QubitSet(new_qubits) + + def __hash__(self): + return hash(tuple(self)) diff --git a/src/braket/tasks/analog_hamiltonian_simulation_quantum_task_result.py b/src/braket/tasks/analog_hamiltonian_simulation_quantum_task_result.py index 74c0e848f..abc39753d 100644 --- a/src/braket/tasks/analog_hamiltonian_simulation_quantum_task_result.py +++ b/src/braket/tasks/analog_hamiltonian_simulation_quantum_task_result.py @@ -16,7 +16,6 @@ from collections import Counter from dataclasses import dataclass from enum import Enum -from typing import Dict, List import numpy as np @@ -53,7 +52,7 @@ def __eq__(self, other) -> bool: class AnalogHamiltonianSimulationQuantumTaskResult: task_metadata: TaskMetadata additional_metadata: AdditionalMetadata - measurements: List[ShotResult] = None + measurements: list[ShotResult] = None def __eq__(self, other) -> bool: if isinstance(other, AnalogHamiltonianSimulationQuantumTaskResult): @@ -90,7 +89,7 @@ def _from_object_internal( ) @classmethod - def _get_measurements(cls, result: AnalogHamiltonianSimulationTaskResult) -> List[ShotResult]: + def _get_measurements(cls, result: AnalogHamiltonianSimulationTaskResult) -> list[ShotResult]: measurements = [] for measurement in result.measurements: status = AnalogHamiltonianSimulationShotStatus(measurement.shotMetadata.shotStatus) @@ -105,7 +104,7 @@ def _get_measurements(cls, result: AnalogHamiltonianSimulationTaskResult) -> Lis measurements.append(ShotResult(status, pre_sequence, post_sequence)) return measurements - def get_counts(self) -> Dict[str, int]: + def get_counts(self) -> dict[str, int]: """Aggregate state counts from AHS shot results. Notes: @@ -115,7 +114,7 @@ def get_counts(self) -> Dict[str, int]: g: ground state atom Returns: - Dict[str, int]: number of times each state configuration is measured. + dict[str, int]: number of times each state configuration is measured. Returns None if none of shot measurements are successful. Only succesful shots contribute to the state count. """ diff --git a/src/braket/tasks/gate_model_quantum_task_result.py b/src/braket/tasks/gate_model_quantum_task_result.py index 9b21b0075..631ca45e7 100644 --- a/src/braket/tasks/gate_model_quantum_task_result.py +++ b/src/braket/tasks/gate_model_quantum_task_result.py @@ -15,8 +15,9 @@ import json from collections import Counter +from collections.abc import Callable from dataclasses import dataclass -from typing import Any, Callable, Dict, List, Optional, TypeVar, Union +from typing import Any, Optional, TypeVar, Union import numpy as np @@ -42,27 +43,27 @@ class GateModelQuantumTaskResult: Args: task_metadata (TaskMetadata): Quantum task metadata. additional_metadata (AdditionalMetadata): Additional metadata about the quantum task - result_types (List[Dict[str, Any]]): List of dictionaries where each dictionary + result_types (list[dict[str, Any]]): List of dictionaries where each dictionary has two keys: 'Type' (the result type in IR JSON form) and 'Value' (the result value for this result type). This can be an empty list if no result types are specified in the IR. This is calculated from `measurements` and the IR of the circuit program when `shots>0`. - values (List[Any]): The values for result types requested in the circuit. + values (list[Any]): The values for result types requested in the circuit. This can be an empty list if no result types are specified in the IR. This is calculated from `measurements` and the IR of the circuit program when `shots>0`. measurements (numpy.ndarray, optional): 2d array - row is shot and column is qubit. Default is None. Only available when shots > 0. The qubits in `measurements` are the ones in `GateModelQuantumTaskResult.measured_qubits`. - measured_qubits (List[int], optional): The indices of the measured qubits. Default + measured_qubits (list[int], optional): The indices of the measured qubits. Default is None. Only available when shots > 0. Indicates which qubits are in `measurements`. measurement_counts (Counter, optional): A `Counter` of measurements. Key is the measurements in a big endian binary string. Value is the number of times that measurement occurred. Default is None. Only available when shots > 0. Note that the keys in `Counter` are unordered. - measurement_probabilities (Dict[str, float], optional): + measurement_probabilities (dict[str, float], optional): A dictionary of probabilistic results. Key is the measurements in a big endian binary string. Value is the probability the measurement occurred. @@ -81,17 +82,17 @@ class GateModelQuantumTaskResult: task_metadata: TaskMetadata additional_metadata: AdditionalMetadata - result_types: List[ResultTypeValue] = None - values: List[Any] = None + result_types: list[ResultTypeValue] = None + values: list[Any] = None measurements: np.ndarray = None - measured_qubits: List[int] = None + measured_qubits: list[int] = None measurement_counts: Counter = None - measurement_probabilities: Dict[str, float] = None + measurement_probabilities: dict[str, float] = None measurements_copied_from_device: bool = None measurement_counts_copied_from_device: bool = None measurement_probabilities_copied_from_device: bool = None - _result_types_indices: Dict[str, int] = None + _result_types_indices: dict[str, int] = None def __post_init__(self): if self.result_types is not None: @@ -133,6 +134,23 @@ def __eq__(self, other) -> bool: return self.task_metadata.id == other.task_metadata.id return NotImplemented + def get_compiled_circuit(self) -> Optional[str]: + """ + Get the compiled circuit, if one is available. + + Returns: + Optional[str]: The compiled circuit or None. + """ + metadata = self.additional_metadata + if not metadata: + return None + if metadata.rigettiMetadata: + return metadata.rigettiMetadata.compiledProgram + elif metadata.oqcMetadata: + return metadata.oqcMetadata.compiledProgram + else: + return None + @staticmethod def measurement_counts_from_measurements(measurements: np.ndarray) -> Counter: """ @@ -153,7 +171,7 @@ def measurement_counts_from_measurements(measurements: np.ndarray) -> Counter: @staticmethod def measurement_probabilities_from_measurement_counts( measurement_counts: Counter, - ) -> Dict[str, float]: + ) -> dict[str, float]: """ Creates measurement probabilities from measurement counts @@ -163,7 +181,7 @@ def measurement_probabilities_from_measurement_counts( occurred. Returns: - Dict[str, float]: A dictionary of probabilistic results. Key is the measurements + dict[str, float]: A dictionary of probabilistic results. Key is the measurements in a big endian binary string. Value is the probability the measurement occurred. """ measurement_probabilities = {} @@ -175,13 +193,13 @@ def measurement_probabilities_from_measurement_counts( @staticmethod def measurements_from_measurement_probabilities( - measurement_probabilities: Dict[str, float], shots: int + measurement_probabilities: dict[str, float], shots: int ) -> np.ndarray: """ Creates measurements from measurement probabilities. Args: - measurement_probabilities (Dict[str, float]): A dictionary of probabilistic results. + measurement_probabilities (dict[str, float]): A dictionary of probabilistic results. Key is the measurements in a big endian binary string. Value is the probability the measurement occurred. shots (int): number of iterations on device. @@ -352,8 +370,8 @@ def cast_result_types(gate_model_task_result: GateModelTaskResult) -> None: @staticmethod def _calculate_result_types( - ir_string: str, measurements: np.ndarray, measured_qubits: List[int] - ) -> List[ResultTypeValue]: + ir_string: str, measurements: np.ndarray, measured_qubits: list[int] + ) -> list[ResultTypeValue]: ir = json.loads(ir_string) result_types = [] if not ir.get("results"): @@ -402,7 +420,7 @@ def _calculate_result_types( @staticmethod def _selected_measurements( - measurements: np.ndarray, measured_qubits: List[int], targets: Optional[List[int]] + measurements: np.ndarray, measured_qubits: list[int], targets: Optional[list[int]] ) -> np.ndarray: if targets is not None and targets != measured_qubits: # Only some qubits targeted @@ -412,12 +430,12 @@ def _selected_measurements( @staticmethod def _calculate_for_targets( - calculate_function: Callable[[np.ndarray, List[int], Observable, List[int]], T], + calculate_function: Callable[[np.ndarray, list[int], Observable, list[int]], T], measurements: np.ndarray, - measured_qubits: List[int], + measured_qubits: list[int], observable: Observable, - targets: List[int], - ) -> Union[T, List[T]]: + targets: list[int], + ) -> Union[T, list[T]]: if targets: return calculate_function(measurements, measured_qubits, observable, targets) else: @@ -434,7 +452,7 @@ def _measurements_base_10(measurements: np.ndarray) -> np.ndarray: @staticmethod def _probability_from_measurements( - measurements: np.ndarray, measured_qubits: List[int], targets: Optional[List[int]] + measurements: np.ndarray, measured_qubits: list[int], targets: Optional[list[int]] ) -> np.ndarray: measurements = GateModelQuantumTaskResult._selected_measurements( measurements, measured_qubits, targets @@ -452,9 +470,9 @@ def _probability_from_measurements( @staticmethod def _variance_from_measurements( measurements: np.ndarray, - measured_qubits: List[int], + measured_qubits: list[int], observable: Observable, - targets: List[int], + targets: list[int], ) -> float: samples = GateModelQuantumTaskResult._samples_from_measurements( measurements, measured_qubits, observable, targets @@ -464,9 +482,9 @@ def _variance_from_measurements( @staticmethod def _expectation_from_measurements( measurements: np.ndarray, - measured_qubits: List[int], + measured_qubits: list[int], observable: Observable, - targets: List[int], + targets: list[int], ) -> float: samples = GateModelQuantumTaskResult._samples_from_measurements( measurements, measured_qubits, observable, targets @@ -476,9 +494,9 @@ def _expectation_from_measurements( @staticmethod def _samples_from_measurements( measurements: np.ndarray, - measured_qubits: List[int], + measured_qubits: list[int], observable: Observable, - targets: List[int], + targets: list[int], ) -> np.ndarray: measurements = GateModelQuantumTaskResult._selected_measurements( measurements, measured_qubits, targets diff --git a/src/braket/tasks/local_quantum_task_batch.py b/src/braket/tasks/local_quantum_task_batch.py index 6e36246e0..a47a47e6a 100644 --- a/src/braket/tasks/local_quantum_task_batch.py +++ b/src/braket/tasks/local_quantum_task_batch.py @@ -11,7 +11,7 @@ # ANY KIND, either express or implied. See the License for the specific # language governing permissions and limitations under the License. -from typing import List, Union +from typing import Union from braket.tasks import ( AnnealingQuantumTaskResult, @@ -29,7 +29,7 @@ class LocalQuantumTaskBatch(QuantumTaskBatch): def __init__( self, - results: List[ + results: list[ Union[ GateModelQuantumTaskResult, AnnealingQuantumTaskResult, @@ -41,7 +41,7 @@ def __init__( def results( self, - ) -> List[ + ) -> list[ Union[ GateModelQuantumTaskResult, AnnealingQuantumTaskResult, PhotonicModelQuantumTaskResult ] diff --git a/src/braket/tasks/quantum_task.py b/src/braket/tasks/quantum_task.py index 47ea8fff0..c306078ca 100644 --- a/src/braket/tasks/quantum_task.py +++ b/src/braket/tasks/quantum_task.py @@ -13,7 +13,7 @@ import asyncio from abc import ABC, abstractmethod -from typing import Any, Dict, Union +from typing import Any, Union from braket.tasks.annealing_quantum_task_result import AnnealingQuantumTaskResult from braket.tasks.gate_model_quantum_task_result import GateModelQuantumTaskResult @@ -62,7 +62,7 @@ def async_result(self) -> asyncio.Task: Task: Get the quantum task result asynchronously. """ - def metadata(self, use_cached_value: bool = False) -> Dict[str, Any]: + def metadata(self, use_cached_value: bool = False) -> dict[str, Any]: """ Get task metadata. @@ -71,6 +71,6 @@ def metadata(self, use_cached_value: bool = False) -> Dict[str, Any]: request. Default is False. Returns: - Dict[str, Any]: The metadata regarding the quantum task. If `use_cached_value` is True, + dict[str, Any]: The metadata regarding the quantum task. If `use_cached_value` is True, then the value retrieved from the most recent request is used. """ diff --git a/src/braket/tasks/quantum_task_batch.py b/src/braket/tasks/quantum_task_batch.py index 721afd7c1..ff0a0b82a 100644 --- a/src/braket/tasks/quantum_task_batch.py +++ b/src/braket/tasks/quantum_task_batch.py @@ -12,7 +12,7 @@ # language governing permissions and limitations under the License. from abc import ABC, abstractmethod -from typing import List, Union +from typing import Union from braket.tasks.annealing_quantum_task_result import AnnealingQuantumTaskResult from braket.tasks.gate_model_quantum_task_result import GateModelQuantumTaskResult @@ -25,13 +25,13 @@ class QuantumTaskBatch(ABC): @abstractmethod def results( self, - ) -> List[ + ) -> list[ Union[ GateModelQuantumTaskResult, AnnealingQuantumTaskResult, PhotonicModelQuantumTaskResult ] ]: """Get the quantum task results. Returns: - List[Union[GateModelQuantumTaskResult, AnnealingQuantumTaskResult, PhotonicModelQuantumTaskResult]]:: # noqa + list[Union[GateModelQuantumTaskResult, AnnealingQuantumTaskResult, PhotonicModelQuantumTaskResult]]:: # noqa Get the quantum task results. """ diff --git a/src/braket/timings/time_series.py b/src/braket/timings/time_series.py index 3baffe634..9d2b9cf00 100644 --- a/src/braket/timings/time_series.py +++ b/src/braket/timings/time_series.py @@ -14,11 +14,12 @@ from __future__ import annotations from collections import OrderedDict +from collections.abc import Iterator from dataclasses import dataclass from decimal import Decimal from enum import Enum from numbers import Number -from typing import Iterator, List, Union +from typing import Union @dataclass @@ -64,20 +65,20 @@ def put( self._sorted = False return self - def times(self) -> List[Number]: + def times(self) -> list[Number]: """Returns the times in the time series. Returns: - List[Number]: The times in the time series. + list[Number]: The times in the time series. """ self._ensure_sorted() return [item.time for item in self._series.values()] - def values(self) -> List[Number]: + def values(self) -> list[Number]: """Returns the values in the time series. Returns: - List[Number]: The values in the time series. + list[Number]: The values in the time series. """ self._ensure_sorted() return [item.value for item in self._series.values()] @@ -95,12 +96,12 @@ def _ensure_sorted(self) -> None: self._sorted = True @staticmethod - def from_lists(times: List[float], values: List[float]) -> TimeSeries: + def from_lists(times: list[float], values: list[float]) -> TimeSeries: """Create a time series from the list of time and value points Args: - times (List[float]): list of time points - values (List[float]): list of value points + times (list[float]): list of time points + values (list[float]): list of value points Returns: TimeSeries: time series constructed from lists @@ -117,18 +118,18 @@ def from_lists(times: List[float], values: List[float]) -> TimeSeries: return ts @staticmethod - def constant_like(times: Union[List[float], TimeSeries], constant: float = 0.0) -> TimeSeries: + def constant_like(times: Union[list[float], TimeSeries], constant: float = 0.0) -> TimeSeries: """Obtain a constant time series given another time series or the list of time points, and the constant values Args: - times (Union[List[float], TimeSeries]): list of time points or a time series + times (Union[list[float], TimeSeries]): list of time points or a time series constant (float): constant value Returns: TimeSeries: A constant time series """ - if not isinstance(times, List): + if not isinstance(times, list): times = times.times() ts = TimeSeries() @@ -280,12 +281,12 @@ def discretize(self, time_resolution: Decimal, value_resolution: Decimal) -> Tim return discretized_ts @staticmethod - def periodic_signal(times: List[float], values: List[float], num_repeat: int = 1) -> TimeSeries: + def periodic_signal(times: list[float], values: list[float], num_repeat: int = 1) -> TimeSeries: """Create a periodic time series by repeating the same block multiple times. Args: - times (List[float]): List of time points in a single block - values (List[float]): Values for the time series in a single block + times (list[float]): List of time points in a single block + values (list[float]): Values for the time series in a single block num_repeat (int): Number of block repeatitions Returns: diff --git a/src/braket/tracking/pricing.py b/src/braket/tracking/pricing.py index fb4b94699..2b049b251 100644 --- a/src/braket/tracking/pricing.py +++ b/src/braket/tracking/pricing.py @@ -17,7 +17,6 @@ import io import os from functools import lru_cache -from typing import Dict, List import urllib3 @@ -55,10 +54,10 @@ def get_prices(self) -> None: self._price_list = list(csv.DictReader(text_response)) @lru_cache() - def price_search(self, **kwargs) -> List[Dict[str, str]]: + def price_search(self, **kwargs) -> list[dict[str, str]]: """Searches the price list for a given set of parameters. Returns: - List[Dict[str, str]]: The price list. + list[dict[str, str]]: The price list. """ if not self._price_list: self.get_prices() diff --git a/src/braket/tracking/tracker.py b/src/braket/tracking/tracker.py index 9558d1d57..c30fc774e 100644 --- a/src/braket/tracking/tracker.py +++ b/src/braket/tracking/tracker.py @@ -16,7 +16,7 @@ from datetime import timedelta from decimal import Decimal from functools import singledispatchmethod -from typing import Any, Dict, List +from typing import Any from braket.tracking.pricing import price_search from braket.tracking.tracking_context import deregister_tracker, register_tracker @@ -66,12 +66,12 @@ def receive_event(self, event: _TaskCreationEvent) -> None: """ self._recieve_internal(event) - def tracked_resources(self) -> List[str]: + def tracked_resources(self) -> list[str]: """ Resources tracked by this tracker. Returns: - List[str]: The list of quantum task ids for quantum tasks tracked by this tracker. + list[str]: The list of quantum task ids for quantum tasks tracked by this tracker. """ return list(self._resources.keys()) @@ -117,12 +117,12 @@ def simulator_tasks_cost(self) -> Decimal: total_cost = total_cost + _get_simulator_task_cost(task_arn, details) return total_cost - def quantum_tasks_statistics(self) -> Dict[str, Dict[str, Any]]: + def quantum_tasks_statistics(self) -> dict[str, dict[str, Any]]: """ Get a summary of quantum tasks grouped by device. Returns: - Dict[str,Dict[str,Any]] : A dictionary where each key is a device arn, and maps to + dict[str,dict[str,Any]] : A dictionary where each key is a device arn, and maps to a dictionary sumarizing the quantum tasks run on the device. The summary includes the total shots sent to the device and the most recent status of the quantum tasks created on this device. For finished quantum tasks on simulator devices, the summary diff --git a/test/integ_tests/job_test_module/job_test_submodule/job_test_submodule_file.py b/test/integ_tests/job_test_module/job_test_submodule/job_test_submodule_file.py new file mode 100644 index 000000000..6c916a60d --- /dev/null +++ b/test/integ_tests/job_test_module/job_test_submodule/job_test_submodule_file.py @@ -0,0 +1,3 @@ +def submodule_helper(): + print("import successful!") + return {"status": "SUCCESS"} diff --git a/test/integ_tests/job_test_module/job_test_submodule/requirements.txt b/test/integ_tests/job_test_module/job_test_submodule/requirements.txt new file mode 100644 index 000000000..e079f8a60 --- /dev/null +++ b/test/integ_tests/job_test_module/job_test_submodule/requirements.txt @@ -0,0 +1 @@ +pytest diff --git a/test/integ_tests/job_test_script.py b/test/integ_tests/job_test_script.py index 7071ffd62..95b890d60 100644 --- a/test/integ_tests/job_test_script.py +++ b/test/integ_tests/job_test_script.py @@ -11,19 +11,19 @@ # ANY KIND, either express or implied. See the License for the specific # language governing permissions and limitations under the License. -import json -import os - from braket.aws import AwsDevice from braket.circuits import Circuit -from braket.jobs import save_job_checkpoint, save_job_result +from braket.jobs import ( + get_hyperparameters, + get_job_device_arn, + save_job_checkpoint, + save_job_result, +) from braket.jobs_data import PersistedJobDataFormat def start_here(): - hp_file = os.environ["AMZN_BRAKET_HP_FILE"] - with open(hp_file, "r") as f: - hyperparameters = json.load(f) + hyperparameters = get_hyperparameters() if hyperparameters["test_case"] == "completed": completed_job_script() @@ -33,14 +33,14 @@ def start_here(): def failed_job_script(): print("Test job started!!!!!") - assert 0 + open("fake_file") def completed_job_script(): print("Test job started!!!!!") # Use the device declared in the Orchestration Script - device = AwsDevice(os.environ["AMZN_BRAKET_DEVICE_ARN"]) + device = AwsDevice(get_job_device_arn()) bell = Circuit().h(0).cnot(0, 1) for count in range(5): @@ -51,3 +51,8 @@ def completed_job_script(): save_job_checkpoint({"some_data": "abc"}, data_format=PersistedJobDataFormat.PICKLED_V4) print("Test job completed!!!!!") + + +def job_helper(): + print("import successful!") + return {"status": "SUCCESS"} diff --git a/test/integ_tests/requirements.txt b/test/integ_tests/requirements.txt new file mode 100644 index 000000000..e079f8a60 --- /dev/null +++ b/test/integ_tests/requirements.txt @@ -0,0 +1 @@ +pytest diff --git a/test/integ_tests/test_adjoint_gradient.py b/test/integ_tests/test_adjoint_gradient.py index 0f0b6982f..99ab7dfe2 100644 --- a/test/integ_tests/test_adjoint_gradient.py +++ b/test/integ_tests/test_adjoint_gradient.py @@ -42,10 +42,10 @@ def test_adjoint_gradient_quantum_task_with_nested_targets( expected_openqasm = ( "OPENQASM 3.0;\n" "input float theta;\n" - "qubit[4] __qubits__;\n" - "rx(theta) __qubits__[0];\n" - "#pragma braket result adjoint_gradient expectation(-6 * " - "y(__qubits__[0]) @ i(__qubits__[1]) + 0.75 * y(__qubits__[2]) @ z(__qubits__[3])) theta" + "qubit[4] q;\n" + "rx(theta) q[0];\n" + "#pragma braket result adjoint_gradient expectation(-6 * y(q[0]) @ i(q[1]) + 0.75 * " + "y(q[2]) @ z(q[3])) theta" ) gradient_task = AwsQuantumTask.create( @@ -83,10 +83,10 @@ def test_adjoint_gradient_with_standard_observable_terms( expected_openqasm = ( "OPENQASM 3.0;\n" "input float theta;\n" - "qubit[3] __qubits__;\n" - "rx(theta) __qubits__[0];\n" - "#pragma braket result adjoint_gradient expectation(2 * " - "x(__qubits__[0]) + 3 * y(__qubits__[1]) - 1 * z(__qubits__[2])) theta" + "qubit[3] q;\n" + "rx(theta) q[0];\n" + "#pragma braket result adjoint_gradient expectation(2 * x(q[0]) + 3 * y(q[1]) " + "- 1 * z(q[2])) theta" ) gradient_task = AwsQuantumTask.create( @@ -132,19 +132,17 @@ def test_adjoint_gradient_with_batch_circuits(aws_session, s3_destination_folder ( "OPENQASM 3.0;\n" "input float theta;\n" - "qubit[2] __qubits__;\n" - "rx(theta) __qubits__[0];\n" - "#pragma braket result adjoint_gradient expectation(6 *" - " y(__qubits__[0]) @ i(__qubits__[1])) theta" + "qubit[2] q;\n" + "rx(theta) q[0];\n" + "#pragma braket result adjoint_gradient expectation(6 * y(q[0]) @ i(q[1])) theta" ), ( "OPENQASM 3.0;\n" "input float theta;\n" - "qubit[2] __qubits__;\n" - "rx(theta) __qubits__[0];\n" - "#pragma braket result adjoint_gradient expectation(-6 *" - " y(__qubits__[0]) @ i(__qubits__[1]) + 0.75 *" - " y(__qubits__[0]) @ z(__qubits__[1])) theta" + "qubit[2] q;\n" + "rx(theta) q[0];\n" + "#pragma braket result adjoint_gradient expectation(-6 * y(q[0]) @ i(q[1]) + 0.75 * " + "y(q[0]) @ z(q[1])) theta" ), ] diff --git a/test/integ_tests/test_create_quantum_job.py b/test/integ_tests/test_create_quantum_job.py index 568a53bc3..3b1b8ae95 100644 --- a/test/integ_tests/test_create_quantum_job.py +++ b/test/integ_tests/test_create_quantum_job.py @@ -14,10 +14,26 @@ import json import os.path import re +import sys import tempfile from pathlib import Path +import job_test_script +import pytest +from job_test_module.job_test_submodule.job_test_submodule_file import submodule_helper + +from braket.aws import AwsSession from braket.aws.aws_quantum_job import AwsQuantumJob +from braket.devices import Devices +from braket.jobs import Framework, get_input_data_dir, hybrid_job, retrieve_image, save_job_result + + +def decorator_python_version(): + aws_session = AwsSession() + image_uri = retrieve_image(Framework.BASE, aws_session.region) + tag = aws_session.get_full_image_tag(image_uri) + major_version, minor_version = re.search(r"-py(\d)(\d+)-", tag).groups() + return int(major_version), int(minor_version) def test_failed_quantum_job(aws_session, capsys): @@ -62,7 +78,7 @@ def test_failed_quantum_job(aws_session, capsys): "braket_container.py", "Running Code As Process", "Test job started!!!!!", - "AssertionError", + "FileNotFoundError: [Errno 2] No such file or directory: 'fake_file'", "Code Run Finished", '"user_entry_point": "braket_container.py"', ] @@ -70,8 +86,9 @@ def test_failed_quantum_job(aws_session, capsys): for data in logs_to_validate: assert data in log_data - assert job.metadata()["failureReason"].startswith( - "AlgorithmError: Job at job_test_script:start_here" + assert job.metadata()["failureReason"] == ( + "AlgorithmError: FileNotFoundError: [Errno 2] " + "No such file or directory: 'fake_file', exit code: 1" ) @@ -152,14 +169,17 @@ def test_completed_quantum_job(aws_session, capsys): with tempfile.TemporaryDirectory() as temp_dir: os.chdir(temp_dir) - job.download_result() - assert ( - Path(AwsQuantumJob.RESULTS_TAR_FILENAME).exists() and Path(downloaded_result).exists() - ) + try: + job.download_result() + assert ( + Path(AwsQuantumJob.RESULTS_TAR_FILENAME).exists() + and Path(downloaded_result).exists() + ) - # Check results match the expectations. - assert job.result() == {"converged": True, "energy": -0.2} - os.chdir(current_dir) + # Check results match the expectations. + assert job.result() == {"converged": True, "energy": -0.2} + finally: + os.chdir(current_dir) # Check the logs and validate it contains required output. job.logs(wait=True) @@ -178,3 +198,114 @@ def test_completed_quantum_job(aws_session, capsys): for data in logs_to_validate: assert data in log_data + + +@pytest.mark.xfail( + (sys.version_info.major, sys.version_info.minor) != decorator_python_version(), + raises=RuntimeError, + reason="Python version mismatch", +) +def test_decorator_job(): + class MyClass: + attribute = "value" + + def __str__(self): + return f"MyClass({self.attribute})" + + @hybrid_job( + device=Devices.Amazon.SV1, + include_modules="job_test_script", + dependencies=str(Path("test", "integ_tests", "requirements.txt")), + input_data=str(Path("test", "integ_tests", "requirements")), + ) + def decorator_job(a, b: int, c=0, d: float = 1.0, **extras): + with open(Path(get_input_data_dir()) / "requirements.txt", "r") as f: + assert f.readlines() == ["pytest\n"] + with open(Path("test", "integ_tests", "requirements.txt"), "r") as f: + assert f.readlines() == ["pytest\n"] + assert dir(pytest) + assert a.attribute == "value" + assert b == 2 + assert c == 0 + assert d == 5 + assert extras["extra_arg"] == "extra_value" + + hp_file = os.environ["AMZN_BRAKET_HP_FILE"] + with open(hp_file, "r") as f: + hyperparameters = json.load(f) + assert hyperparameters == { + "a": "MyClass{value}", + "b": "2", + "c": "0", + "d": "5", + "extra_arg": "extra_value", + } + + with open("test/output_file.txt", "w") as f: + f.write("hello") + + return job_test_script.job_helper() + + job = decorator_job(MyClass(), 2, d=5, extra_arg="extra_value") + assert job.result()["status"] == "SUCCESS" + + current_dir = Path.cwd() + with tempfile.TemporaryDirectory() as temp_dir: + os.chdir(temp_dir) + try: + job.download_result() + with open(Path(job.name, "test", "output_file.txt"), "r") as f: + assert f.read() == "hello" + assert ( + Path(job.name, "results.json").exists() + and Path(job.name, "test").exists() + and not Path(job.name, "test", "integ_tests").exists() + ) + finally: + os.chdir(current_dir) + + +@pytest.mark.xfail( + (sys.version_info.major, sys.version_info.minor) != decorator_python_version(), + raises=RuntimeError, + reason="Python version mismatch", +) +def test_decorator_job_submodule(): + @hybrid_job( + device=Devices.Amazon.SV1, + include_modules=[ + "job_test_module", + ], + dependencies=Path( + "test", "integ_tests", "job_test_module", "job_test_submodule", "requirements.txt" + ), + input_data={ + "my_input": str(Path("test", "integ_tests", "requirements.txt")), + "my_dir": str(Path("test", "integ_tests", "job_test_module")), + }, + ) + def decorator_job_submodule(): + with open(Path(get_input_data_dir("my_input")) / "requirements.txt", "r") as f: + assert f.readlines() == ["pytest\n"] + with open(Path("test", "integ_tests", "requirements.txt"), "r") as f: + assert f.readlines() == ["pytest\n"] + with open( + Path(get_input_data_dir("my_dir")) / "job_test_submodule" / "requirements.txt", "r" + ) as f: + assert f.readlines() == ["pytest\n"] + with open( + Path( + "test", + "integ_tests", + "job_test_module", + "job_test_submodule", + "requirements.txt", + ), + "r", + ) as f: + assert f.readlines() == ["pytest\n"] + assert dir(pytest) + save_job_result(submodule_helper()) + + job = decorator_job_submodule() + assert job.result()["status"] == "SUCCESS" diff --git a/test/integ_tests/test_device_creation.py b/test/integ_tests/test_device_creation.py index decd7d876..4cb7de2b1 100644 --- a/test/integ_tests/test_device_creation.py +++ b/test/integ_tests/test_device_creation.py @@ -18,7 +18,7 @@ from braket.aws import AwsDevice from braket.devices import Devices -RIGETTI_ARN = "arn:aws:braket:::device/qpu/rigetti/Aspen-10" +RIGETTI_ARN = "arn:aws:braket:us-west-1::device/qpu/rigetti/Aspen-M-3" IONQ_ARN = "arn:aws:braket:us-east-1::device/qpu/ionq/Harmony" SIMULATOR_ARN = "arn:aws:braket:::device/quantum-simulator/amazon/sv1" OQC_ARN = "arn:aws:braket:eu-west-2::device/qpu/oqc/Lucy" diff --git a/test/integ_tests/test_pulse.py b/test/integ_tests/test_pulse.py index c40a4556a..4eb3ffa93 100644 --- a/test/integ_tests/test_pulse.py +++ b/test/integ_tests/test_pulse.py @@ -210,6 +210,8 @@ def cz_pulse( def test_pulse_bell(arbitrary_waveform, device): + if device.status == "OFFLINE": + pytest.skip("Device offline") ( a, b, @@ -258,6 +260,8 @@ def test_pulse_bell(arbitrary_waveform, device): def test_pulse_sequence(arbitrary_waveform, device): + if device.status == "OFFLINE": + pytest.skip("Device offline") ( a, b, @@ -310,6 +314,8 @@ def test_pulse_sequence(arbitrary_waveform, device): def test_gate_calibration_run(device, pulse_sequence): + if device.status == "OFFLINE": + pytest.skip("Device offline") user_gate_calibrations = GateCalibrations({(Gate.Rx(math.pi / 2), QubitSet(0)): pulse_sequence}) num_shots = 50 bell_circuit = Circuit().rx(0, math.pi / 2).rx(1, math.pi / 2).cz(0, 1).rx(1, -math.pi / 2) diff --git a/test/integ_tests/test_queue_information.py b/test/integ_tests/test_queue_information.py new file mode 100644 index 000000000..3398fde40 --- /dev/null +++ b/test/integ_tests/test_queue_information.py @@ -0,0 +1,84 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"). You +# may not use this file except in compliance with the License. A copy of +# the License is located at +# +# http://aws.amazon.com/apache2.0/ +# +# or in the "license" file accompanying this file. This file is +# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF +# ANY KIND, either express or implied. See the License for the specific +# language governing permissions and limitations under the License. + +from braket.aws import AwsDevice, AwsQuantumJob +from braket.aws.queue_information import ( + HybridJobQueueInfo, + QuantumTaskQueueInfo, + QueueDepthInfo, + QueueType, +) +from braket.circuits import Circuit +from braket.devices import Devices + + +def test_task_queue_position(): + device = AwsDevice(Devices.Amazon.SV1) + + bell = Circuit().h(0).cnot(0, 1) + task = device.run(bell, shots=10) + + # call the queue_position method. + queue_information = task.queue_position() + + # data type validations + assert isinstance(queue_information, QuantumTaskQueueInfo) + assert isinstance(queue_information.queue_type, QueueType) + assert isinstance(queue_information.queue_position, (str, type(None))) + + # assert queue priority + assert queue_information.queue_type in [QueueType.NORMAL, QueueType.PRIORITY] + + # assert message + if queue_information.queue_position is None: + assert queue_information.message is not None + assert isinstance(queue_information.message, (str, type(None))) + else: + assert queue_information.message is None + + +def test_job_queue_position(aws_session): + job = AwsQuantumJob.create( + device=Devices.Amazon.SV1, + source_module="test/integ_tests/job_test_script.py", + entry_point="job_test_script:start_here", + aws_session=aws_session, + wait_until_complete=True, + hyperparameters={"test_case": "completed"}, + ) + + # call the queue_position method. + queue_information = job.queue_position() + + # data type validations + assert isinstance(queue_information, HybridJobQueueInfo) + + # assert message + assert queue_information.queue_position is None + assert isinstance(queue_information.message, str) + + +def test_queue_depth(): + device = AwsDevice(Devices.Amazon.SV1) + + # call the queue_depth method. + queue_information = device.queue_depth() + + # data type validations + assert isinstance(queue_information, QueueDepthInfo) + assert isinstance(queue_information.quantum_tasks, dict) + assert isinstance(queue_information.jobs, str) + + for key, value in queue_information.quantum_tasks.items(): + assert isinstance(key, QueueType) + assert isinstance(value, str) diff --git a/test/unit_tests/braket/aws/test_aws_device.py b/test/unit_tests/braket/aws/test_aws_device.py index 40abf6389..3770d38a2 100644 --- a/test/unit_tests/braket/aws/test_aws_device.py +++ b/test/unit_tests/braket/aws/test_aws_device.py @@ -34,6 +34,7 @@ from jsonschema import validate from braket.aws import AwsDevice, AwsDeviceType, AwsQuantumTask +from braket.aws.queue_information import QueueDepthInfo, QueueType from braket.circuits import Circuit, FreeParameter, Gate, QubitSet from braket.circuits.gate_calibrations import GateCalibrations from braket.device_schema.device_execution_window import DeviceExecutionWindow @@ -77,7 +78,6 @@ MOCK_GATE_MODEL_QPU_CAPABILITIES_JSON_1 ) - MOCK_gate_calibrations_JSON = { "gates": { "0": { @@ -218,6 +218,11 @@ def test_mock_rigetti_schema_1(): "providerName": "Rigetti", "deviceStatus": "OFFLINE", "deviceCapabilities": MOCK_GATE_MODEL_QPU_CAPABILITIES_1.json(), + "deviceQueueInfo": [ + {"queue": "QUANTUM_TASKS_QUEUE", "queueSize": "19", "queuePriority": "Normal"}, + {"queue": "QUANTUM_TASKS_QUEUE", "queueSize": "3", "queuePriority": "Priority"}, + {"queue": "JOBS_QUEUE", "queueSize": "0 (3 prioritized job(s) running)"}, + ], } MOCK_GATE_MODEL_QPU_CAPABILITIES_JSON_2 = { @@ -628,7 +633,6 @@ def test_device_refresh_metadata(arn): "nativeGateCalibrationsRef": "file://hostname/foo/bar", } - MOCK_PULSE_MODEL_QPU_PULSE_CAPABILITIES_JSON_2 = { "braketSchemaHeader": { "name": "braket.device_schema.pulse.pulse_device_action_properties", @@ -1669,6 +1673,75 @@ def test_get_devices_simulators_only(mock_copy_session, aws_session): assert [result.name for result in results] == ["SV1"] +@pytest.mark.filterwarnings("ignore:Test Code:") +@patch("braket.aws.aws_device.AwsSession.copy_session") +def test_get_devices_with_error_in_region(mock_copy_session, aws_session): + aws_session.search_devices.side_effect = [ + # us-west-1 + [ + { + "deviceArn": SV1_ARN, + "deviceName": "SV1", + "deviceType": "SIMULATOR", + "deviceStatus": "ONLINE", + "providerName": "Amazon Braket", + } + ], + ValueError("should not be reachable"), + ] + aws_session.get_device.side_effect = [ + MOCK_GATE_MODEL_SIMULATOR, + ValueError("should not be reachable"), + ] + session_for_region = Mock() + session_for_region.search_devices.side_effect = [ + # us-east-1 + [ + { + "deviceArn": IONQ_ARN, + "deviceName": "IonQ Device", + "deviceType": "QPU", + "deviceStatus": "ONLINE", + "providerName": "IonQ", + }, + ], + # us-west-2 + ClientError( + { + "Error": { + "Code": "Test Code", + "Message": "Test Message", + } + }, + "Test Operation", + ), + # eu-west-2 + [ + { + "deviceArn": OQC_ARN, + "deviceName": "Lucy", + "deviceType": "QPU", + "deviceStatus": "ONLINE", + "providerName": "OQC", + } + ], + # Only two regions to search outside of current + ValueError("should not be reachable"), + ] + session_for_region.get_device.side_effect = [ + MOCK_GATE_MODEL_QPU_2, + MOCK_GATE_MODEL_QPU_3, + ValueError("should not be reachable"), + ] + mock_copy_session.return_value = session_for_region + # Search order: us-east-1, us-west-1, us-west-2, eu-west-2 + results = AwsDevice.get_devices( + statuses=["ONLINE"], + aws_session=aws_session, + ) + assert [result.name for result in results] == ["Blah", "Lucy", "SV1"] + + @pytest.mark.xfail(raises=ValueError) def test_get_devices_invalid_order_by(): AwsDevice.get_devices(order_by="foo") @@ -1937,3 +2010,14 @@ def test_parse_calibration_data_bad_instr(bad_input): ) device = AwsDevice(DWAVE_ARN, mock_session) device._parse_calibration_json(bad_input) + + +def test_queue_depth(arn): + mock_session = Mock() + mock_session.get_device.return_value = MOCK_GATE_MODEL_QPU_1 + mock_session.region = RIGETTI_REGION + device = AwsDevice(arn, mock_session) + assert device.queue_depth() == QueueDepthInfo( + quantum_tasks={QueueType.NORMAL: "19", QueueType.PRIORITY: "3"}, + jobs="0 (3 prioritized job(s) running)", + ) diff --git a/test/unit_tests/braket/aws/test_aws_quantum_job.py b/test/unit_tests/braket/aws/test_aws_quantum_job.py index cbc535a2a..19b46d72b 100644 --- a/test/unit_tests/braket/aws/test_aws_quantum_job.py +++ b/test/unit_tests/braket/aws/test_aws_quantum_job.py @@ -24,6 +24,7 @@ from botocore.exceptions import ClientError from braket.aws import AwsQuantumJob, AwsSession +from braket.aws.queue_information import HybridJobQueueInfo @pytest.fixture @@ -42,7 +43,7 @@ def fake_copy_session(region): _aws_session.copy_session.side_effect = fake_copy_session _aws_session.list_keys.return_value = ["job-path/output/model.tar.gz"] - _aws_session.region = "us-test-1" + _aws_session.region = job_region _braket_client_mock = Mock(meta=Mock(region_name=job_region)) _aws_session.braket_client = _braket_client_mock @@ -141,7 +142,7 @@ def quantum_job(quantum_job_arn, aws_session): def test_equality(quantum_job_arn, aws_session, job_region): - new_aws_session = Mock(braket_client=Mock(meta=Mock(region_name=job_region))) + new_aws_session = Mock(region=job_region) quantum_job_1 = AwsQuantumJob(quantum_job_arn, aws_session) quantum_job_2 = AwsQuantumJob(quantum_job_arn, aws_session) quantum_job_3 = AwsQuantumJob(quantum_job_arn, new_aws_session) @@ -194,7 +195,7 @@ def test_quantum_job_constructor_invalid_region(aws_session): @patch("braket.aws.aws_quantum_job.boto3.Session") def test_quantum_job_constructor_explicit_session(mock_session, quantum_job_arn, job_region): - aws_session_mock = Mock(braket_client=Mock(meta=Mock(region_name=job_region))) + aws_session_mock = Mock(region=job_region) job = AwsQuantumJob(quantum_job_arn, aws_session_mock) assert job._aws_session == aws_session_mock assert job.arn == quantum_job_arn @@ -226,6 +227,27 @@ def test_metadata_caching(quantum_job, aws_session, generate_get_job_response, q assert aws_session.get_job.call_count == 1 +def test_queue_position(quantum_job, aws_session, generate_get_job_response): + state_1 = "COMPLETED" + queue_info = { + "queue": "JOBS_QUEUE", + "position": "None", + "message": "Job is in COMPLETED status. " + "AmazonBraket does not show queue position for this status.", + } + get_job_response_completed = generate_get_job_response(status=state_1, queueInfo=queue_info) + aws_session.get_job.return_value = get_job_response_completed + assert quantum_job.queue_position() == HybridJobQueueInfo( + queue_position=None, message=queue_info["message"] + ) + + state_2 = "QUEUED" + queue_info = {"queue": "JOBS_QUEUE", "position": "2"} + get_job_response_queued = generate_get_job_response(status=state_2, queueInfo=queue_info) + aws_session.get_job.return_value = get_job_response_queued + assert quantum_job.queue_position() == HybridJobQueueInfo(queue_position="2", message=None) + + def test_state(quantum_job, aws_session, generate_get_job_response, quantum_job_arn): state_1 = "RUNNING" get_job_response_running = generate_get_job_response(status=state_1) @@ -485,7 +507,7 @@ def role_arn(): @pytest.fixture( params=[ - "arn:aws:braket:us-test-1::device/qpu/test/device-name", + "arn:aws:braket:us-west-2::device/qpu/test/device-name", "arn:aws:braket:::device/qpu/test/device-name", ] ) @@ -939,7 +961,7 @@ def test_no_region_routing_simulator(aws_session): ) device_arn = "arn:aws:braket:::device/simulator/test/device-name" - device_not_found = f"Simulator '{device_arn}' not found in 'us-test-1'" + device_not_found = f"Simulator '{device_arn}' not found in 'us-west-2'" with pytest.raises(ValueError, match=device_not_found): AwsQuantumJob._initialize_session(aws_session, device_arn, logger) diff --git a/test/unit_tests/braket/aws/test_aws_quantum_task.py b/test/unit_tests/braket/aws/test_aws_quantum_task.py index 0e16a7380..99270ad25 100644 --- a/test/unit_tests/braket/aws/test_aws_quantum_task.py +++ b/test/unit_tests/braket/aws/test_aws_quantum_task.py @@ -26,6 +26,7 @@ from braket.aws import AwsQuantumTask from braket.aws.aws_quantum_task import _create_annealing_device_params from braket.aws.aws_session import AwsSession +from braket.aws.queue_information import QuantumTaskQueueInfo, QueueType from braket.circuits import Circuit from braket.circuits.gates import PulseGate from braket.circuits.serialization import ( @@ -202,6 +203,23 @@ def test_metadata_call_if_none(quantum_task): quantum_task._aws_session.get_quantum_task.assert_called_with(quantum_task.id) +def test_queue_position(quantum_task): + state_1 = "QUEUED" + _mock_metadata(quantum_task._aws_session, state_1) + assert quantum_task.queue_position() == QuantumTaskQueueInfo( + queue_type=QueueType.NORMAL, queue_position="2", message=None + ) + + state_2 = "COMPLETED" + message = ( + f"'Task is in {state_2} status. AmazonBraket does not show queue position for this status.'" + ) + _mock_metadata(quantum_task._aws_session, state_2) + assert quantum_task.queue_position() == QuantumTaskQueueInfo( + queue_type=QueueType.NORMAL, queue_position=None, message=message + ) + + def test_state(quantum_task): state_1 = "RUNNING" _mock_metadata(quantum_task._aws_session, state_1) @@ -573,12 +591,12 @@ def test_create_pulse_gate_circuit( expected_openqasm = "\n".join( ( "OPENQASM 3.0;", - "bit[2] __bits__;", + "bit[2] b;", "cal {", " set_frequency(predefined_frame_1, 6000000.0);", "}", - "__bits__[0] = measure $0;", - "__bits__[1] = measure $1;", + "b[0] = measure $0;", + "b[1] = measure $1;", ) ) @@ -1097,11 +1115,32 @@ def _assert_create_quantum_task_called_with( def _mock_metadata(aws_session, state): - aws_session.get_quantum_task.return_value = { - "status": state, - "outputS3Bucket": S3_TARGET.bucket, - "outputS3Directory": S3_TARGET.key, - } + message = ( + f"'Task is in {state} status. AmazonBraket does not show queue position for this status.'" + ) + if state in AwsQuantumTask.TERMINAL_STATES or state in ["RUNNING", "CANCELLING"]: + aws_session.get_quantum_task.return_value = { + "status": state, + "outputS3Bucket": S3_TARGET.bucket, + "outputS3Directory": S3_TARGET.key, + "queueInfo": { + "queue": "QUANTUM_TASKS_QUEUE", + "position": "None", + "queuePriority": "Normal", + "message": message, + }, + } + else: + aws_session.get_quantum_task.return_value = { + "status": state, + "outputS3Bucket": S3_TARGET.bucket, + "outputS3Directory": S3_TARGET.key, + "queueInfo": { + "queue": "QUANTUM_TASKS_QUEUE", + "position": "2", + "queuePriority": "Normal", + }, + } def _mock_s3(aws_session, result): diff --git a/test/unit_tests/braket/aws/test_aws_session.py b/test/unit_tests/braket/aws/test_aws_session.py index 53423d99f..c61d22606 100644 --- a/test/unit_tests/braket/aws/test_aws_session.py +++ b/test/unit_tests/braket/aws/test_aws_session.py @@ -418,16 +418,20 @@ def test_create_quantum_task_with_job_token(aws_session): def test_get_quantum_task(aws_session): arn = "foo:bar:arn" status = "STATUS" + queue_info = ["QueueInfo"] return_value = {"quantumTaskArn": arn, "status": status} aws_session.braket_client.get_quantum_task.return_value = return_value assert aws_session.get_quantum_task(arn) == return_value - aws_session.braket_client.get_quantum_task.assert_called_with(quantumTaskArn=arn) + aws_session.braket_client.get_quantum_task.assert_called_with( + quantumTaskArn=arn, additionalAttributeNames=queue_info + ) def test_get_quantum_task_retry(aws_session, throttling_response, resource_not_found_response): arn = "foo:bar:arn" status = "STATUS" + queue_info = ["QueueInfo"] return_value = {"quantumTaskArn": arn, "status": status} aws_session.braket_client.get_quantum_task.side_effect = [ @@ -437,7 +441,9 @@ def test_get_quantum_task_retry(aws_session, throttling_response, resource_not_f ] assert aws_session.get_quantum_task(arn) == return_value - aws_session.braket_client.get_quantum_task.assert_called_with(quantumTaskArn=arn) + aws_session.braket_client.get_quantum_task.assert_called_with( + quantumTaskArn=arn, additionalAttributeNames=queue_info + ) assert aws_session.braket_client.get_quantum_task.call_count == 3 @@ -474,16 +480,20 @@ def test_get_quantum_task_does_not_retry_other_exceptions(aws_session): def test_get_job(aws_session, get_job_response): arn = "arn:aws:braket:us-west-2:1234567890:job/job-name" + queue_info = ["QueueInfo"] aws_session.braket_client.get_job.return_value = get_job_response assert aws_session.get_job(arn) == get_job_response - aws_session.braket_client.get_job.assert_called_with(jobArn=arn) + aws_session.braket_client.get_job.assert_called_with( + jobArn=arn, additionalAttributeNames=queue_info + ) def test_get_job_retry( aws_session, get_job_response, throttling_response, resource_not_found_response ): arn = "arn:aws:braket:us-west-2:1234567890:job/job-name" + queue_info = ["QueueInfo"] aws_session.braket_client.get_job.side_effect = [ ClientError(resource_not_found_response, "unit-test"), @@ -492,12 +502,15 @@ def test_get_job_retry( ] assert aws_session.get_job(arn) == get_job_response - aws_session.braket_client.get_job.assert_called_with(jobArn=arn) + aws_session.braket_client.get_job.assert_called_with( + jobArn=arn, additionalAttributeNames=queue_info + ) assert aws_session.braket_client.get_job.call_count == 3 def test_get_job_fail_after_retries(aws_session, throttling_response, resource_not_found_response): arn = "arn:aws:braket:us-west-2:1234567890:job/job-name" + queue_info = ["QueueInfo"] aws_session.braket_client.get_job.side_effect = [ ClientError(resource_not_found_response, "unit-test"), @@ -507,12 +520,15 @@ def test_get_job_fail_after_retries(aws_session, throttling_response, resource_n with pytest.raises(ClientError): aws_session.get_job(arn) - aws_session.braket_client.get_job.assert_called_with(jobArn=arn) + aws_session.braket_client.get_job.assert_called_with( + jobArn=arn, additionalAttributeNames=queue_info + ) assert aws_session.braket_client.get_job.call_count == 3 def test_get_job_does_not_retry_other_exceptions(aws_session): arn = "arn:aws:braket:us-west-2:1234567890:job/job-name" + queue_info = ["QueueInfo"] exception_response = { "Error": { "Code": "SomeOtherException", @@ -526,7 +542,9 @@ def test_get_job_does_not_retry_other_exceptions(aws_session): with pytest.raises(ClientError): aws_session.get_job(arn) - aws_session.braket_client.get_job.assert_called_with(jobArn=arn) + aws_session.braket_client.get_job.assert_called_with( + jobArn=arn, additionalAttributeNames=queue_info + ) assert aws_session.braket_client.get_job.call_count == 1 @@ -655,6 +673,18 @@ def test_cancel_job_surfaces_errors(exception_type, aws_session): }, ], ), + ( + {"statuses": ["RETIRED"]}, + [ + { + "deviceArn": "arn4", + "deviceName": "name4", + "deviceType": "QPU", + "deviceStatus": "RETIRED", + "providerName": "pname3", + }, + ], + ), ( {"provider_names": ["pname2"]}, [ @@ -727,6 +757,13 @@ def test_search_devices(input, output, aws_session): "deviceStatus": "ONLINE", "providerName": "pname2", }, + { + "deviceArn": "arn4", + "deviceName": "name4", + "deviceType": "QPU", + "deviceStatus": "RETIRED", + "providerName": "pname3", + }, ] } ] @@ -1328,3 +1365,38 @@ def test_add_braket_user_agent(aws_session): aws_session.add_braket_user_agent(user_agent) aws_session.add_braket_user_agent(user_agent) aws_session._braket_user_agents.count(user_agent) == 1 + + +def test_get_full_image_tag(aws_session): + aws_session.ecr_client.batch_get_image.side_effect = ( + {"images": [{"imageId": {"imageDigest": "my-digest"}}]}, + { + "images": [ + {"imageId": {"imageTag": "my-tag"}}, + {"imageId": {"imageTag": "my-tag-py3"}}, + {"imageId": {"imageTag": "my-tag-py310"}}, + {"imageId": {"imageTag": "latest"}}, + ] + }, + AssertionError("Image tag not cached"), + ) + image_uri = "123456.image_uri/repo-name:my-tag" + assert aws_session.get_full_image_tag(image_uri) == "my-tag-py310" + assert aws_session.get_full_image_tag(image_uri) == "my-tag-py310" + + +def test_get_full_image_tag_no_py_info(aws_session): + aws_session.ecr_client.batch_get_image.side_effect = ( + {"images": [{"imageId": {"imageDigest": "my-digest"}}]}, + { + "images": [ + {"imageId": {"imageTag": "my-tag"}}, + {"imageId": {"imageTag": "latest"}}, + ] + }, + ) + image_uri = "123456.image_uri/repo-name:my-tag" + + no_py_info = "Full image tag missing." + with pytest.raises(ValueError, match=no_py_info): + aws_session.get_full_image_tag(image_uri) diff --git a/test/unit_tests/braket/circuits/test_circuit.py b/test/unit_tests/braket/circuits/test_circuit.py index c49b83bfc..4d20c7366 100644 --- a/test/unit_tests/braket/circuits/test_circuit.py +++ b/test/unit_tests/braket/circuits/test_circuit.py @@ -736,8 +736,8 @@ def test_ir_non_empty_instructions_result_types_basis_rotation_instructions(): source="\n".join( [ "OPENQASM 3.0;", - "bit[2] __bits__;", - "qubit[2] __qubits__;", + "bit[2] b;", + "qubit[2] q;", "cal {", " waveform drag_gauss_wf = drag_gaussian" + "(3.0ms, 400.0ms, 0.2, 1, false);", @@ -750,10 +750,10 @@ def test_ir_non_empty_instructions_result_types_basis_rotation_instructions(): " set_frequency(predefined_frame_1, 6000000.0);", " play(predefined_frame_1, drag_gauss_wf);", "}", - "rx(0.15) __qubits__[0];", - "rx(0.3) __qubits__[1];", - "__bits__[0] = measure __qubits__[0];", - "__bits__[1] = measure __qubits__[1];", + "rx(0.15) q[0];", + "rx(0.3) q[1];", + "b[0] = measure q[0];", + "b[1] = measure q[1];", ] ), inputs={}, @@ -766,7 +766,7 @@ def test_ir_non_empty_instructions_result_types_basis_rotation_instructions(): source="\n".join( [ "OPENQASM 3.0;", - "bit[2] __bits__;", + "bit[2] b;", "cal {", " waveform drag_gauss_wf = drag_gaussian" + "(3.0ms, 400.0ms, 0.2, 1, false);", @@ -781,8 +781,8 @@ def test_ir_non_empty_instructions_result_types_basis_rotation_instructions(): "}", "rx(0.15) $0;", "rx(0.3) $4;", - "__bits__[0] = measure $0;", - "__bits__[1] = measure $4;", + "b[0] = measure $0;", + "b[1] = measure $4;", ] ), inputs={}, @@ -832,7 +832,7 @@ def test_ir_non_empty_instructions_result_types_basis_rotation_instructions(): source="\n".join( [ "OPENQASM 3.0;", - "qubit[5] __qubits__;", + "qubit[5] q;", "cal {", " waveform drag_gauss_wf = drag_gaussian" + "(3.0ms, 400.0ms, 0.2, 1, false);", @@ -845,10 +845,10 @@ def test_ir_non_empty_instructions_result_types_basis_rotation_instructions(): " set_frequency(predefined_frame_1, 6000000.0);", " play(predefined_frame_1, drag_gauss_wf);", "}", - "rx(0.15) __qubits__[0];", - "rx(0.3) __qubits__[4];", - "#pragma braket noise bit_flip(0.2) __qubits__[3]", - "#pragma braket result expectation i(__qubits__[0])", + "rx(0.15) q[0];", + "rx(0.3) q[4];", + "#pragma braket noise bit_flip(0.2) q[3]", + "#pragma braket result expectation i(q[0])", ] ), inputs={}, @@ -862,8 +862,8 @@ def test_ir_non_empty_instructions_result_types_basis_rotation_instructions(): [ "OPENQASM 3.0;", "input float theta;", - "bit[2] __bits__;", - "qubit[2] __qubits__;", + "bit[2] b;", + "qubit[2] q;", "cal {", " waveform drag_gauss_wf = drag_gaussian" + "(3.0ms, 400.0ms, 0.2, 1, false);", @@ -876,10 +876,10 @@ def test_ir_non_empty_instructions_result_types_basis_rotation_instructions(): " set_frequency(predefined_frame_1, 6000000.0);", " play(predefined_frame_1, drag_gauss_wf);", "}", - "rx(0.15) __qubits__[0];", - "rx(theta) __qubits__[1];", - "__bits__[0] = measure __qubits__[0];", - "__bits__[1] = measure __qubits__[1];", + "rx(0.15) q[0];", + "rx(theta) q[1];", + "b[0] = measure q[0];", + "b[1] = measure q[1];", ] ), inputs={}, @@ -895,8 +895,8 @@ def test_ir_non_empty_instructions_result_types_basis_rotation_instructions(): source="\n".join( [ "OPENQASM 3.0;", - "bit[5] __bits__;", - "qubit[5] __qubits__;", + "bit[5] b;", + "qubit[5] q;", "cal {", " waveform drag_gauss_wf = drag_gaussian" + "(3.0ms, 400.0ms, 0.2, 1, false);", @@ -909,14 +909,14 @@ def test_ir_non_empty_instructions_result_types_basis_rotation_instructions(): " set_frequency(predefined_frame_1, 6000000.0);", " play(predefined_frame_1, drag_gauss_wf);", "}", - "negctrl @ rx(0.15) __qubits__[2], __qubits__[0];", - "ctrl(2) @ rx(0.3) __qubits__[2], __qubits__[3], __qubits__[1];", - "ctrl(2) @ cnot __qubits__[2], __qubits__[3], __qubits__[4], __qubits__[0];", # noqa - "__bits__[0] = measure __qubits__[0];", - "__bits__[1] = measure __qubits__[1];", - "__bits__[2] = measure __qubits__[2];", - "__bits__[3] = measure __qubits__[3];", - "__bits__[4] = measure __qubits__[4];", + "negctrl @ rx(0.15) q[2], q[0];", + "ctrl(2) @ rx(0.3) q[2], q[3], q[1];", + "ctrl(2) @ cnot q[2], q[3], q[4], q[0];", + "b[0] = measure q[0];", + "b[1] = measure q[1];", + "b[2] = measure q[2];", + "b[3] = measure q[3];", + "b[4] = measure q[4];", ] ), inputs={}, @@ -929,8 +929,8 @@ def test_ir_non_empty_instructions_result_types_basis_rotation_instructions(): source="\n".join( [ "OPENQASM 3.0;", - "bit[7] __bits__;", - "qubit[7] __qubits__;", + "bit[7] b;", + "qubit[7] q;", "cal {", " waveform drag_gauss_wf = drag_gaussian" + "(3.0ms, 400.0ms, 0.2, 1, false);", @@ -939,16 +939,16 @@ def test_ir_non_empty_instructions_result_types_basis_rotation_instructions(): " set_frequency(predefined_frame_1, 6000000.0);", " play(predefined_frame_1, drag_gauss_wf);", "}", - "cnot __qubits__[0], __qubits__[1];", - "cnot __qubits__[3], __qubits__[2];", - "ctrl @ cnot __qubits__[5], __qubits__[6], __qubits__[4];", - "__bits__[0] = measure __qubits__[0];", - "__bits__[1] = measure __qubits__[1];", - "__bits__[2] = measure __qubits__[2];", - "__bits__[3] = measure __qubits__[3];", - "__bits__[4] = measure __qubits__[4];", - "__bits__[5] = measure __qubits__[5];", - "__bits__[6] = measure __qubits__[6];", + "cnot q[0], q[1];", + "cnot q[3], q[2];", + "ctrl @ cnot q[5], q[6], q[4];", + "b[0] = measure q[0];", + "b[1] = measure q[1];", + "b[2] = measure q[2];", + "b[3] = measure q[3];", + "b[4] = measure q[4];", + "b[5] = measure q[5];", + "b[6] = measure q[6];", ] ), inputs={}, @@ -961,8 +961,8 @@ def test_ir_non_empty_instructions_result_types_basis_rotation_instructions(): source="\n".join( [ "OPENQASM 3.0;", - "bit[2] __bits__;", - "qubit[2] __qubits__;", + "bit[2] b;", + "qubit[2] q;", "cal {", " waveform drag_gauss_wf = drag_gaussian" + "(3.0ms, 400.0ms, 0.2, 1, false);", @@ -977,11 +977,11 @@ def test_ir_non_empty_instructions_result_types_basis_rotation_instructions(): " shift_phase(predefined_frame_1, -0.2);", " play(predefined_frame_1, drag_gauss_wf);", "}", - "inv @ pow(2.5) @ h __qubits__[0];", - "pow(0) @ h __qubits__[0];", - "ms(-0.1, -0.2, -0.3) __qubits__[0], __qubits__[1];", - "__bits__[0] = measure __qubits__[0];", - "__bits__[1] = measure __qubits__[1];", + "inv @ pow(2.5) @ h q[0];", + "pow(0) @ h q[0];", + "ms(-0.1, -0.2, -0.3) q[0], q[1];", + "b[0] = measure q[0];", + "b[1] = measure q[1];", ] ), inputs={}, @@ -1030,8 +1030,8 @@ def test_parametric_circuit_with_fixed_argument_defcal(pulse_sequence): [ "OPENQASM 3.0;", "input float theta;", - "bit[1] __bits__;", - "qubit[1] __qubits__;", + "bit[1] b;", + "qubit[1] q;", "cal {", " waveform drag_gauss_wf = drag_gaussian" + "(3.0ms, 400.0ms, 0.2, 1, false);", "}", @@ -1043,10 +1043,10 @@ def test_parametric_circuit_with_fixed_argument_defcal(pulse_sequence): " set_frequency(predefined_frame_1, 6000000.0);", " play(predefined_frame_1, drag_gauss_wf);", "}", - "inv @ pow(2.5) @ h __qubits__[0];", - "pow(0) @ h __qubits__[0];", - "rx(theta) __qubits__[0];", - "__bits__[0] = measure __qubits__[0];", + "inv @ pow(2.5) @ h q[0];", + "pow(0) @ h q[0];", + "rx(theta) q[0];", + "b[0] = measure q[0];", ] ), inputs={}, @@ -1127,8 +1127,8 @@ def foo( source="\n".join( [ "OPENQASM 3.0;", - "bit[1] __bits__;", - "qubit[1] __qubits__;", + "bit[1] b;", + "qubit[1] q;", "cal {", " waveform drag_gauss_wf = drag_gaussian" + "(3.0ms, 400.0ms, 0.2, 1, false);", "}", @@ -1138,8 +1138,8 @@ def foo( " shift_phase(predefined_frame_1, -0.2);", " play(predefined_frame_1, drag_gauss_wf);", "}", - "foo(-0.2) __qubits__[0];", - "__bits__[0] = measure __qubits__[0];", + "foo(-0.2) q[0];", + "b[0] = measure q[0];", ] ), inputs={}, @@ -1164,11 +1164,11 @@ def foo( source="\n".join( [ "OPENQASM 3.0;", - "bit[2] __bits__;", - "qubit[2] __qubits__;", - "negctrl @ h __qubits__[1], __qubits__[0];", - "__bits__[0] = measure __qubits__[0];", - "__bits__[1] = measure __qubits__[1];", + "bit[2] b;", + "qubit[2] q;", + "negctrl @ h q[1], q[0];", + "b[0] = measure q[0];", + "b[1] = measure q[1];", ] ), inputs={}, @@ -1180,11 +1180,11 @@ def foo( source="\n".join( [ "OPENQASM 3.0;", - "bit[2] __bits__;", - "qubit[2] __qubits__;", - "cnot __qubits__[1], __qubits__[0];", - "__bits__[0] = measure __qubits__[0];", - "__bits__[1] = measure __qubits__[1];", + "bit[2] b;", + "qubit[2] q;", + "cnot q[1], q[0];", + "b[0] = measure q[0];", + "b[1] = measure q[1];", ] ), inputs={}, @@ -1196,11 +1196,11 @@ def foo( source="\n".join( [ "OPENQASM 3.0;", - "bit[2] __bits__;", - "qubit[2] __qubits__;", - "negctrl @ x __qubits__[1], __qubits__[0];", - "__bits__[0] = measure __qubits__[0];", - "__bits__[1] = measure __qubits__[1];", + "bit[2] b;", + "qubit[2] q;", + "negctrl @ x q[1], q[0];", + "b[0] = measure q[0];", + "b[1] = measure q[1];", ] ), inputs={}, @@ -1212,11 +1212,11 @@ def foo( source="\n".join( [ "OPENQASM 3.0;", - "bit[2] __bits__;", - "qubit[2] __qubits__;", - "ctrl @ rx(0.15) __qubits__[1], __qubits__[0];", - "__bits__[0] = measure __qubits__[0];", - "__bits__[1] = measure __qubits__[1];", + "bit[2] b;", + "qubit[2] q;", + "ctrl @ rx(0.15) q[1], q[0];", + "b[0] = measure q[0];", + "b[1] = measure q[1];", ] ), inputs={}, @@ -1228,11 +1228,11 @@ def foo( source="\n".join( [ "OPENQASM 3.0;", - "bit[2] __bits__;", - "qubit[2] __qubits__;", - "ctrl @ ry(0.2) __qubits__[1], __qubits__[0];", - "__bits__[0] = measure __qubits__[0];", - "__bits__[1] = measure __qubits__[1];", + "bit[2] b;", + "qubit[2] q;", + "ctrl @ ry(0.2) q[1], q[0];", + "b[0] = measure q[0];", + "b[1] = measure q[1];", ] ), inputs={}, @@ -1244,11 +1244,11 @@ def foo( source="\n".join( [ "OPENQASM 3.0;", - "bit[2] __bits__;", - "qubit[2] __qubits__;", - "negctrl @ rz(0.25) __qubits__[1], __qubits__[0];", - "__bits__[0] = measure __qubits__[0];", - "__bits__[1] = measure __qubits__[1];", + "bit[2] b;", + "qubit[2] q;", + "negctrl @ rz(0.25) q[1], q[0];", + "b[0] = measure q[0];", + "b[1] = measure q[1];", ] ), inputs={}, @@ -1260,11 +1260,11 @@ def foo( source="\n".join( [ "OPENQASM 3.0;", - "bit[2] __bits__;", - "qubit[2] __qubits__;", - "negctrl @ s __qubits__[1], __qubits__[0];", - "__bits__[0] = measure __qubits__[0];", - "__bits__[1] = measure __qubits__[1];", + "bit[2] b;", + "qubit[2] q;", + "negctrl @ s q[1], q[0];", + "b[0] = measure q[0];", + "b[1] = measure q[1];", ] ), inputs={}, @@ -1276,11 +1276,11 @@ def foo( source="\n".join( [ "OPENQASM 3.0;", - "bit[2] __bits__;", - "qubit[2] __qubits__;", - "negctrl @ t __qubits__[0], __qubits__[1];", - "__bits__[0] = measure __qubits__[0];", - "__bits__[1] = measure __qubits__[1];", + "bit[2] b;", + "qubit[2] q;", + "negctrl @ t q[0], q[1];", + "b[0] = measure q[0];", + "b[1] = measure q[1];", ] ), inputs={}, @@ -1292,11 +1292,11 @@ def foo( source="\n".join( [ "OPENQASM 3.0;", - "bit[2] __bits__;", - "qubit[2] __qubits__;", - "cphaseshift(0.15) __qubits__[1], __qubits__[0];", - "__bits__[0] = measure __qubits__[0];", - "__bits__[1] = measure __qubits__[1];", + "bit[2] b;", + "qubit[2] q;", + "cphaseshift(0.15) q[1], q[0];", + "b[0] = measure q[0];", + "b[1] = measure q[1];", ] ), inputs={}, @@ -1308,12 +1308,12 @@ def foo( source="\n".join( [ "OPENQASM 3.0;", - "bit[3] __bits__;", - "qubit[3] __qubits__;", - "ccnot __qubits__[0], __qubits__[1], __qubits__[2];", - "__bits__[0] = measure __qubits__[0];", - "__bits__[1] = measure __qubits__[1];", - "__bits__[2] = measure __qubits__[2];", + "bit[3] b;", + "qubit[3] q;", + "ccnot q[0], q[1], q[2];", + "b[0] = measure q[0];", + "b[1] = measure q[1];", + "b[2] = measure q[2];", ] ), inputs={}, @@ -1325,8 +1325,8 @@ def foo( source="\n".join( [ "OPENQASM 3.0;", - "qubit[1] __qubits__;", - "h __qubits__[0];", + "qubit[1] q;", + "h q[0];", "#pragma braket result state_vector", ] ), @@ -1339,9 +1339,9 @@ def foo( source="\n".join( [ "OPENQASM 3.0;", - "qubit[1] __qubits__;", - "h __qubits__[0];", - "#pragma braket result expectation x(__qubits__[0])", + "qubit[1] q;", + "h q[0];", + "#pragma braket result expectation x(q[0])", ] ), inputs={}, @@ -1353,9 +1353,9 @@ def foo( source="\n".join( [ "OPENQASM 3.0;", - "qubit[2] __qubits__;", - "h __qubits__[0];", - "#pragma braket result expectation h(__qubits__[0]) @ x(__qubits__[1])", + "qubit[2] q;", + "h q[0];", + "#pragma braket result expectation h(q[0]) @ x(q[1])", ] ), inputs={}, @@ -1367,9 +1367,9 @@ def foo( source="\n".join( [ "OPENQASM 3.0;", - "qubit[2] __qubits__;", - "h __qubits__[0];", - "#pragma braket result variance h(__qubits__[0]) @ x(__qubits__[1])", + "qubit[2] q;", + "h q[0];", + "#pragma braket result variance h(q[0]) @ x(q[1])", ] ), inputs={}, @@ -1381,9 +1381,9 @@ def foo( source="\n".join( [ "OPENQASM 3.0;", - "qubit[1] __qubits__;", - "h __qubits__[0];", - "#pragma braket result probability __qubits__[0]", + "qubit[1] q;", + "h q[0];", + "#pragma braket result probability q[0]", ] ), inputs={}, @@ -1395,10 +1395,10 @@ def foo( source="\n".join( [ "OPENQASM 3.0;", - "bit[1] __bits__;", - "qubit[1] __qubits__;", - "#pragma braket noise bit_flip(0.1) __qubits__[0]", - "__bits__[0] = measure __qubits__[0];", + "bit[1] b;", + "qubit[1] q;", + "#pragma braket noise bit_flip(0.1) q[0]", + "b[0] = measure q[0];", ] ), inputs={}, @@ -1410,10 +1410,10 @@ def foo( source="\n".join( [ "OPENQASM 3.0;", - "bit[1] __bits__;", - "qubit[1] __qubits__;", - "#pragma braket noise generalized_amplitude_damping(0.1, 0.1) __qubits__[0]", # noqa - "__bits__[0] = measure __qubits__[0];", + "bit[1] b;", + "qubit[1] q;", + "#pragma braket noise generalized_amplitude_damping(0.1, 0.1) q[0]", + "b[0] = measure q[0];", ] ), inputs={}, @@ -1425,10 +1425,10 @@ def foo( source="\n".join( [ "OPENQASM 3.0;", - "bit[1] __bits__;", - "qubit[1] __qubits__;", - "#pragma braket noise phase_flip(0.2) __qubits__[0]", - "__bits__[0] = measure __qubits__[0];", + "bit[1] b;", + "qubit[1] q;", + "#pragma braket noise phase_flip(0.2) q[0]", + "b[0] = measure q[0];", ] ), inputs={}, @@ -1440,10 +1440,10 @@ def foo( source="\n".join( [ "OPENQASM 3.0;", - "bit[1] __bits__;", - "qubit[1] __qubits__;", - "#pragma braket noise depolarizing(0.5) __qubits__[0]", - "__bits__[0] = measure __qubits__[0];", + "bit[1] b;", + "qubit[1] q;", + "#pragma braket noise depolarizing(0.5) q[0]", + "b[0] = measure q[0];", ] ), inputs={}, @@ -1455,10 +1455,10 @@ def foo( source="\n".join( [ "OPENQASM 3.0;", - "bit[1] __bits__;", - "qubit[1] __qubits__;", - "#pragma braket noise amplitude_damping(0.8) __qubits__[0]", - "__bits__[0] = measure __qubits__[0];", + "bit[1] b;", + "qubit[1] q;", + "#pragma braket noise amplitude_damping(0.8) q[0]", + "b[0] = measure q[0];", ] ), inputs={}, @@ -1470,10 +1470,10 @@ def foo( source="\n".join( [ "OPENQASM 3.0;", - "bit[1] __bits__;", - "qubit[1] __qubits__;", - "#pragma braket noise phase_damping(0.1) __qubits__[0]", - "__bits__[0] = measure __qubits__[0];", + "bit[1] b;", + "qubit[1] q;", + "#pragma braket noise phase_damping(0.1) q[0]", + "b[0] = measure q[0];", ] ), inputs={}, @@ -1485,8 +1485,8 @@ def foo( source="\n".join( [ "OPENQASM 3.0;", - "qubit[1] __qubits__;", - "h __qubits__[0];", + "qubit[1] q;", + "h q[0];", '#pragma braket result amplitude "0", "1"', ] ), @@ -1502,16 +1502,16 @@ def foo( source="\n".join( [ "OPENQASM 3.0;", - "bit[5] __bits__;", - "qubit[5] __qubits__;", - "negctrl @ rx(0.15) __qubits__[2], __qubits__[0];", - "ctrl(2) @ rx(0.3) __qubits__[2], __qubits__[3], __qubits__[1];", - "ctrl(2) @ cnot __qubits__[2], __qubits__[3], __qubits__[4], __qubits__[0];", # noqa - "__bits__[0] = measure __qubits__[0];", - "__bits__[1] = measure __qubits__[1];", - "__bits__[2] = measure __qubits__[2];", - "__bits__[3] = measure __qubits__[3];", - "__bits__[4] = measure __qubits__[4];", + "bit[5] b;", + "qubit[5] q;", + "negctrl @ rx(0.15) q[2], q[0];", + "ctrl(2) @ rx(0.3) q[2], q[3], q[1];", + "ctrl(2) @ cnot q[2], q[3], q[4], q[0];", + "b[0] = measure q[0];", + "b[1] = measure q[1];", + "b[2] = measure q[2];", + "b[3] = measure q[3];", + "b[4] = measure q[4];", ] ), inputs={}, @@ -1523,18 +1523,18 @@ def foo( source="\n".join( [ "OPENQASM 3.0;", - "bit[7] __bits__;", - "qubit[7] __qubits__;", - "cnot __qubits__[0], __qubits__[1];", - "cnot __qubits__[3], __qubits__[2];", - "ctrl @ cnot __qubits__[5], __qubits__[6], __qubits__[4];", - "__bits__[0] = measure __qubits__[0];", - "__bits__[1] = measure __qubits__[1];", - "__bits__[2] = measure __qubits__[2];", - "__bits__[3] = measure __qubits__[3];", - "__bits__[4] = measure __qubits__[4];", - "__bits__[5] = measure __qubits__[5];", - "__bits__[6] = measure __qubits__[6];", + "bit[7] b;", + "qubit[7] q;", + "cnot q[0], q[1];", + "cnot q[3], q[2];", + "ctrl @ cnot q[5], q[6], q[4];", + "b[0] = measure q[0];", + "b[1] = measure q[1];", + "b[2] = measure q[2];", + "b[3] = measure q[3];", + "b[4] = measure q[4];", + "b[5] = measure q[5];", + "b[6] = measure q[6];", ] ), inputs={}, @@ -1546,11 +1546,11 @@ def foo( source="\n".join( [ "OPENQASM 3.0;", - "bit[1] __bits__;", - "qubit[1] __qubits__;", - "inv @ pow(2.5) @ h __qubits__[0];", - "pow(0) @ h __qubits__[0];", - "__bits__[0] = measure __qubits__[0];", + "bit[1] b;", + "qubit[1] q;", + "inv @ pow(2.5) @ h q[0];", + "pow(0) @ h q[0];", + "b[0] = measure q[0];", ] ), inputs={}, @@ -1562,10 +1562,10 @@ def foo( source="\n".join( [ "OPENQASM 3.0;", - "bit[1] __bits__;", - "qubit[1] __qubits__;", - "#pragma braket unitary([[0, 1.0], [1.0, 0]]) __qubits__[0]", - "__bits__[0] = measure __qubits__[0];", + "bit[1] b;", + "qubit[1] q;", + "#pragma braket unitary([[0, 1.0], [1.0, 0]]) q[0]", + "b[0] = measure q[0];", ] ), inputs={}, @@ -1577,10 +1577,10 @@ def foo( source="\n".join( [ "OPENQASM 3.0;", - "bit[1] __bits__;", - "qubit[1] __qubits__;", - "#pragma braket noise pauli_channel(0.1, 0.2, 0.3) __qubits__[0]", - "__bits__[0] = measure __qubits__[0];", + "bit[1] b;", + "qubit[1] q;", + "#pragma braket noise pauli_channel(0.1, 0.2, 0.3) q[0]", + "b[0] = measure q[0];", ] ), inputs={}, @@ -1592,11 +1592,11 @@ def foo( source="\n".join( [ "OPENQASM 3.0;", - "bit[2] __bits__;", - "qubit[2] __qubits__;", - "#pragma braket noise two_qubit_depolarizing(0.1) __qubits__[0], __qubits__[1]", # noqa - "__bits__[0] = measure __qubits__[0];", - "__bits__[1] = measure __qubits__[1];", + "bit[2] b;", + "qubit[2] q;", + "#pragma braket noise two_qubit_depolarizing(0.1) q[0], q[1]", + "b[0] = measure q[0];", + "b[1] = measure q[1];", ] ), inputs={}, @@ -1608,11 +1608,11 @@ def foo( source="\n".join( [ "OPENQASM 3.0;", - "bit[2] __bits__;", - "qubit[2] __qubits__;", - "#pragma braket noise two_qubit_dephasing(0.1) __qubits__[0], __qubits__[1]", # noqa - "__bits__[0] = measure __qubits__[0];", - "__bits__[1] = measure __qubits__[1];", + "bit[2] b;", + "qubit[2] q;", + "#pragma braket noise two_qubit_dephasing(0.1) q[0], q[1]", + "b[0] = measure q[0];", + "b[1] = measure q[1];", ] ), inputs={}, @@ -1624,11 +1624,11 @@ def foo( source="\n".join( [ "OPENQASM 3.0;", - "bit[2] __bits__;", - "qubit[2] __qubits__;", - "#pragma braket noise two_qubit_dephasing(0.1) __qubits__[0], __qubits__[1]", # noqa - "__bits__[0] = measure __qubits__[0];", - "__bits__[1] = measure __qubits__[1];", + "bit[2] b;", + "qubit[2] q;", + "#pragma braket noise two_qubit_dephasing(0.1) q[0], q[1]", + "b[0] = measure q[0];", + "b[1] = measure q[1];", ] ), inputs={}, @@ -1640,9 +1640,9 @@ def foo( source="\n".join( [ "OPENQASM 3.0;", - "qubit[1] __qubits__;", - "h __qubits__[0];", - "#pragma braket result sample z(__qubits__[0])", + "qubit[1] q;", + "h q[0];", + "#pragma braket result sample z(q[0])", ] ), inputs={}, @@ -1654,9 +1654,9 @@ def foo( source="\n".join( [ "OPENQASM 3.0;", - "qubit[1] __qubits__;", - "h __qubits__[0];", - "#pragma braket result sample z(__qubits__[0])", + "qubit[1] q;", + "h q[0];", + "#pragma braket result sample z(q[0])", ] ), inputs={}, @@ -1668,10 +1668,10 @@ def foo( source="\n".join( [ "OPENQASM 3.0;", - "qubit[2] __qubits__;", - "h __qubits__[0];", - "x __qubits__[1];", - "#pragma braket result density_matrix __qubits__[0], __qubits__[1]", + "qubit[2] q;", + "h q[0];", + "x q[1];", + "#pragma braket result density_matrix q[0], q[1]", ] ), inputs={}, @@ -1689,12 +1689,12 @@ def foo( source="\n".join( [ "OPENQASM 3.0;", - "bit[1] __bits__;", - "qubit[1] __qubits__;", + "bit[1] b;", + "qubit[1] q;", "#pragma braket noise " "kraus([[0.9486833im, 0], [0, 0.9486833im]], [[0, 0.31622777], " - "[0.31622777, 0]]) __qubits__[0]", - "__bits__[0] = measure __qubits__[0];", + "[0.31622777, 0]]) q[0]", + "b[0] = measure q[0];", ] ), inputs={}, @@ -1707,10 +1707,10 @@ def foo( [ "OPENQASM 3.0;", "input float theta;", - "bit[1] __bits__;", - "qubit[1] __qubits__;", - "rx(theta) __qubits__[0];", - "__bits__[0] = measure __qubits__[0];", + "bit[1] b;", + "qubit[1] q;", + "rx(theta) q[0];", + "b[0] = measure q[0];", ] ), inputs={}, @@ -1731,11 +1731,11 @@ def test_from_ir_inputs_updated(): "OPENQASM 3.0;", "input float theta;", "input float phi;", - "bit[1] __bits__;", - "qubit[1] __qubits__;", - "rx(theta) __qubits__[0];", - "ry(phi) __qubits__[0];", - "__bits__[0] = measure __qubits__[0];", + "bit[1] b;", + "qubit[1] q;", + "rx(theta) q[0];", + "ry(phi) q[0];", + "b[0] = measure q[0];", ] ), inputs={"theta": 0.2, "phi": 0.3}, @@ -1752,15 +1752,15 @@ def test_from_ir_inputs_updated(): source="\n".join( [ "OPENQASM 3.0;", - "bit[2] __bits__;", - "qubit[2] __qubits__;", + "bit[2] b;", + "qubit[2] q;", "gate my_gate a,b {", "h a;", - "cnot a, b;", + "cnot a,b;", "}", - "my_gate __qubits__[0], __qubits__[1];", - "__bits__[0] = measure __qubits__[0];", - "__bits__[1] = measure __qubits__[1];", + "my_gate q[0], q[1];", + "b[0] = measure q[0];", + "b[1] = measure q[1];", ] ), inputs={}, @@ -1772,15 +1772,15 @@ def test_from_ir_inputs_updated(): source="\n".join( [ "OPENQASM 3.0;", - "bit[2] __bits__;", - "qubit[2] __qubits__;", + "bit[2] b;", + "qubit[2] q;", "def my_sub(qubit q) {", "h q;", "}", - "h __qubits__[0];", - "my_sub(__qubits__[1]);", - "__bits__[0] = measure __qubits__[0];", - "__bits__[1] = measure __qubits__[1];", + "h q[0];", + "my_sub(q[1]);", + "b[0] = measure q[0];", + "b[1] = measure q[1];", ] ), inputs={}, @@ -1792,14 +1792,14 @@ def test_from_ir_inputs_updated(): source="\n".join( [ "OPENQASM 3.0;", - "bit[2] __bits__;", - "qubit[2] __qubits__;", + "bit[2] b;", + "qubit[2] q;", "for uint i in [0:1] {", - "h __qubits__[i];", + "h q[i];", "}", - "cnot __qubits__[0], __qubits__[1];", - "__bits__[0] = measure __qubits__[0];", - "__bits__[1] = measure __qubits__[1];", + "cnot q[0], q[1];", + "b[0] = measure q[0];", + "b[1] = measure q[1];", ] ), inputs={}, @@ -1811,14 +1811,14 @@ def test_from_ir_inputs_updated(): source="\n".join( [ "OPENQASM 3.0;", - "bit[2] __bits__;", - "qubit[2] __qubits__;", + "bit[2] b;", + "qubit[2] q;", "for uint i in [0:1] {", - "h __qubits__[i];", + "h q[i];", "}", - "cnot __qubits__[0], __qubits__[1];", - "__bits__[0] = measure __qubits__[0];", - "__bits__[1] = measure __qubits__[1];", + "cnot q[0], q[1];", + "b[0] = measure q[0];", + "b[1] = measure q[1];", ] ), inputs={}, @@ -1830,13 +1830,13 @@ def test_from_ir_inputs_updated(): source="\n".join( [ "OPENQASM 3.0;", - "bit[1] __bits__;", - "qubit[1] __qubits__;", + "bit[1] b;", + "qubit[1] q;", "bit c = 0;", "if (c ==0){", - "x __qubits__[0];", + "x q[0];", "}", - "__bits__[0] = measure __qubits__[0];", + "b[0] = measure q[0];", ] ), inputs={}, @@ -1848,11 +1848,11 @@ def test_from_ir_inputs_updated(): source="\n".join( [ "OPENQASM 3.0;", - "input float theta;" "bit[1] __bits__;", - "qubit[1] __qubits__;", - "rx(theta) __qubits__[0];", - "rx(2*theta) __qubits__[0];", - "__bits__[0] = measure __qubits__[0];", + "input float theta;" "bit[1] b;", + "qubit[1] q;", + "rx(theta) q[0];", + "rx(2*theta) q[0];", + "b[0] = measure q[0];", ] ), inputs={}, @@ -1904,419 +1904,6 @@ def test_circuit_to_ir_invalid_inputs( assert exc.value.args[0] == expected_message -def test_as_unitary_empty_instructions_returns_empty_array(): - circ = Circuit() - circ.as_unitary() == [] - - -@pytest.mark.parametrize( - "circuit", - [ - (Circuit().phaseshift(0, 0.15).apply_gate_noise(noise.Noise.BitFlip(probability=0.1))), - (Circuit().cnot(1, 0).apply_gate_noise(noise.Noise.TwoQubitDepolarizing(probability=0.1))), - ( - Circuit() - .x(1) - .i(2) - .apply_gate_noise(noise.Noise.BitFlip(probability=0.1), target_qubits=[1]) - ), - ( - Circuit() - .x(1) - .i(2) - .apply_gate_noise(noise.Noise.BitFlip(probability=0.1), target_qubits=[2]) - ), - (Circuit().x(1).i(2).apply_gate_noise(noise.Noise.BitFlip(probability=0.1))), - (Circuit().x(1).apply_gate_noise(noise.Noise.BitFlip(probability=0.1)).i(2)), - ( - Circuit() - .y(1) - .z(2) - .apply_gate_noise(noise.Noise.BitFlip(probability=0.1), target_qubits=[1]) - ), - ( - Circuit() - .y(1) - .z(2) - .apply_gate_noise(noise.Noise.BitFlip(probability=0.1), target_qubits=[2]) - ), - (Circuit().y(1).z(2).apply_gate_noise(noise.Noise.BitFlip(probability=0.1))), - (Circuit().y(1).apply_gate_noise(noise.Noise.BitFlip(probability=0.1)).z(2)), - ( - Circuit() - .cphaseshift(2, 1, 0.15) - .si(3) - .apply_gate_noise( - noise.Noise.TwoQubitDepolarizing(probability=0.1), target_qubits=[1, 2] - ) - ), - ( - Circuit() - .cphaseshift(2, 1, 0.15) - .apply_gate_noise(noise.Noise.TwoQubitDepolarizing(probability=0.1)) - .si(3) - ), - ], -) -def test_as_unitary_noise_raises_error(circuit): - with pytest.raises(TypeError): - circuit.as_unitary() - - -def test_as_unitary_parameterized(): - theta = FreeParameter("theta") - circ = Circuit().rx(angle=theta, target=0) - with pytest.raises(TypeError): - assert np.allclose(circ.as_unitary()) - - -def test_as_unitary_noise_not_apply_returns_expected_unitary(recwarn): - circuit = ( - Circuit() - .cphaseshift(2, 1, 0.15) - .si(3) - .apply_gate_noise(noise.Noise.TwoQubitDepolarizing(probability=0.1), target_qubits=[1, 3]) - ) - - assert len(recwarn) == 1 - assert str(recwarn[0].message).startswith("Noise is not applied to any gate") - - assert np.allclose( - circuit.as_unitary(), - np.kron(gates.Si().to_matrix(), np.kron(gates.CPhaseShift(0.15).to_matrix(), np.eye(2))), - ) - - -def test_as_unitary_with_compiler_directives_returns_expected_unitary(): - circuit = Circuit().add_verbatim_box(Circuit().cphaseshift(2, 1, 0.15).si(3)) - assert np.allclose( - circuit.as_unitary(), - np.kron(gates.Si().to_matrix(), np.kron(gates.CPhaseShift(0.15).to_matrix(), np.eye(2))), - ) - - -@pytest.mark.parametrize( - "circuit,expected_unitary", - [ - (Circuit().h(0), gates.H().to_matrix()), - (Circuit().h(0).add_result_type(ResultType.Probability(target=[0])), gates.H().to_matrix()), - (Circuit().x(0), gates.X().to_matrix()), - (Circuit().y(0), gates.Y().to_matrix()), - (Circuit().z(0), gates.Z().to_matrix()), - (Circuit().s(0), gates.S().to_matrix()), - (Circuit().si(0), gates.Si().to_matrix()), - (Circuit().t(0), gates.T().to_matrix()), - (Circuit().ti(0), gates.Ti().to_matrix()), - (Circuit().v(0), gates.V().to_matrix()), - (Circuit().vi(0), gates.Vi().to_matrix()), - (Circuit().rx(0, 0.15), gates.Rx(0.15).to_matrix()), - (Circuit().ry(0, 0.15), gates.Ry(0.15).to_matrix()), - (Circuit().rz(0, 0.15), gates.Rz(0.15).to_matrix()), - (Circuit().phaseshift(0, 0.15), gates.PhaseShift(0.15).to_matrix()), - (Circuit().cnot(1, 0), gates.CNot().to_matrix()), - (Circuit().cnot(1, 0).add_result_type(ResultType.StateVector()), gates.CNot().to_matrix()), - (Circuit().swap(1, 0), gates.Swap().to_matrix()), - (Circuit().swap(0, 1), gates.Swap().to_matrix()), - (Circuit().iswap(1, 0), gates.ISwap().to_matrix()), - (Circuit().iswap(0, 1), gates.ISwap().to_matrix()), - (Circuit().pswap(1, 0, 0.15), gates.PSwap(0.15).to_matrix()), - (Circuit().pswap(0, 1, 0.15), gates.PSwap(0.15).to_matrix()), - (Circuit().xy(1, 0, 0.15), gates.XY(0.15).to_matrix()), - (Circuit().xy(0, 1, 0.15), gates.XY(0.15).to_matrix()), - (Circuit().cphaseshift(1, 0, 0.15), gates.CPhaseShift(0.15).to_matrix()), - (Circuit().cphaseshift00(1, 0, 0.15), gates.CPhaseShift00(0.15).to_matrix()), - (Circuit().cphaseshift01(1, 0, 0.15), gates.CPhaseShift01(0.15).to_matrix()), - (Circuit().cphaseshift10(1, 0, 0.15), gates.CPhaseShift10(0.15).to_matrix()), - (Circuit().cy(1, 0), gates.CY().to_matrix()), - (Circuit().cz(1, 0), gates.CZ().to_matrix()), - (Circuit().xx(1, 0, 0.15), gates.XX(0.15).to_matrix()), - (Circuit().yy(1, 0, 0.15), gates.YY(0.15).to_matrix()), - (Circuit().zz(1, 0, 0.15), gates.ZZ(0.15).to_matrix()), - (Circuit().ccnot(2, 1, 0), gates.CCNot().to_matrix()), - ( - Circuit() - .ccnot(2, 1, 0) - .add_result_type(ResultType.Expectation(observable=Observable.Y(), target=[1])), - gates.CCNot().to_matrix(), - ), - (Circuit().ccnot(1, 2, 0), gates.CCNot().to_matrix()), - (Circuit().cswap(2, 1, 0), gates.CSwap().to_matrix()), - (Circuit().cswap(2, 0, 1), gates.CSwap().to_matrix()), - (Circuit().h(1), np.kron(gates.H().to_matrix(), np.eye(2))), - (Circuit().x(1).i(2), np.kron(np.eye(2), np.kron(gates.X().to_matrix(), np.eye(2)))), - ( - Circuit().y(1).z(2), - np.kron(gates.Z().to_matrix(), np.kron(gates.Y().to_matrix(), np.eye(2))), - ), - (Circuit().rx(1, 0.15), np.kron(gates.Rx(0.15).to_matrix(), np.eye(2))), - ( - Circuit().ry(1, 0.15).i(2), - np.kron(np.eye(2), np.kron(gates.Ry(0.15).to_matrix(), np.eye(2))), - ), - ( - Circuit().rz(1, 0.15).s(2), - np.kron(gates.S().to_matrix(), np.kron(gates.Rz(0.15).to_matrix(), np.eye(2))), - ), - (Circuit().pswap(2, 1, 0.15), np.kron(gates.PSwap(0.15).to_matrix(), np.eye(2))), - (Circuit().pswap(1, 2, 0.15), np.kron(gates.PSwap(0.15).to_matrix(), np.eye(2))), - ( - Circuit().xy(2, 1, 0.15).i(3), - np.kron(np.eye(2), np.kron(gates.XY(0.15).to_matrix(), np.eye(2))), - ), - ( - Circuit().xy(1, 2, 0.15).i(3), - np.kron(np.eye(2), np.kron(gates.XY(0.15).to_matrix(), np.eye(2))), - ), - ( - Circuit().cphaseshift(2, 1, 0.15).si(3), - np.kron( - gates.Si().to_matrix(), np.kron(gates.CPhaseShift(0.15).to_matrix(), np.eye(2)) - ), - ), - (Circuit().ccnot(3, 2, 1), np.kron(gates.CCNot().to_matrix(), np.eye(2))), - (Circuit().ccnot(2, 3, 1), np.kron(gates.CCNot().to_matrix(), np.eye(2))), - ( - Circuit().cswap(3, 2, 1).i(4), - np.kron(np.eye(2), np.kron(gates.CSwap().to_matrix(), np.eye(2))), - ), - ( - Circuit().cswap(3, 1, 2).i(4), - np.kron(np.eye(2), np.kron(gates.CSwap().to_matrix(), np.eye(2))), - ), - ( - Circuit().cswap(3, 2, 1).t(4), - np.kron(gates.T().to_matrix(), np.kron(gates.CSwap().to_matrix(), np.eye(2))), - ), - ( - Circuit().cswap(3, 1, 2).t(4), - np.kron(gates.T().to_matrix(), np.kron(gates.CSwap().to_matrix(), np.eye(2))), - ), - (Circuit().h(0).h(0), gates.I().to_matrix()), - (Circuit().h(0).x(0), np.dot(gates.X().to_matrix(), gates.H().to_matrix())), - (Circuit().x(0).h(0), np.dot(gates.H().to_matrix(), gates.X().to_matrix())), - ( - Circuit().y(0).z(1).cnot(1, 0), - np.dot(gates.CNot().to_matrix(), np.kron(gates.Z().to_matrix(), gates.Y().to_matrix())), - ), - ( - Circuit().z(0).y(1).cnot(1, 0), - np.dot(gates.CNot().to_matrix(), np.kron(gates.Y().to_matrix(), gates.Z().to_matrix())), - ), - ( - Circuit().z(0).y(1).cnot(1, 0).cnot(2, 1), - np.dot( - np.dot( - np.dot( - np.kron(gates.CNot().to_matrix(), np.eye(2)), - np.kron(np.eye(2), gates.CNot().to_matrix()), - ), - np.kron(np.kron(np.eye(2), gates.Y().to_matrix()), np.eye(2)), - ), - np.kron(np.eye(4), gates.Z().to_matrix()), - ), - ), - ( - Circuit().z(0).y(1).cnot(1, 0).ccnot(2, 1, 0), - np.dot( - np.dot( - np.dot( - gates.CCNot().to_matrix(), - np.kron(np.eye(2), gates.CNot().to_matrix()), - ), - np.kron(np.kron(np.eye(2), gates.Y().to_matrix()), np.eye(2)), - ), - np.kron(np.eye(4), gates.Z().to_matrix()), - ), - ), - ( - Circuit().cnot(0, 1), - np.array( - [ - [1.0, 0.0, 0.0, 0.0], - [0.0, 0.0, 0.0, 1.0], - [0.0, 0.0, 1.0, 0.0], - [0.0, 1.0, 0.0, 0.0], - ], - dtype=complex, - ), - ), - ( - Circuit().ccnot(0, 1, 2), - np.array( - [ - [1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], - [0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], - [0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0], - [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0], - [0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0], - [0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0], - [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0], - [0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0], - ], - dtype=complex, - ), - ), - ( - Circuit().ccnot(1, 0, 2), - np.array( - [ - [1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], - [0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], - [0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0], - [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0], - [0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0], - [0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0], - [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0], - [0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0], - ], - dtype=complex, - ), - ), - ( - Circuit().ccnot(0, 2, 1), - np.array( - [ - [1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], - [0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], - [0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0], - [0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0], - [0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0], - [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0], - [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0], - [0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0], - ], - dtype=complex, - ), - ), - ( - Circuit().ccnot(2, 0, 1), - np.array( - [ - [1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], - [0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], - [0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0], - [0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0], - [0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0], - [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0], - [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0], - [0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0], - ], - dtype=complex, - ), - ), - ( - Circuit().s(0).v(1).cnot(0, 1).cnot(1, 2), - np.dot( - np.dot( - np.dot( - np.kron( - np.array( - [ - [1.0, 0.0, 0.0, 0.0], - [0.0, 0.0, 0.0, 1.0], - [0.0, 0.0, 1.0, 0.0], - [0.0, 1.0, 0.0, 0.0], - ], - dtype=complex, - ), - np.eye(2), - ), - np.kron( - np.eye(2), - np.array( - [ - [1.0, 0.0, 0.0, 0.0], - [0.0, 0.0, 0.0, 1.0], - [0.0, 0.0, 1.0, 0.0], - [0.0, 1.0, 0.0, 0.0], - ], - dtype=complex, - ), - ), - ), - np.kron(np.kron(np.eye(2), gates.V().to_matrix()), np.eye(2)), - ), - np.kron(np.eye(4), gates.S().to_matrix()), - ), - ), - ( - Circuit().z(0).y(1).cnot(0, 1).ccnot(0, 1, 2), - np.dot( - np.dot( - np.dot( - np.array( - [ - [1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], - [0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], - [0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0], - [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0], - [0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0], - [0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0], - [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0], - [0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0], - ], - dtype=complex, - ), - np.kron( - np.eye(2), - np.array( - [ - [1.0, 0.0, 0.0, 0.0], - [0.0, 0.0, 0.0, 1.0], - [0.0, 0.0, 1.0, 0.0], - [0.0, 1.0, 0.0, 0.0], - ], - dtype=complex, - ), - ), - ), - np.kron(np.kron(np.eye(2), gates.Y().to_matrix()), np.eye(2)), - ), - np.kron(np.eye(4), gates.Z().to_matrix()), - ), - ), - ( - Circuit().z(0).y(1).cnot(0, 1).ccnot(2, 0, 1), - np.dot( - np.dot( - np.dot( - np.array( - [ - [1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], - [0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], - [0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0], - [0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0], - [0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0], - [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0], - [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0], - [0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0], - ], - dtype=complex, - ), - np.kron( - np.eye(2), - np.array( - [ - [1.0, 0.0, 0.0, 0.0], - [0.0, 0.0, 0.0, 1.0], - [0.0, 0.0, 1.0, 0.0], - [0.0, 1.0, 0.0, 0.0], - ], - dtype=complex, - ), - ), - ), - np.kron(np.kron(np.eye(2), gates.Y().to_matrix()), np.eye(2)), - ), - np.kron(np.eye(4), gates.Z().to_matrix()), - ), - ), - ], -) -def test_as_unitary_one_gate_returns_expected_unitary(circuit, expected_unitary): - assert np.allclose(circuit.as_unitary(), expected_unitary) - - def test_to_unitary_empty_instructions_returns_empty_array(): circ = Circuit() circ.to_unitary() == [] @@ -3355,7 +2942,7 @@ def test_pulse_circuit_to_openqasm(predefined_frame_1, user_defined_frame): ).source == "\n".join( [ "OPENQASM 3.0;", - "bit[2] __bits__;", + "bit[2] b;", "cal {", " frame user_defined_frame_0 = newframe(device_port_x0, 10000000.0, 3.14);", " waveform gauss_wf = gaussian(1.0ms, 700.0ms, 1, false);", @@ -3375,8 +2962,8 @@ def test_pulse_circuit_to_openqasm(predefined_frame_1, user_defined_frame): " play(predefined_frame_1, drag_gauss_wf_2);", "}", "h $1;", - "__bits__[0] = measure $0;", - "__bits__[1] = measure $1;", + "b[0] = measure $0;", + "b[1] = measure $1;", ] ) @@ -3470,7 +3057,7 @@ def test_parametrized_pulse_circuit(user_defined_frame): [ "OPENQASM 3.0;", "input float frequency;", - "bit[2] __bits__;", + "bit[2] b;", "cal {", " frame user_defined_frame_0 = newframe(device_port_x0, 10000000.0, 3.14);", " waveform gauss_wf = gaussian(10.0us, 700.0ms, 1, false);", @@ -3480,8 +3067,8 @@ def test_parametrized_pulse_circuit(user_defined_frame): " set_frequency(user_defined_frame_0, frequency);", " play(user_defined_frame_0, gauss_wf);", "}", - "__bits__[0] = measure $0;", - "__bits__[1] = measure $1;", + "b[0] = measure $0;", + "b[1] = measure $1;", ] ) @@ -3495,7 +3082,7 @@ def test_parametrized_pulse_circuit(user_defined_frame): ).source == "\n".join( [ "OPENQASM 3.0;", - "bit[2] __bits__;", + "bit[2] b;", "cal {", " frame user_defined_frame_0 = newframe(device_port_x0, 10000000.0, 3.14);", " waveform gauss_wf = gaussian(10.0us, 700.0ms, 1, false);", @@ -3505,8 +3092,8 @@ def test_parametrized_pulse_circuit(user_defined_frame): " set_frequency(user_defined_frame_0, 10000000.0);", " play(user_defined_frame_0, gauss_wf);", "}", - "__bits__[0] = measure $0;", - "__bits__[1] = measure $1;", + "b[0] = measure $0;", + "b[1] = measure $1;", ] ) diff --git a/test/unit_tests/braket/circuits/test_gates.py b/test/unit_tests/braket/circuits/test_gates.py index 61f890f84..23483486b 100644 --- a/test/unit_tests/braket/circuits/test_gates.py +++ b/test/unit_tests/braket/circuits/test_gates.py @@ -306,7 +306,7 @@ def test_ir_gate_level(testclass, subroutine_name, irclass, irsubclasses, kwargs Gate.Rx(angle=0.17), [4], OpenQASMSerializationProperties(qubit_reference_type=QubitReferenceType.VIRTUAL), - "rx(0.17) __qubits__[4];", + "rx(0.17) q[4];", ), ( Gate.Rx(angle=0.17), @@ -318,7 +318,7 @@ def test_ir_gate_level(testclass, subroutine_name, irclass, irsubclasses, kwargs Gate.X(), [4], OpenQASMSerializationProperties(qubit_reference_type=QubitReferenceType.VIRTUAL), - "x __qubits__[4];", + "x q[4];", ), ( Gate.X(), @@ -330,7 +330,7 @@ def test_ir_gate_level(testclass, subroutine_name, irclass, irsubclasses, kwargs Gate.Z(), [4], OpenQASMSerializationProperties(qubit_reference_type=QubitReferenceType.VIRTUAL), - "z __qubits__[4];", + "z q[4];", ), ( Gate.Z(), @@ -342,7 +342,7 @@ def test_ir_gate_level(testclass, subroutine_name, irclass, irsubclasses, kwargs Gate.Y(), [4], OpenQASMSerializationProperties(qubit_reference_type=QubitReferenceType.VIRTUAL), - "y __qubits__[4];", + "y q[4];", ), ( Gate.Y(), @@ -354,7 +354,7 @@ def test_ir_gate_level(testclass, subroutine_name, irclass, irsubclasses, kwargs Gate.H(), [4], OpenQASMSerializationProperties(qubit_reference_type=QubitReferenceType.VIRTUAL), - "h __qubits__[4];", + "h q[4];", ), ( Gate.H(), @@ -366,7 +366,7 @@ def test_ir_gate_level(testclass, subroutine_name, irclass, irsubclasses, kwargs Gate.Ry(angle=0.17), [4], OpenQASMSerializationProperties(qubit_reference_type=QubitReferenceType.VIRTUAL), - "ry(0.17) __qubits__[4];", + "ry(0.17) q[4];", ), ( Gate.Ry(angle=0.17), @@ -378,7 +378,7 @@ def test_ir_gate_level(testclass, subroutine_name, irclass, irsubclasses, kwargs Gate.ZZ(angle=0.17), [4, 5], OpenQASMSerializationProperties(qubit_reference_type=QubitReferenceType.VIRTUAL), - "zz(0.17) __qubits__[4], __qubits__[5];", + "zz(0.17) q[4], q[5];", ), ( Gate.ZZ(angle=0.17), @@ -390,7 +390,7 @@ def test_ir_gate_level(testclass, subroutine_name, irclass, irsubclasses, kwargs Gate.I(), [4], OpenQASMSerializationProperties(qubit_reference_type=QubitReferenceType.VIRTUAL), - "i __qubits__[4];", + "i q[4];", ), ( Gate.I(), @@ -402,7 +402,7 @@ def test_ir_gate_level(testclass, subroutine_name, irclass, irsubclasses, kwargs Gate.V(), [4], OpenQASMSerializationProperties(qubit_reference_type=QubitReferenceType.VIRTUAL), - "v __qubits__[4];", + "v q[4];", ), ( Gate.V(), @@ -414,7 +414,7 @@ def test_ir_gate_level(testclass, subroutine_name, irclass, irsubclasses, kwargs Gate.CY(), [0, 1], OpenQASMSerializationProperties(qubit_reference_type=QubitReferenceType.VIRTUAL), - "cy __qubits__[0], __qubits__[1];", + "cy q[0], q[1];", ), ( Gate.CY(), @@ -426,7 +426,7 @@ def test_ir_gate_level(testclass, subroutine_name, irclass, irsubclasses, kwargs Gate.Rz(angle=0.17), [4], OpenQASMSerializationProperties(qubit_reference_type=QubitReferenceType.VIRTUAL), - "rz(0.17) __qubits__[4];", + "rz(0.17) q[4];", ), ( Gate.Rz(angle=0.17), @@ -438,7 +438,7 @@ def test_ir_gate_level(testclass, subroutine_name, irclass, irsubclasses, kwargs Gate.XX(angle=0.17), [4, 5], OpenQASMSerializationProperties(qubit_reference_type=QubitReferenceType.VIRTUAL), - "xx(0.17) __qubits__[4], __qubits__[5];", + "xx(0.17) q[4], q[5];", ), ( Gate.XX(angle=0.17), @@ -450,7 +450,7 @@ def test_ir_gate_level(testclass, subroutine_name, irclass, irsubclasses, kwargs Gate.T(), [4], OpenQASMSerializationProperties(qubit_reference_type=QubitReferenceType.VIRTUAL), - "t __qubits__[4];", + "t q[4];", ), ( Gate.T(), @@ -468,13 +468,13 @@ def test_ir_gate_level(testclass, subroutine_name, irclass, irsubclasses, kwargs Gate.CZ(), [0, 1], OpenQASMSerializationProperties(qubit_reference_type=QubitReferenceType.VIRTUAL), - "cz __qubits__[0], __qubits__[1];", + "cz q[0], q[1];", ), ( Gate.YY(angle=0.17), [4, 5], OpenQASMSerializationProperties(qubit_reference_type=QubitReferenceType.VIRTUAL), - "yy(0.17) __qubits__[4], __qubits__[5];", + "yy(0.17) q[4], q[5];", ), ( Gate.YY(angle=0.17), @@ -486,7 +486,7 @@ def test_ir_gate_level(testclass, subroutine_name, irclass, irsubclasses, kwargs Gate.XY(angle=0.17), [4, 5], OpenQASMSerializationProperties(qubit_reference_type=QubitReferenceType.VIRTUAL), - "xy(0.17) __qubits__[4], __qubits__[5];", + "xy(0.17) q[4], q[5];", ), ( Gate.XY(angle=0.17), @@ -504,7 +504,7 @@ def test_ir_gate_level(testclass, subroutine_name, irclass, irsubclasses, kwargs Gate.ISwap(), [0, 1], OpenQASMSerializationProperties(qubit_reference_type=QubitReferenceType.VIRTUAL), - "iswap __qubits__[0], __qubits__[1];", + "iswap q[0], q[1];", ), ( Gate.Swap(), @@ -516,7 +516,7 @@ def test_ir_gate_level(testclass, subroutine_name, irclass, irsubclasses, kwargs Gate.Swap(), [0, 1], OpenQASMSerializationProperties(qubit_reference_type=QubitReferenceType.VIRTUAL), - "swap __qubits__[0], __qubits__[1];", + "swap q[0], q[1];", ), ( Gate.ECR(), @@ -528,7 +528,7 @@ def test_ir_gate_level(testclass, subroutine_name, irclass, irsubclasses, kwargs Gate.ECR(), [0, 1], OpenQASMSerializationProperties(qubit_reference_type=QubitReferenceType.VIRTUAL), - "ecr __qubits__[0], __qubits__[1];", + "ecr q[0], q[1];", ), ( Gate.CV(), @@ -540,13 +540,13 @@ def test_ir_gate_level(testclass, subroutine_name, irclass, irsubclasses, kwargs Gate.CV(), [0, 1], OpenQASMSerializationProperties(qubit_reference_type=QubitReferenceType.VIRTUAL), - "cv __qubits__[0], __qubits__[1];", + "cv q[0], q[1];", ), ( Gate.Vi(), [4], OpenQASMSerializationProperties(qubit_reference_type=QubitReferenceType.VIRTUAL), - "vi __qubits__[4];", + "vi q[4];", ), ( Gate.Vi(), @@ -558,7 +558,7 @@ def test_ir_gate_level(testclass, subroutine_name, irclass, irsubclasses, kwargs Gate.CSwap(), [0, 1, 2], OpenQASMSerializationProperties(qubit_reference_type=QubitReferenceType.VIRTUAL), - "cswap __qubits__[0], __qubits__[1], __qubits__[2];", + "cswap q[0], q[1], q[2];", ), ( Gate.CSwap(), @@ -570,7 +570,7 @@ def test_ir_gate_level(testclass, subroutine_name, irclass, irsubclasses, kwargs Gate.CPhaseShift01(angle=0.17), [4, 5], OpenQASMSerializationProperties(qubit_reference_type=QubitReferenceType.VIRTUAL), - "cphaseshift01(0.17) __qubits__[4], __qubits__[5];", + "cphaseshift01(0.17) q[4], q[5];", ), ( Gate.CPhaseShift01(angle=0.17), @@ -582,7 +582,7 @@ def test_ir_gate_level(testclass, subroutine_name, irclass, irsubclasses, kwargs Gate.CPhaseShift00(angle=0.17), [4, 5], OpenQASMSerializationProperties(qubit_reference_type=QubitReferenceType.VIRTUAL), - "cphaseshift00(0.17) __qubits__[4], __qubits__[5];", + "cphaseshift00(0.17) q[4], q[5];", ), ( Gate.CPhaseShift00(angle=0.17), @@ -594,7 +594,7 @@ def test_ir_gate_level(testclass, subroutine_name, irclass, irsubclasses, kwargs Gate.CPhaseShift(angle=0.17), [4, 5], OpenQASMSerializationProperties(qubit_reference_type=QubitReferenceType.VIRTUAL), - "cphaseshift(0.17) __qubits__[4], __qubits__[5];", + "cphaseshift(0.17) q[4], q[5];", ), ( Gate.CPhaseShift(angle=0.17), @@ -606,7 +606,7 @@ def test_ir_gate_level(testclass, subroutine_name, irclass, irsubclasses, kwargs Gate.S(), [4], OpenQASMSerializationProperties(qubit_reference_type=QubitReferenceType.VIRTUAL), - "s __qubits__[4];", + "s q[4];", ), ( Gate.S(), @@ -618,7 +618,7 @@ def test_ir_gate_level(testclass, subroutine_name, irclass, irsubclasses, kwargs Gate.Si(), [4], OpenQASMSerializationProperties(qubit_reference_type=QubitReferenceType.VIRTUAL), - "si __qubits__[4];", + "si q[4];", ), ( Gate.Si(), @@ -630,7 +630,7 @@ def test_ir_gate_level(testclass, subroutine_name, irclass, irsubclasses, kwargs Gate.Ti(), [4], OpenQASMSerializationProperties(qubit_reference_type=QubitReferenceType.VIRTUAL), - "ti __qubits__[4];", + "ti q[4];", ), ( Gate.Ti(), @@ -642,7 +642,7 @@ def test_ir_gate_level(testclass, subroutine_name, irclass, irsubclasses, kwargs Gate.PhaseShift(angle=0.17), [4], OpenQASMSerializationProperties(qubit_reference_type=QubitReferenceType.VIRTUAL), - "phaseshift(0.17) __qubits__[4];", + "phaseshift(0.17) q[4];", ), ( Gate.PhaseShift(angle=0.17), @@ -654,7 +654,7 @@ def test_ir_gate_level(testclass, subroutine_name, irclass, irsubclasses, kwargs Gate.CNot(), [4, 5], OpenQASMSerializationProperties(qubit_reference_type=QubitReferenceType.VIRTUAL), - "cnot __qubits__[4], __qubits__[5];", + "cnot q[4], q[5];", ), ( Gate.CNot(), @@ -666,7 +666,7 @@ def test_ir_gate_level(testclass, subroutine_name, irclass, irsubclasses, kwargs Gate.PSwap(angle=0.17), [4, 5], OpenQASMSerializationProperties(qubit_reference_type=QubitReferenceType.VIRTUAL), - "pswap(0.17) __qubits__[4], __qubits__[5];", + "pswap(0.17) q[4], q[5];", ), ( Gate.PSwap(angle=0.17), @@ -678,7 +678,7 @@ def test_ir_gate_level(testclass, subroutine_name, irclass, irsubclasses, kwargs Gate.CPhaseShift10(angle=0.17), [4, 5], OpenQASMSerializationProperties(qubit_reference_type=QubitReferenceType.VIRTUAL), - "cphaseshift10(0.17) __qubits__[4], __qubits__[5];", + "cphaseshift10(0.17) q[4], q[5];", ), ( Gate.CPhaseShift10(angle=0.17), @@ -690,7 +690,7 @@ def test_ir_gate_level(testclass, subroutine_name, irclass, irsubclasses, kwargs Gate.CCNot(), [4, 5, 6], OpenQASMSerializationProperties(qubit_reference_type=QubitReferenceType.VIRTUAL), - "ccnot __qubits__[4], __qubits__[5], __qubits__[6];", + "ccnot q[4], q[5], q[6];", ), ( Gate.CCNot(), @@ -711,7 +711,7 @@ def test_ir_gate_level(testclass, subroutine_name, irclass, irsubclasses, kwargs "[0, 0, 0, 0, 0, 1.0, 0, 0], " "[0, 0, 0, 0, 0, 0, 0, 1.0], " "[0, 0, 0, 0, 0, 0, 1.0, 0]" - "]) __qubits__[4], __qubits__[5], __qubits__[6]", + "]) q[4], q[5], q[6]", ), ( Gate.Unitary(Gate.CCNot().to_matrix()), @@ -737,7 +737,7 @@ def test_ir_gate_level(testclass, subroutine_name, irclass, irsubclasses, kwargs "[0, 0, 0.70710678im, 0.70710678], " "[0.70710678, -0.70710678im, 0, 0], " "[-0.70710678im, 0.70710678, 0, 0]" - "]) __qubits__[4], __qubits__[5]", + "]) q[4], q[5]", ), ( Gate.Unitary(np.round(Gate.ECR().to_matrix(), 8)), @@ -754,7 +754,7 @@ def test_ir_gate_level(testclass, subroutine_name, irclass, irsubclasses, kwargs Gate.Unitary(np.round(Gate.T().to_matrix(), 8)), [4], OpenQASMSerializationProperties(qubit_reference_type=QubitReferenceType.VIRTUAL), - "#pragma braket unitary([[1.0, 0], [0, 0.70710678 + 0.70710678im]]) __qubits__[4]", + "#pragma braket unitary([[1.0, 0], [0, 0.70710678 + 0.70710678im]]) q[4]", ), ( Gate.Unitary(np.round(Gate.T().to_matrix(), 8)), @@ -766,7 +766,7 @@ def test_ir_gate_level(testclass, subroutine_name, irclass, irsubclasses, kwargs Gate.Unitary(np.array([[1.0, 0], [0, 0.70710678 - 0.70710678j]])), [4], OpenQASMSerializationProperties(qubit_reference_type=QubitReferenceType.VIRTUAL), - "#pragma braket unitary([[1.0, 0], [0, 0.70710678 - 0.70710678im]]) __qubits__[4]", + "#pragma braket unitary([[1.0, 0], [0, 0.70710678 - 0.70710678im]]) q[4]", ), ( Gate.PulseGate( @@ -784,7 +784,7 @@ def test_ir_gate_level(testclass, subroutine_name, irclass, irsubclasses, kwargs Gate.GPi(angle=0.17), [4], OpenQASMSerializationProperties(qubit_reference_type=QubitReferenceType.VIRTUAL), - "gpi(0.17) __qubits__[4];", + "gpi(0.17) q[4];", ), ( Gate.GPi(angle=0.17), @@ -796,7 +796,7 @@ def test_ir_gate_level(testclass, subroutine_name, irclass, irsubclasses, kwargs Gate.GPi2(angle=0.17), [4], OpenQASMSerializationProperties(qubit_reference_type=QubitReferenceType.VIRTUAL), - "gpi2(0.17) __qubits__[4];", + "gpi2(0.17) q[4];", ), ( Gate.GPi2(angle=0.17), @@ -808,7 +808,7 @@ def test_ir_gate_level(testclass, subroutine_name, irclass, irsubclasses, kwargs Gate.MS(angle_1=0.17, angle_2=3.45), [4, 5], OpenQASMSerializationProperties(qubit_reference_type=QubitReferenceType.VIRTUAL), - f"ms(0.17, 3.45, {np.pi / 2}) __qubits__[4], __qubits__[5];", + f"ms(0.17, 3.45, {np.pi / 2}) q[4], q[5];", ), ( Gate.MS(angle_1=0.17, angle_2=3.45), @@ -1015,34 +1015,22 @@ def test_pulse_gate_to_matrix(): @pytest.mark.parametrize( "gate, target, control, control_state, expected_ir", ( - (Gate.H(), QubitSet(0), QubitSet(1), None, "ctrl @ h __qubits__[1], __qubits__[0];"), - ( - Gate.H(), - QubitSet(0), - QubitSet([1, 2]), - None, - "ctrl(2) @ h __qubits__[1], __qubits__[2], __qubits__[0];", - ), - ( - Gate.Ry(angle=1.23), - QubitSet(0), - QubitSet([2]), - None, - "ctrl @ ry(1.23) __qubits__[2], __qubits__[0];", - ), + (Gate.H(), QubitSet(0), QubitSet(1), None, "ctrl @ h q[1], q[0];"), + (Gate.H(), QubitSet(0), QubitSet([1, 2]), None, "ctrl(2) @ h q[1], q[2], q[0];"), + (Gate.Ry(angle=1.23), QubitSet(0), QubitSet([2]), None, "ctrl @ ry(1.23) q[2], q[0];"), ( Gate.MS(angle_1=0.17, angle_2=3.45), QubitSet(0), QubitSet([1, 2]), None, - f"ctrl(2) @ ms(0.17, 3.45, {np.pi / 2}) __qubits__[1], __qubits__[2], __qubits__[0];", + f"ctrl(2) @ ms(0.17, 3.45, {np.pi / 2}) q[1], q[2], q[0];", ), ( Gate.CCNot(), QubitSet([0, 1, 2]), QubitSet([3, 4]), None, - "ctrl(2) @ ccnot __qubits__[3], __qubits__[4], __qubits__[0], __qubits__[1], __qubits__[2];", # noqa + "ctrl(2) @ ccnot q[3], q[4], q[0], q[1], q[2];", ), ( Gate.Z(), @@ -1050,7 +1038,7 @@ def test_pulse_gate_to_matrix(): QubitSet([1, 2, 3, 4, 5, 6, 7]), [1, 1, 1, 0, 0, 1, 0], "ctrl(3) @ negctrl(2) @ ctrl @ negctrl @ " - "z __qubits__[1], __qubits__[2], __qubits__[3], __qubits__[4], __qubits__[5], __qubits__[6], __qubits__[7], __qubits__[0];", # noqa + "z q[1], q[2], q[3], q[4], q[5], q[6], q[7], q[0];", ), ( Gate.Z(), @@ -1058,7 +1046,7 @@ def test_pulse_gate_to_matrix(): QubitSet([1, 2, 3, 4, 5, 6, 7]), "1110010", "ctrl(3) @ negctrl(2) @ ctrl @ negctrl @ " - "z __qubits__[1], __qubits__[2], __qubits__[3], __qubits__[4], __qubits__[5], __qubits__[6], __qubits__[7], __qubits__[0];", # noqa + "z q[1], q[2], q[3], q[4], q[5], q[6], q[7], q[0];", ), ( Gate.Z(), @@ -1066,21 +1054,21 @@ def test_pulse_gate_to_matrix(): QubitSet([1, 2, 3, 4, 5, 6, 7]), 114, "ctrl(3) @ negctrl(2) @ ctrl @ negctrl @ " - "z __qubits__[1], __qubits__[2], __qubits__[3], __qubits__[4], __qubits__[5], __qubits__[6], __qubits__[7], __qubits__[0];", # noqa + "z q[1], q[2], q[3], q[4], q[5], q[6], q[7], q[0];", ), ( Gate.Z(), QubitSet([0]), QubitSet([1, 2, 3]), [1, 0], - "negctrl @ ctrl @ negctrl @ z __qubits__[1], __qubits__[2], __qubits__[3], __qubits__[0];", # noqa + "negctrl @ ctrl @ negctrl @ z q[1], q[2], q[3], q[0];", ), ( Gate.Z(), QubitSet([0]), QubitSet([1, 2, 3]), "10", - "negctrl @ ctrl @ negctrl @ z __qubits__[1], __qubits__[2], __qubits__[3], __qubits__[0];", # noqa + "negctrl @ ctrl @ negctrl @ z q[1], q[2], q[3], q[0];", ), ), ) @@ -1139,13 +1127,13 @@ def test_gate_control_invalid_state(control, control_state, error_string): @pytest.mark.parametrize( "gate, target, power, expected_ir", ( - (Gate.H(), QubitSet(0), 2, "pow(2) @ h __qubits__[0];"), - (Gate.H(), QubitSet(0), 2.0, "pow(2.0) @ h __qubits__[0];"), - (Gate.H(), QubitSet(0), 2.5, "pow(2.5) @ h __qubits__[0];"), - (Gate.H(), QubitSet(0), 0, "pow(0) @ h __qubits__[0];"), - (Gate.H(), QubitSet(0), -2, "inv @ pow(2) @ h __qubits__[0];"), - (Gate.H(), QubitSet(0), -2.0, "inv @ pow(2.0) @ h __qubits__[0];"), - (Gate.H(), QubitSet(0), -2.5, "inv @ pow(2.5) @ h __qubits__[0];"), + (Gate.H(), QubitSet(0), 2, "pow(2) @ h q[0];"), + (Gate.H(), QubitSet(0), 2.0, "pow(2.0) @ h q[0];"), + (Gate.H(), QubitSet(0), 2.5, "pow(2.5) @ h q[0];"), + (Gate.H(), QubitSet(0), 0, "pow(0) @ h q[0];"), + (Gate.H(), QubitSet(0), -2, "inv @ pow(2) @ h q[0];"), + (Gate.H(), QubitSet(0), -2.0, "inv @ pow(2.0) @ h q[0];"), + (Gate.H(), QubitSet(0), -2.5, "inv @ pow(2.5) @ h q[0];"), ), ) def test_gate_power(gate, target, power, expected_ir): diff --git a/test/unit_tests/braket/circuits/test_noise_helpers.py b/test/unit_tests/braket/circuits/test_noise_helpers.py index 421cf69c0..f2c956eab 100644 --- a/test/unit_tests/braket/circuits/test_noise_helpers.py +++ b/test/unit_tests/braket/circuits/test_noise_helpers.py @@ -20,7 +20,7 @@ from braket.circuits.moments import Moments from braket.circuits.noise import Noise from braket.circuits.noise_helpers import apply_noise_to_gates, apply_noise_to_moments -from braket.circuits.qubit_set import QubitSet +from braket.registers.qubit_set import QubitSet invalid_data_noise_type = [Gate.X(), None, 1.5] invalid_data_target_gates_type = [[-1, "foo"], [1.5, None, -1], "X", [Gate.X, "CNot"]] diff --git a/test/unit_tests/braket/circuits/test_noises.py b/test/unit_tests/braket/circuits/test_noises.py index 64710c8fd..2b55dfa4f 100644 --- a/test/unit_tests/braket/circuits/test_noises.py +++ b/test/unit_tests/braket/circuits/test_noises.py @@ -547,7 +547,7 @@ def test_valid_values_pauli_channel_two_qubit(probs): Noise.BitFlip(0.5), OpenQASMSerializationProperties(qubit_reference_type=QubitReferenceType.VIRTUAL), [3], - "#pragma braket noise bit_flip(0.5) __qubits__[3]", + "#pragma braket noise bit_flip(0.5) q[3]", ), ( Noise.BitFlip(0.5), @@ -559,7 +559,7 @@ def test_valid_values_pauli_channel_two_qubit(probs): Noise.PhaseFlip(0.5), OpenQASMSerializationProperties(qubit_reference_type=QubitReferenceType.VIRTUAL), [3], - "#pragma braket noise phase_flip(0.5) __qubits__[3]", + "#pragma braket noise phase_flip(0.5) q[3]", ), ( Noise.PhaseFlip(0.5), @@ -571,7 +571,7 @@ def test_valid_values_pauli_channel_two_qubit(probs): Noise.PauliChannel(0.1, 0.2, 0.3), OpenQASMSerializationProperties(qubit_reference_type=QubitReferenceType.VIRTUAL), [3], - "#pragma braket noise pauli_channel(0.1, 0.2, 0.3) __qubits__[3]", + "#pragma braket noise pauli_channel(0.1, 0.2, 0.3) q[3]", ), ( Noise.PauliChannel(0.1, 0.2, 0.3), @@ -583,7 +583,7 @@ def test_valid_values_pauli_channel_two_qubit(probs): Noise.Depolarizing(0.5), OpenQASMSerializationProperties(qubit_reference_type=QubitReferenceType.VIRTUAL), [3], - "#pragma braket noise depolarizing(0.5) __qubits__[3]", + "#pragma braket noise depolarizing(0.5) q[3]", ), ( Noise.Depolarizing(0.5), @@ -595,7 +595,7 @@ def test_valid_values_pauli_channel_two_qubit(probs): Noise.TwoQubitDepolarizing(0.5), OpenQASMSerializationProperties(qubit_reference_type=QubitReferenceType.VIRTUAL), [3, 5], - "#pragma braket noise two_qubit_depolarizing(0.5) __qubits__[3], __qubits__[5]", + "#pragma braket noise two_qubit_depolarizing(0.5) q[3], q[5]", ), ( Noise.TwoQubitDepolarizing(0.5), @@ -607,7 +607,7 @@ def test_valid_values_pauli_channel_two_qubit(probs): Noise.TwoQubitDephasing(0.5), OpenQASMSerializationProperties(qubit_reference_type=QubitReferenceType.VIRTUAL), [3, 5], - "#pragma braket noise two_qubit_dephasing(0.5) __qubits__[3], __qubits__[5]", + "#pragma braket noise two_qubit_dephasing(0.5) q[3], q[5]", ), ( Noise.TwoQubitDephasing(0.5), @@ -619,7 +619,7 @@ def test_valid_values_pauli_channel_two_qubit(probs): Noise.AmplitudeDamping(0.5), OpenQASMSerializationProperties(qubit_reference_type=QubitReferenceType.VIRTUAL), [3], - "#pragma braket noise amplitude_damping(0.5) __qubits__[3]", + "#pragma braket noise amplitude_damping(0.5) q[3]", ), ( Noise.AmplitudeDamping(0.5), @@ -631,7 +631,7 @@ def test_valid_values_pauli_channel_two_qubit(probs): Noise.GeneralizedAmplitudeDamping(0.5, 0.1), OpenQASMSerializationProperties(qubit_reference_type=QubitReferenceType.VIRTUAL), [3], - "#pragma braket noise generalized_amplitude_damping(0.5, 0.1) __qubits__[3]", + "#pragma braket noise generalized_amplitude_damping(0.5, 0.1) q[3]", ), ( Noise.GeneralizedAmplitudeDamping(0.5, 0.1), @@ -643,7 +643,7 @@ def test_valid_values_pauli_channel_two_qubit(probs): Noise.PhaseDamping(0.5), OpenQASMSerializationProperties(qubit_reference_type=QubitReferenceType.VIRTUAL), [3], - "#pragma braket noise phase_damping(0.5) __qubits__[3]", + "#pragma braket noise phase_damping(0.5) q[3]", ), ( Noise.PhaseDamping(0.5), @@ -668,7 +668,7 @@ def test_valid_values_pauli_channel_two_qubit(probs): "[0, 0.31622776601683794, 0, 0], " "[0.31622776601683794, 0, 0, 0], " "[0, 0, 0, 0.31622776601683794], " - "[0, 0, 0.31622776601683794, 0]]) __qubits__[3], __qubits__[5]", + "[0, 0, 0.31622776601683794, 0]]) q[3], q[5]", ), ( Noise.Kraus( @@ -700,7 +700,7 @@ def test_valid_values_pauli_channel_two_qubit(probs): [3], "#pragma braket noise kraus([" "[0.9486833im, 0], [0, 0.9486833im]], [" - "[0, 0.31622777], [0.31622777, 0]]) __qubits__[3]", + "[0, 0.31622777], [0.31622777, 0]]) q[3]", ), ( Noise.Kraus( diff --git a/test/unit_tests/braket/circuits/test_observables.py b/test/unit_tests/braket/circuits/test_observables.py index 415786375..79f917af9 100644 --- a/test/unit_tests/braket/circuits/test_observables.py +++ b/test/unit_tests/braket/circuits/test_observables.py @@ -67,7 +67,7 @@ def test_to_ir(testobject, gateobject, expected_ir, basis_rotation_gates, eigenv Observable.I(), OpenQASMSerializationProperties(qubit_reference_type=QubitReferenceType.VIRTUAL), [3], - "i(__qubits__[3])", + "i(q[3])", ), ( Observable.I(), @@ -85,7 +85,7 @@ def test_to_ir(testobject, gateobject, expected_ir, basis_rotation_gates, eigenv Observable.X(), OpenQASMSerializationProperties(qubit_reference_type=QubitReferenceType.VIRTUAL), [3], - "x(__qubits__[3])", + "x(q[3])", ), ( Observable.X(), @@ -103,7 +103,7 @@ def test_to_ir(testobject, gateobject, expected_ir, basis_rotation_gates, eigenv Observable.Y(), OpenQASMSerializationProperties(qubit_reference_type=QubitReferenceType.VIRTUAL), [3], - "y(__qubits__[3])", + "y(q[3])", ), ( Observable.Y(), @@ -121,7 +121,7 @@ def test_to_ir(testobject, gateobject, expected_ir, basis_rotation_gates, eigenv Observable.Z(), OpenQASMSerializationProperties(qubit_reference_type=QubitReferenceType.VIRTUAL), [3], - "z(__qubits__[3])", + "z(q[3])", ), ( Observable.Z(), @@ -139,7 +139,7 @@ def test_to_ir(testobject, gateobject, expected_ir, basis_rotation_gates, eigenv Observable.H(), OpenQASMSerializationProperties(qubit_reference_type=QubitReferenceType.VIRTUAL), [3], - "h(__qubits__[3])", + "h(q[3])", ), ( Observable.H(), @@ -158,7 +158,7 @@ def test_to_ir(testobject, gateobject, expected_ir, basis_rotation_gates, eigenv OpenQASMSerializationProperties(qubit_reference_type=QubitReferenceType.VIRTUAL), [1, 2], "hermitian([[1+0im, 0im, 0im, 0im], [0im, 1+0im, 0im, 0im], " - "[0im, 0im, 1+0im, 0im], [0im, 0im, 0im, 1+0im]]) __qubits__[1], __qubits__[2]", + "[0im, 0im, 1+0im, 0im], [0im, 0im, 0im, 1+0im]]) q[1], q[2]", ), ( Observable.Hermitian(np.eye(4)), @@ -177,7 +177,7 @@ def test_to_ir(testobject, gateobject, expected_ir, basis_rotation_gates, eigenv Observable.H() @ Observable.Z(), OpenQASMSerializationProperties(qubit_reference_type=QubitReferenceType.VIRTUAL), [3, 0], - "h(__qubits__[3]) @ z(__qubits__[0])", + "h(q[3]) @ z(q[0])", ), ( Observable.H() @ Observable.Z(), @@ -189,7 +189,7 @@ def test_to_ir(testobject, gateobject, expected_ir, basis_rotation_gates, eigenv Observable.H() @ Observable.Z() @ Observable.I(), OpenQASMSerializationProperties(qubit_reference_type=QubitReferenceType.VIRTUAL), [3, 0, 1], - "h(__qubits__[3]) @ z(__qubits__[0]) @ i(__qubits__[1])", + "h(q[3]) @ z(q[0]) @ i(q[1])", ), ( Observable.H() @ Observable.Z() @ Observable.I(), @@ -202,8 +202,8 @@ def test_to_ir(testobject, gateobject, expected_ir, basis_rotation_gates, eigenv OpenQASMSerializationProperties(qubit_reference_type=QubitReferenceType.VIRTUAL), [3, 0, 1], "hermitian([[1+0im, 0im, 0im, 0im], [0im, 1+0im, 0im, 0im], " - "[0im, 0im, 1+0im, 0im], [0im, 0im, 0im, 1+0im]]) __qubits__[3], __qubits__[0]" - " @ i(__qubits__[1])", + "[0im, 0im, 1+0im, 0im], [0im, 0im, 0im, 1+0im]]) q[3], q[0]" + " @ i(q[1])", ), ( Observable.I() @ Observable.Hermitian(np.eye(4)), diff --git a/test/unit_tests/braket/circuits/test_result_types.py b/test/unit_tests/braket/circuits/test_result_types.py index 8b3b9cd00..5f76eeaf9 100644 --- a/test/unit_tests/braket/circuits/test_result_types.py +++ b/test/unit_tests/braket/circuits/test_result_types.py @@ -155,7 +155,7 @@ def test_ir_result_level(testclass, subroutine_name, irclass, input, ir_input): ( ResultType.Expectation(Observable.I(), target=0), OpenQASMSerializationProperties(qubit_reference_type=QubitReferenceType.VIRTUAL), - "#pragma braket result expectation i(__qubits__[0])", + "#pragma braket result expectation i(q[0])", ), ( ResultType.Expectation(Observable.I()), @@ -175,7 +175,7 @@ def test_ir_result_level(testclass, subroutine_name, irclass, input, ir_input): ( ResultType.DensityMatrix([0, 2]), OpenQASMSerializationProperties(qubit_reference_type=QubitReferenceType.VIRTUAL), - "#pragma braket result density_matrix __qubits__[0], __qubits__[2]", + "#pragma braket result density_matrix q[0], q[2]", ), ( ResultType.DensityMatrix(0), @@ -195,7 +195,7 @@ def test_ir_result_level(testclass, subroutine_name, irclass, input, ir_input): ( ResultType.Probability([0, 2]), OpenQASMSerializationProperties(qubit_reference_type=QubitReferenceType.VIRTUAL), - "#pragma braket result probability __qubits__[0], __qubits__[2]", + "#pragma braket result probability q[0], q[2]", ), ( ResultType.Probability(0), @@ -205,7 +205,7 @@ def test_ir_result_level(testclass, subroutine_name, irclass, input, ir_input): ( ResultType.Sample(Observable.I(), target=0), OpenQASMSerializationProperties(qubit_reference_type=QubitReferenceType.VIRTUAL), - "#pragma braket result sample i(__qubits__[0])", + "#pragma braket result sample i(q[0])", ), ( ResultType.Sample(Observable.I()), @@ -215,7 +215,7 @@ def test_ir_result_level(testclass, subroutine_name, irclass, input, ir_input): ( ResultType.Variance(Observable.I(), target=0), OpenQASMSerializationProperties(qubit_reference_type=QubitReferenceType.VIRTUAL), - "#pragma braket result variance i(__qubits__[0])", + "#pragma braket result variance i(q[0])", ), ( ResultType.Variance(Observable.I()), @@ -225,12 +225,12 @@ def test_ir_result_level(testclass, subroutine_name, irclass, input, ir_input): ( ResultType.AdjointGradient(Observable.I(), target=0, parameters=["alpha", "beta"]), OpenQASMSerializationProperties(qubit_reference_type=QubitReferenceType.VIRTUAL), - "#pragma braket result adjoint_gradient expectation(i(__qubits__[0])) alpha, beta", + "#pragma braket result adjoint_gradient expectation(i(q[0])) alpha, beta", ), ( ResultType.AdjointGradient(Observable.I(), target=0, parameters=["alpha"]), OpenQASMSerializationProperties(qubit_reference_type=QubitReferenceType.VIRTUAL), - "#pragma braket result adjoint_gradient expectation(i(__qubits__[0])) alpha", + "#pragma braket result adjoint_gradient expectation(i(q[0])) alpha", ), ( ResultType.AdjointGradient( @@ -239,7 +239,7 @@ def test_ir_result_level(testclass, subroutine_name, irclass, input, ir_input): parameters=[FreeParameter("alpha"), "beta", FreeParameter("gamma")], ), OpenQASMSerializationProperties(qubit_reference_type=QubitReferenceType.VIRTUAL), - "#pragma braket result adjoint_gradient expectation(h(__qubits__[0]) @ i(__qubits__[1])) " # noqa + "#pragma braket result adjoint_gradient expectation(h(q[0]) @ i(q[1])) " "alpha, beta, gamma", ), ( @@ -249,20 +249,20 @@ def test_ir_result_level(testclass, subroutine_name, irclass, input, ir_input): parameters=[FreeParameter("alpha"), "beta", FreeParameter("gamma")], ), OpenQASMSerializationProperties(qubit_reference_type=QubitReferenceType.VIRTUAL), - "#pragma braket result adjoint_gradient expectation(h(__qubits__[0]) @ i(__qubits__[1]) + 2 * z(__qubits__[2])) " # noqa + "#pragma braket result adjoint_gradient expectation(h(q[0]) @ i(q[1]) + 2 * z(q[2])) " "alpha, beta, gamma", ), ( ResultType.AdjointGradient(Observable.I(), target=0, parameters=[]), OpenQASMSerializationProperties(qubit_reference_type=QubitReferenceType.VIRTUAL), - "#pragma braket result adjoint_gradient expectation(i(__qubits__[0])) all", + "#pragma braket result adjoint_gradient expectation(i(q[0])) all", ), ( ResultType.AdjointGradient( Observable.X() @ Observable.Y(), target=[0, 1], parameters=[] ), OpenQASMSerializationProperties(qubit_reference_type=QubitReferenceType.VIRTUAL), - "#pragma braket result adjoint_gradient expectation(x(__qubits__[0]) @ y(__qubits__[1])) all", # noqa + "#pragma braket result adjoint_gradient expectation(x(q[0]) @ y(q[1])) all", ), ( ResultType.AdjointGradient( @@ -270,7 +270,7 @@ def test_ir_result_level(testclass, subroutine_name, irclass, input, ir_input): ), OpenQASMSerializationProperties(qubit_reference_type=QubitReferenceType.VIRTUAL), "#pragma braket result adjoint_gradient expectation(hermitian([[1+0im, 0im], " - "[0im, 1+0im]]) __qubits__[0]) all", + "[0im, 1+0im]]) q[0]) all", ), ], ) diff --git a/test/unit_tests/braket/devices/test_local_simulator.py b/test/unit_tests/braket/devices/test_local_simulator.py index 736c510ab..8485dc5e5 100644 --- a/test/unit_tests/braket/devices/test_local_simulator.py +++ b/test/unit_tests/braket/devices/test_local_simulator.py @@ -316,7 +316,7 @@ def test_batch_circuit(): theta = FreeParameter("theta") task = Circuit().rx(angle=theta, target=0) device = LocalSimulator(dummy) - num_tasks = 10 + num_tasks = 3 circuits = [task for _ in range(num_tasks)] inputs = [{"theta": i} for i in range(num_tasks)] batch = device.run_batch(circuits, inputs=inputs, shots=10) @@ -329,7 +329,7 @@ def test_batch_with_max_parallel(): dummy = DummyProgramSimulator() task = Circuit().h(0).cnot(0, 1) device = LocalSimulator(dummy) - num_tasks = 10 + num_tasks = 3 circuits = [task for _ in range(num_tasks)] batch = device.run_batch(circuits, shots=10, max_parallel=2) assert len(batch.results()) == num_tasks @@ -341,7 +341,7 @@ def test_batch_with_annealing_problems(): dummy = DummyAnnealingSimulator() problem = Problem(ProblemType.ISING) device = LocalSimulator(dummy) - num_tasks = 10 + num_tasks = 3 problems = [problem for _ in range(num_tasks)] batch = device.run_batch(problems, shots=10) assert len(batch.results()) == num_tasks @@ -353,7 +353,7 @@ def test_batch_circuit_without_inputs(): dummy = DummyProgramSimulator() bell = Circuit().h(0).cnot(0, 1) device = LocalSimulator(dummy) - num_tasks = 10 + num_tasks = 3 circuits = [bell for _ in range(num_tasks)] batch = device.run_batch(circuits, shots=10) assert len(batch.results()) == num_tasks @@ -385,7 +385,7 @@ def test_batch_circuit_with_task_and_input_mismatch(): dummy = DummyProgramSimulator() bell = Circuit().h(0).cnot(0, 1) device = LocalSimulator(dummy) - num_tasks = 10 + num_tasks = 3 circuits = [bell for _ in range(num_tasks)] inputs = [{} for _ in range(num_tasks - 1)] with pytest.raises(ValueError): @@ -404,10 +404,10 @@ def test_run_gate_model_inputs(): ( "OPENQASM 3.0;", "input float theta;", - "bit[1] __bits__;", - "qubit[1] __qubits__;", - "rx(theta) __qubits__[0];", - "__bits__[0] = measure __qubits__[0];", + "bit[1] b;", + "qubit[1] q;", + "rx(theta) q[0];", + "b[0] = measure q[0];", ) ), inputs={"theta": 2}, diff --git a/test/unit_tests/braket/jobs/job_module.py b/test/unit_tests/braket/jobs/job_module.py new file mode 100644 index 000000000..5dbc56d00 --- /dev/null +++ b/test/unit_tests/braket/jobs/job_module.py @@ -0,0 +1,2 @@ +def some_helper(): + return "success" diff --git a/test/unit_tests/braket/jobs/test_data_persistence.py b/test/unit_tests/braket/jobs/test_data_persistence.py index d40d2c203..6a5e27283 100644 --- a/test/unit_tests/braket/jobs/test_data_persistence.py +++ b/test/unit_tests/braket/jobs/test_data_persistence.py @@ -20,7 +20,12 @@ import numpy as np import pytest -from braket.jobs.data_persistence import load_job_checkpoint, save_job_checkpoint, save_job_result +from braket.jobs.data_persistence import ( + load_job_checkpoint, + load_job_result, + save_job_checkpoint, + save_job_result, +) from braket.jobs_data import PersistedJobDataFormat @@ -266,9 +271,68 @@ def test_save_job_result(data_format, result_data, expected_saved_data): assert expected_file.read() == expected_saved_data -@pytest.mark.xfail(raises=ValueError) @pytest.mark.parametrize("result_data", [{}, None]) -def test_save_job_result_raises_error_empty_data(result_data): +def test_save_job_result_does_not_raise_error_empty_data(result_data): with tempfile.TemporaryDirectory() as tmp_dir: - with patch.dict(os.environ, {"AMZN_BRAKET_CHECKPOINT_DIR": tmp_dir}): + with patch.dict(os.environ, {"AMZN_BRAKET_JOB_RESULTS_DIR": tmp_dir}): save_job_result(result_data) + + +@pytest.mark.parametrize( + "first_result_data," + "first_data_format," + "second_result_data," + "second_data_format," + "expected_result_data", + ( + ( + "hello", + PersistedJobDataFormat.PLAINTEXT, + "goodbye", + PersistedJobDataFormat.PLAINTEXT, + {"result": "goodbye"}, + ), + ( + "hello", + PersistedJobDataFormat.PLAINTEXT, + "goodbye", + PersistedJobDataFormat.PICKLED_V4, + {"result": "goodbye"}, + ), + ("hello", PersistedJobDataFormat.PICKLED_V4, "goodbye", None, {"result": "goodbye"}), + ( + # not json serializable + PersistedJobDataFormat, + PersistedJobDataFormat.PICKLED_V4, + {"other_field": "value"}, + None, + {"result": PersistedJobDataFormat, "other_field": "value"}, + ), + ), +) +def test_update_result_data( + first_result_data, + first_data_format, + second_result_data, + second_data_format, + expected_result_data, +): + with tempfile.TemporaryDirectory() as tmp_dir: + with patch.dict(os.environ, {"AMZN_BRAKET_JOB_RESULTS_DIR": tmp_dir}): + save_job_result(first_result_data, first_data_format) + save_job_result(second_result_data, second_data_format) + + assert load_job_result() == expected_result_data + + +def test_update_pickled_results_as_plaintext_error(): + with tempfile.TemporaryDirectory() as tmp_dir: + with patch.dict(os.environ, {"AMZN_BRAKET_JOB_RESULTS_DIR": tmp_dir}): + save_job_result(np.arange(5), PersistedJobDataFormat.PICKLED_V4) + + cannot_convert_pickled_to_plaintext = ( + "Cannot update results object serialized with " + "pickled_v4 using data format plaintext." + ) + with pytest.raises(TypeError, match=cannot_convert_pickled_to_plaintext): + save_job_result("hello", PersistedJobDataFormat.PLAINTEXT) diff --git a/test/unit_tests/braket/jobs/test_environment_variables.py b/test/unit_tests/braket/jobs/test_environment_variables.py new file mode 100644 index 000000000..2c8a2f546 --- /dev/null +++ b/test/unit_tests/braket/jobs/test_environment_variables.py @@ -0,0 +1,66 @@ +import json +import os +import tempfile +from pathlib import Path +from unittest.mock import patch + +from braket.jobs import ( + get_checkpoint_dir, + get_hyperparameters, + get_input_data_dir, + get_job_device_arn, + get_job_name, + get_results_dir, +) + + +def test_job_name(): + assert get_job_name() == "" + job_name = "my_job_name" + with patch.dict(os.environ, {"AMZN_BRAKET_JOB_NAME": job_name}): + assert get_job_name() == job_name + + +def test_job_device_arn(): + assert get_job_device_arn() == "local:none/none" + device_arn = "my_device_arn" + with patch.dict(os.environ, {"AMZN_BRAKET_DEVICE_ARN": device_arn}): + assert get_job_device_arn() == device_arn + + +def test_input_data_dir(): + assert get_input_data_dir() == "." + input_path = "my/input/path" + with patch.dict(os.environ, {"AMZN_BRAKET_INPUT_DIR": input_path}): + assert get_input_data_dir() == f"{input_path}/input" + channel_name = "my_channel" + assert get_input_data_dir(channel_name) == f"{input_path}/{channel_name}" + + +def test_results_dir(): + assert get_results_dir() == "." + results_dir = "my_results_dir" + with patch.dict(os.environ, {"AMZN_BRAKET_JOB_RESULTS_DIR": results_dir}): + assert get_results_dir() == results_dir + + +def test_checkpoint_dir(): + assert get_checkpoint_dir() == "." + checkpoint_dir = "my_checkpoint_dir" + with patch.dict(os.environ, {"AMZN_BRAKET_CHECKPOINT_DIR": checkpoint_dir}): + assert get_checkpoint_dir() == checkpoint_dir + + +def test_hyperparameters(): + assert get_hyperparameters() == {} + hp_file = "my_hyperparameters.json" + hyperparameters = { + "a": "a_val", + "b": 2, + } + with tempfile.TemporaryDirectory() as temp_dir, patch.dict( + os.environ, {"AMZN_BRAKET_HP_FILE": str(Path(temp_dir) / hp_file)} + ): + with open(str(Path(temp_dir) / hp_file), "w") as f: + json.dump(hyperparameters, f) + assert get_hyperparameters() == hyperparameters diff --git a/test/unit_tests/braket/jobs/test_hybrid_job.py b/test/unit_tests/braket/jobs/test_hybrid_job.py new file mode 100644 index 000000000..b7b7485d7 --- /dev/null +++ b/test/unit_tests/braket/jobs/test_hybrid_job.py @@ -0,0 +1,549 @@ +import ast +import importlib +import re +import sys +import tempfile +from logging import getLogger +from pathlib import Path +from ssl import SSLContext +from unittest.mock import MagicMock, patch + +import job_module +import pytest +from cloudpickle import cloudpickle + +from braket.aws import AwsQuantumJob +from braket.devices import Devices +from braket.jobs import hybrid_job +from braket.jobs.config import ( + CheckpointConfig, + InstanceConfig, + OutputDataConfig, + S3DataSourceConfig, + StoppingCondition, +) +from braket.jobs.hybrid_job import _sanitize, _serialize_entry_point +from braket.jobs.local import LocalQuantumJob + + +@pytest.fixture +def aws_session(): + aws_session = MagicMock() + python_version_str = f"py{sys.version_info.major}{sys.version_info.minor}" + aws_session.get_full_image_tag.return_value = f"1.0-cpu-{python_version_str}-ubuntu22.04" + aws_session.region = "us-west-2" + return aws_session + + +@patch.object(sys.modules["braket.jobs.hybrid_job"], "retrieve_image") +@patch("time.time", return_value=123.0) +@patch("builtins.open") +@patch("tempfile.TemporaryDirectory") +@patch.object(AwsQuantumJob, "create") +def test_decorator_defaults( + mock_create, mock_tempdir, _mock_open, mock_time, mock_retrieve, aws_session +): + mock_retrieve.return_value = "00000000.dkr.ecr.us-west-2.amazonaws.com/latest" + + @hybrid_job(device=None, aws_session=aws_session) + def my_entry(c=0, d: float = 1.0, **extras): + return "my entry return value" + + mock_tempdir_name = "job_temp_dir_00000" + mock_tempdir.return_value.__enter__.return_value = mock_tempdir_name + + source_module = mock_tempdir_name + entry_point = f"{mock_tempdir_name}.entry_point:my_entry" + wait_until_complete = False + + device = "local:none/none" + + my_entry() + + mock_create.assert_called_with( + device=device, + source_module=source_module, + entry_point=entry_point, + wait_until_complete=wait_until_complete, + job_name="my-entry-123000", + hyperparameters={"c": "0", "d": "1.0"}, + logger=getLogger("braket.jobs.hybrid_job"), + aws_session=aws_session, + ) + assert mock_tempdir.return_value.__exit__.called + + +@pytest.mark.parametrize("include_modules", (job_module, ["job_module"])) +@patch("braket.jobs.image_uris.retrieve_image") +@patch("sys.stdout") +@patch("time.time", return_value=123.0) +@patch("cloudpickle.register_pickle_by_value") +@patch("cloudpickle.unregister_pickle_by_value") +@patch("shutil.copy") +@patch("builtins.open") +@patch.object(AwsQuantumJob, "create") +def test_decorator_non_defaults( + mock_create, + _mock_open, + mock_copy, + mock_register, + mock_unregister, + mock_time, + mock_stdout, + mock_retrieve, + include_modules, +): + mock_retrieve.return_value = "should-not-be-used" + dependencies = "my_requirements.txt" + image_uri = "my_image.uri" + default_instance = InstanceConfig() + distribution = "data_parallel" + copy_checkpoints_from_job = "arn/other-job" + checkpoint_config = CheckpointConfig(localPath="local", s3Uri="s3") + role_arn = "role_arn" + stopping_condition = StoppingCondition(maxRuntimeInSeconds=10) + output_data_config = OutputDataConfig(s3Path="s3") + aws_session = MagicMock() + tags = {"my_tag": "my_value"} + logger = getLogger(__name__) + + with tempfile.TemporaryDirectory() as tempdir: + Path(tempdir, "temp_dir").mkdir() + Path(tempdir, "temp_file").touch() + + input_data = { + "my_prefix": "my_input_data", + "my_dir": Path(tempdir, "temp_dir"), + "my_file": Path(tempdir, "temp_file"), + "my_s3_prefix": "s3://bucket/path/to/prefix", + "my_s3_config": S3DataSourceConfig(s3_data="s3://bucket/path/to/prefix"), + } + + @hybrid_job( + device=Devices.Amazon.SV1, + include_modules=include_modules, + dependencies=dependencies, + image_uri=image_uri, + input_data=input_data, + wait_until_complete=True, + instance_config=default_instance, + distribution=distribution, + checkpoint_config=checkpoint_config, + copy_checkpoints_from_job=copy_checkpoints_from_job, + role_arn=role_arn, + stopping_condition=stopping_condition, + output_data_config=output_data_config, + aws_session=aws_session, + tags=tags, + logger=logger, + ) + def my_entry(a, b: int, c=0, d: float = 1.0, **extras) -> str: + return "my entry return value" + + mock_tempdir = MagicMock(spec=tempfile.TemporaryDirectory) + mock_tempdir_name = "job_temp_dir_00000" + mock_tempdir.__enter__.return_value = mock_tempdir_name + + device = Devices.Amazon.SV1 + source_module = mock_tempdir_name + entry_point = f"{mock_tempdir_name}.entry_point:my_entry" + wait_until_complete = True + + s3_not_linked = ( + "Input data channels mapped to an S3 source will not be available in the working " + 'directory. Use `get_input_data_dir(channel="my_s3_prefix")` to read input data ' + "from S3 source inside the job container." + ) + + with patch("tempfile.TemporaryDirectory", return_value=mock_tempdir): + my_entry("a", 2, 3, 4, extra_param="value", another=6) + + mock_create.assert_called_with( + device=device, + source_module=source_module, + entry_point=entry_point, + image_uri=image_uri, + input_data=input_data, + wait_until_complete=wait_until_complete, + job_name="my-entry-123000", + instance_config=default_instance, + distribution=distribution, + hyperparameters={ + "a": "a", + "b": "2", + "c": "3", + "d": "4", + "extra_param": "value", + "another": "6", + }, + checkpoint_config=checkpoint_config, + copy_checkpoints_from_job=copy_checkpoints_from_job, + role_arn=role_arn, + stopping_condition=stopping_condition, + output_data_config=output_data_config, + aws_session=aws_session, + tags=tags, + logger=logger, + ) + included_module = importlib.import_module("job_module") + mock_register.assert_called_with(included_module) + mock_unregister.assert_called_with(included_module) + mock_copy.assert_called_with( + Path("my_requirements.txt").resolve(), Path(mock_tempdir_name, "requirements.txt") + ) + assert mock_tempdir.__exit__.called + mock_stdout.write.assert_any_call(s3_not_linked) + + +@patch.object(sys.modules["braket.jobs.hybrid_job"], "retrieve_image") +@patch("time.time", return_value=123.0) +@patch("builtins.open") +@patch("tempfile.TemporaryDirectory") +@patch.object(AwsQuantumJob, "create") +def test_decorator_non_dict_input( + mock_create, mock_tempdir, _mock_open, mock_time, mock_retrieve, aws_session +): + mock_retrieve.return_value = "00000000.dkr.ecr.us-west-2.amazonaws.com/latest" + input_prefix = "my_input" + + @hybrid_job(device=None, input_data=input_prefix, aws_session=aws_session) + def my_entry(): + return "my entry return value" + + mock_tempdir_name = "job_temp_dir_00000" + mock_tempdir.return_value.__enter__.return_value = mock_tempdir_name + + source_module = mock_tempdir_name + entry_point = f"{mock_tempdir_name}.entry_point:my_entry" + wait_until_complete = False + + device = "local:none/none" + + my_entry() + + mock_create.assert_called_with( + device=device, + source_module=source_module, + entry_point=entry_point, + wait_until_complete=wait_until_complete, + job_name="my-entry-123000", + hyperparameters={}, + logger=getLogger("braket.jobs.hybrid_job"), + input_data=input_prefix, + aws_session=aws_session, + ) + assert mock_tempdir.return_value.__exit__.called + + +@patch.object(sys.modules["braket.jobs.hybrid_job"], "retrieve_image") +@patch("time.time", return_value=123.0) +@patch("builtins.open") +@patch("tempfile.TemporaryDirectory") +@patch.object(AwsQuantumJob, "create") +def test_decorator_list_dependencies( + mock_create, mock_tempdir, _mock_open, mock_time, mock_retrieve, aws_session +): + mock_retrieve.return_value = "00000000.dkr.ecr.us-west-2.amazonaws.com/latest" + dependency_list = ["dep_1", "dep_2", "dep_3"] + + @hybrid_job( + device=None, + aws_session=aws_session, + dependencies=dependency_list, + ) + def my_entry(c=0, d: float = 1.0, **extras): + return "my entry return value" + + mock_tempdir_name = "job_temp_dir_00000" + mock_tempdir.return_value.__enter__.return_value = mock_tempdir_name + + source_module = mock_tempdir_name + entry_point = f"{mock_tempdir_name}.entry_point:my_entry" + wait_until_complete = False + + device = "local:none/none" + + my_entry() + + mock_create.assert_called_with( + device=device, + source_module=source_module, + entry_point=entry_point, + wait_until_complete=wait_until_complete, + job_name="my-entry-123000", + hyperparameters={"c": "0", "d": "1.0"}, + logger=getLogger("braket.jobs.hybrid_job"), + aws_session=aws_session, + ) + assert mock_tempdir.return_value.__exit__.called + _mock_open.assert_called_with(Path(mock_tempdir_name) / "requirements.txt", "w") + _mock_open.return_value.__enter__.return_value.write.assert_called_with( + "\n".join(dependency_list) + ) + + +@patch.object(sys.modules["braket.jobs.hybrid_job"], "retrieve_image") +@patch("time.time", return_value=123.0) +@patch("builtins.open") +@patch("tempfile.TemporaryDirectory") +@patch.object(LocalQuantumJob, "create") +def test_decorator_local( + mock_create, mock_tempdir, _mock_open, mock_time, mock_retrieve, aws_session +): + mock_retrieve.return_value = "00000000.dkr.ecr.us-west-2.amazonaws.com/latest" + + @hybrid_job(device=Devices.Amazon.SV1, local=True, aws_session=aws_session) + def my_entry(): + return "my entry return value" + + mock_tempdir_name = "job_temp_dir_00000" + mock_tempdir.return_value.__enter__.return_value = mock_tempdir_name + + device = Devices.Amazon.SV1 + source_module = mock_tempdir_name + entry_point = f"{mock_tempdir_name}.entry_point:my_entry" + + my_entry() + + mock_create.assert_called_with( + device=device, + source_module=source_module, + entry_point=entry_point, + job_name="my-entry-123000", + hyperparameters={}, + aws_session=aws_session, + ) + assert mock_tempdir.return_value.__exit__.called + + +@patch.object(sys.modules["braket.jobs.hybrid_job"], "retrieve_image") +@patch("time.time", return_value=123.0) +@patch("builtins.open") +@patch("tempfile.TemporaryDirectory") +@patch.object(LocalQuantumJob, "create") +def test_decorator_local_unsupported_args( + mock_create, mock_tempdir, _mock_open, mock_time, mock_retrieve, aws_session +): + mock_retrieve.return_value = "00000000.dkr.ecr.us-west-2.amazonaws.com/latest" + + @hybrid_job( + device=Devices.Amazon.SV1, + local=True, + wait_until_complete=True, + copy_checkpoints_from_job="arn/other-job", + instance_config=InstanceConfig(), + distribution="data_parallel", + stopping_condition=StoppingCondition(), + tags={"my_tag": "my_value"}, + logger=getLogger(__name__), + aws_session=aws_session, + ) + def my_entry(): + return "my entry return value" + + mock_tempdir_name = "job_temp_dir_00000" + mock_tempdir.return_value.__enter__.return_value = mock_tempdir_name + + device = Devices.Amazon.SV1 + source_module = mock_tempdir_name + entry_point = f"{mock_tempdir_name}.entry_point:my_entry" + + my_entry() + + mock_create.assert_called_with( + device=device, + source_module=source_module, + entry_point=entry_point, + job_name="my-entry-123000", + hyperparameters={}, + aws_session=aws_session, + ) + assert mock_tempdir.return_value.__exit__.called + + +@patch.object(sys.modules["braket.jobs.hybrid_job"], "retrieve_image") +@patch("time.time", return_value=123.0) +@patch("builtins.open") +@patch("tempfile.TemporaryDirectory") +@patch.object(AwsQuantumJob, "create") +def test_job_name_too_long( + mock_create, mock_tempdir, _mock_open, mock_time, mock_retrieve, aws_session +): + mock_retrieve.return_value = "00000000.dkr.ecr.us-west-2.amazonaws.com/latest" + + @hybrid_job(device="local:braket/default", aws_session=aws_session) + def this_is_a_50_character_func_name_for_testing_names(): + return "my entry return value" + + mock_tempdir_name = "job_temp_dir_00000" + mock_tempdir.return_value.__enter__.return_value = mock_tempdir_name + + device = "local:braket/default" + source_module = mock_tempdir_name + entry_point = ( + f"{mock_tempdir_name}.entry_point:this_is_a_50_character_func_name_for_testing_names" + ) + wait_until_complete = False + + with pytest.warns(UserWarning): + this_is_a_50_character_func_name_for_testing_names() + + expected_job_name = "this-is-a-50-character-func-name-for-testin-123000" + + mock_create.assert_called_with( + device=device, + source_module=source_module, + entry_point=entry_point, + wait_until_complete=wait_until_complete, + job_name=expected_job_name, + hyperparameters={}, + logger=getLogger("braket.jobs.hybrid_job"), + aws_session=aws_session, + ) + assert len(expected_job_name) == 50 + assert mock_tempdir.return_value.__exit__.called + + +@patch.object(sys.modules["braket.jobs.hybrid_job"], "retrieve_image") +@patch("time.time", return_value=123.0) +@patch("builtins.open") +@patch("tempfile.TemporaryDirectory") +@patch.object(AwsQuantumJob, "create") +def test_decorator_pos_only_slash( + mock_create, mock_tempdir, _mock_open, mock_time, mock_retrieve, aws_session +): + mock_retrieve.return_value = "00000000.dkr.ecr.us-west-2.amazonaws.com/latest" + + @hybrid_job(device="local:braket/default", aws_session=aws_session) + def my_entry(pos_only, /): + return "my entry return value" + + mock_tempdir_name = "job_temp_dir_00000" + mock_tempdir.return_value.__enter__.return_value = mock_tempdir_name + + device = "local:braket/default" + source_module = mock_tempdir_name + entry_point = f"{mock_tempdir_name}.entry_point:my_entry" + wait_until_complete = False + + pos_only_warning = "Positional only arguments will not be logged to the hyperparameters file." + with pytest.warns(match=pos_only_warning): + my_entry("pos_only") + + mock_create.assert_called_with( + device=device, + source_module=source_module, + entry_point=entry_point, + wait_until_complete=wait_until_complete, + job_name="my-entry-123000", + hyperparameters={}, + logger=getLogger("braket.jobs.hybrid_job"), + aws_session=aws_session, + ) + assert mock_tempdir.return_value.__exit__.called + + +@patch.object(sys.modules["braket.jobs.hybrid_job"], "retrieve_image") +@patch("time.time", return_value=123.0) +@patch("builtins.open") +@patch("tempfile.TemporaryDirectory") +@patch.object(AwsQuantumJob, "create") +def test_decorator_pos_only_args( + mock_create, mock_tempdir, _mock_open, mock_time, mock_retrieve, aws_session +): + mock_retrieve.return_value = "00000000.dkr.ecr.us-west-2.amazonaws.com/latest" + + @hybrid_job(device="local:braket/default", aws_session=aws_session) + def my_entry(*args): + return "my entry return value" + + mock_tempdir_name = "job_temp_dir_00000" + mock_tempdir.return_value.__enter__.return_value = mock_tempdir_name + + device = "local:braket/default" + source_module = mock_tempdir_name + entry_point = f"{mock_tempdir_name}.entry_point:my_entry" + wait_until_complete = False + + pos_only_warning = "Positional only arguments will not be logged to the hyperparameters file." + with pytest.warns(match=pos_only_warning): + my_entry("pos_only") + + mock_create.assert_called_with( + device=device, + source_module=source_module, + entry_point=entry_point, + wait_until_complete=wait_until_complete, + job_name="my-entry-123000", + hyperparameters={}, + logger=getLogger("braket.jobs.hybrid_job"), + aws_session=aws_session, + ) + assert mock_tempdir.return_value.__exit__.called + + +def test_serialization_error(aws_session): + ssl_context = SSLContext() + + @hybrid_job(device=None, aws_session=aws_session) + def fails_serialization(): + print(ssl_context) + + serialization_failed = ( + "Serialization failed for decorator hybrid job. If you are referencing " + "an object from outside the function scope, either directly or through " + "function parameters, try instantiating the object inside the decorated " + "function instead." + ) + with pytest.raises(RuntimeError, match=serialization_failed): + fails_serialization() + + +def test_serialization_wrapping(): + def my_entry(*args, **kwargs): + print("something with \" and ' and \n") + return args, kwargs + + args, kwargs = (1, "two"), {"three": 3} + template = _serialize_entry_point(my_entry, args, kwargs) + pickled_str = re.search(r"(?s)cloudpickle.loads\((.*?)\)\ndef my_entry", template).group(1) + byte_str = ast.literal_eval(pickled_str) + + recovered = cloudpickle.loads(byte_str) + assert recovered() == (args, kwargs) + + +def test_python_validation(aws_session): + aws_session.get_full_image_tag.return_value = "1.0-cpu-py38-ubuntu22.04" + + bad_version = ( + "Python version must match between local environment and container. " + f"Client is running Python {sys.version_info.major}.{sys.version_info.minor} " + "locally, but container uses Python 3.8." + ) + with pytest.raises(RuntimeError, match=bad_version): + + @hybrid_job(device=None, aws_session=aws_session) + def my_job(): + pass + + +@pytest.mark.parametrize( + "hyperparameter, expected", + ( + ( + "with\nnewline", + "with newline", + ), + ( + "with weird chars: (&$`)", + "with weird chars: {+?'}", + ), + ( + "?" * 2600, + f"{'?' * 2477}...{'?' * 20}", + ), + ), +) +def test_sanitize_hyperparameters(hyperparameter, expected): + assert _sanitize(hyperparameter) == expected diff --git a/test/unit_tests/braket/jobs/test_quantum_job_creation.py b/test/unit_tests/braket/jobs/test_quantum_job_creation.py index f1a1f2fc6..1b2c6b5b4 100644 --- a/test/unit_tests/braket/jobs/test_quantum_job_creation.py +++ b/test/unit_tests/braket/jobs/test_quantum_job_creation.py @@ -22,7 +22,7 @@ import pytest from braket.aws import AwsSession -from braket.jobs import Framework, image_uris +from braket.jobs import Framework, retrieve_image from braket.jobs.config import ( CheckpointConfig, InstanceConfig, @@ -349,7 +349,7 @@ def _translate_creation_args(create_job_args): "compressionType": "GZIP", } } - image_uri = image_uri or image_uris.retrieve_image(Framework.BASE, aws_session.region) + image_uri = image_uri or retrieve_image(Framework.BASE, aws_session.region) algorithm_specification["containerImage"] = {"uri": image_uri} tags = create_job_args.get("tags", {}) @@ -381,7 +381,8 @@ def test_generate_default_job_name(mock_time, image_uri): } job_type = job_type_mapping[image_uri] mock_time.return_value = datetime.datetime.now().timestamp() - assert _generate_default_job_name(image_uri) == f"braket-job{job_type}-{time.time() * 1000:.0f}" + timestamp = str(int(time.time() * 1000)) + assert _generate_default_job_name(image_uri) == f"braket-job{job_type}-{timestamp}" @pytest.mark.parametrize( diff --git a/test/unit_tests/braket/parametric/test_free_parameter.py b/test/unit_tests/braket/parametric/test_free_parameter.py index b3b391a8e..816bc0cee 100644 --- a/test/unit_tests/braket/parametric/test_free_parameter.py +++ b/test/unit_tests/braket/parametric/test_free_parameter.py @@ -61,36 +61,3 @@ def test_sub_successful(free_parameter): def test_sub_wrong_param(free_parameter): assert free_parameter.subs({"alpha": 1}) == FreeParameter("theta") - - -@pytest.mark.parametrize( - "name", - ( - "a", - "b", - "q", - "bit", - "qubit", - "_a", - "\u03B8", - "a\u03B8", - "a123", - "z123", - "\u03B8\u03B8", - "\u03B8a1", - ), -) -def test_valid_names(name): - FreeParameter(name) - - -@pytest.mark.parametrize( - "name", - ( - "", - "__a", - ), -) -def test_invalid_names(name): - with pytest.raises(ValueError): - FreeParameter(name) diff --git a/test/unit_tests/braket/pulse/ast/test_approximation_parser.py b/test/unit_tests/braket/pulse/ast/test_approximation_parser.py index 786c6b7e6..c4199e81b 100644 --- a/test/unit_tests/braket/pulse/ast/test_approximation_parser.py +++ b/test/unit_tests/braket/pulse/ast/test_approximation_parser.py @@ -19,12 +19,12 @@ from openpulse import ast from oqpy import IntVar -from braket.circuits.qubit_set import QubitSet from braket.pulse import ArbitraryWaveform, ConstantWaveform, DragGaussianWaveform, GaussianWaveform from braket.pulse.ast.approximation_parser import _ApproximationParser from braket.pulse.frame import Frame from braket.pulse.port import Port from braket.pulse.pulse_sequence import PulseSequence +from braket.registers.qubit_set import QubitSet from braket.timings.time_series import TimeSeries, _all_close diff --git a/test/unit_tests/braket/circuits/test_qubit.py b/test/unit_tests/braket/registers/test_qubit.py similarity index 97% rename from test/unit_tests/braket/circuits/test_qubit.py rename to test/unit_tests/braket/registers/test_qubit.py index b961986c1..98f89cf8d 100644 --- a/test/unit_tests/braket/circuits/test_qubit.py +++ b/test/unit_tests/braket/registers/test_qubit.py @@ -14,7 +14,7 @@ import numpy as np import pytest -from braket.circuits import Qubit +from braket.registers import Qubit @pytest.fixture diff --git a/test/unit_tests/braket/circuits/test_qubit_set.py b/test/unit_tests/braket/registers/test_qubit_set.py similarity index 97% rename from test/unit_tests/braket/circuits/test_qubit_set.py rename to test/unit_tests/braket/registers/test_qubit_set.py index 4a7eda395..1fd8d7212 100644 --- a/test/unit_tests/braket/circuits/test_qubit_set.py +++ b/test/unit_tests/braket/registers/test_qubit_set.py @@ -13,7 +13,7 @@ import pytest -from braket.circuits import Qubit, QubitSet +from braket.registers import Qubit, QubitSet @pytest.fixture diff --git a/test/unit_tests/braket/tasks/test_gate_model_quantum_task_result.py b/test/unit_tests/braket/tasks/test_gate_model_quantum_task_result.py index 7a9ae3a20..8c0f8d9a9 100644 --- a/test/unit_tests/braket/tasks/test_gate_model_quantum_task_result.py +++ b/test/unit_tests/braket/tasks/test_gate_model_quantum_task_result.py @@ -26,6 +26,8 @@ ResultTypeValue, TaskMetadata, ) +from braket.task_result.oqc_metadata_v1 import OqcMetadata +from braket.task_result.rigetti_metadata_v1 import RigettiMetadata from braket.tasks import GateModelQuantumTaskResult @@ -61,6 +63,79 @@ def additional_metadata_openqasm(): return AdditionalMetadata(action=program) +@pytest.fixture +def quil_program(): + return """ +PRAGMA INITIAL_REWIRING "NAIVE" +RESET +DECLARE ro BIT[2] +PRAGMA PRESERVE_BLOCK +RX(1.5707963267948966) 0 +RX(1.5707963267948966) 7 +RZ(1.5707963267948966) 0 +RZ(1.5707963267948966) 7 +RX(-1.5707963267948966) 7 +CZ 0 7 +RZ(3.141592653589793) 7 +RX(1.5707963267948966) 7 +RZ(1.5707963267948966) 7 +RX(-1.5707963267948966) 7 +PRAGMA END_PRESERVE_BLOCK +MEASURE 0 ro[0] +MEASURE 7 ro[1] +""" + + +@pytest.fixture +def additional_metadata_rigetti(quil_program): + program = openqasm.Program( + source=""" + OPENQASM 3.0; + bit[2] b; + h $0; + cnot $0, $7; + b[0] = measure $0; + b[1] = measure $7; + """ + ) + rigetti_metadata = RigettiMetadata(compiledProgram=quil_program) + + return AdditionalMetadata(action=program, rigettiMetadata=rigetti_metadata) + + +@pytest.fixture +def qasm2_program(): + return """ + OPENQASM 2.0; + include "qelib1.inc"; + + qreg node[8]; + creg b[2]; + u3(0.5*pi,0.0*pi,1.0*pi) node[0]; + cx node[0],node[1]; + measure node[0] -> b[0]; + measure node[1] -> b[1]; + """ + + +@pytest.fixture +def additional_metadata_oqc(qasm2_program): + program = openqasm.Program( + source=""" + OPENQASM 3.0; + bit[2] b; + qubit[8] q; + h q[0]; + cnot q[0], q[7]; + b[0] = measure q[0]; + b[1] = measure q[7]; + """ + ) + oqc_metadata = OqcMetadata(compiledProgram=qasm2_program) + + return AdditionalMetadata(action=program, oqcMetadata=oqc_metadata) + + @pytest.fixture def result_obj_1(task_metadata_shots, additional_metadata): return GateModelTaskResult( @@ -71,6 +146,20 @@ def result_obj_1(task_metadata_shots, additional_metadata): ) +@pytest.fixture +def result_rigetti(result_obj_1, additional_metadata_rigetti): + result = GateModelQuantumTaskResult.from_object(result_obj_1) + result.additional_metadata = additional_metadata_rigetti + return result + + +@pytest.fixture +def result_oqc(result_obj_1, additional_metadata_oqc): + result = GateModelQuantumTaskResult.from_object(result_obj_1) + result.additional_metadata = additional_metadata_oqc + return result + + @pytest.fixture def result_str_1(result_obj_1): return result_obj_1.json() @@ -234,6 +323,29 @@ def test_openqasm_shots_calculate_result_types(openqasm_result_obj_shots): ] +def test_get_compiled_circuit_rigetti(result_rigetti, quil_program): + """Test get_compiled_circuit method.""" + assert result_rigetti.get_compiled_circuit() == quil_program + + +def test_get_compiled_circuit_oqc(result_oqc, qasm2_program): + """Test get_compiled_circuit method.""" + assert result_oqc.get_compiled_circuit() == qasm2_program + + +def test_get_compiled_circuit_no_qhp_metadata(result_obj_1): + """Test get_compiled_circuit method.""" + result = GateModelQuantumTaskResult.from_object(result_obj_1) + assert result.get_compiled_circuit() is None + + +def test_get_compiled_circuit_no_metadata(result_obj_1): + """Test that the method does not raise an error if metadata is missing.""" + result = GateModelQuantumTaskResult.from_object(result_obj_1) + result.additional_metadata = None + assert result.get_compiled_circuit() is None + + def test_measurement_counts_from_measurements(): measurements: np.ndarray = np.array( [[1, 0, 1, 0], [0, 0, 0, 0], [1, 0, 1, 0], [1, 0, 0, 0], [1, 0, 0, 0], [1, 0, 1, 0]] diff --git a/tox.ini b/tox.ini index af1fd9097..b77ac4525 100644 --- a/tox.ini +++ b/tox.ini @@ -41,16 +41,29 @@ commands = {[testenv:black]commands} {[testenv:flake8]commands} +# Read only linter env +[testenv:linters_check] +basepython = python3 +skip_install = true +deps = + {[testenv:isort_check]deps} + {[testenv:black_check]deps} + {[testenv:flake8]deps} +commands = + {[testenv:isort_check]commands} + {[testenv:black_check]commands} + {[testenv:flake8]commands} + [testenv:flake8] basepython = python3 skip_install = true deps = flake8 flake8-rst-docstrings - git+https://github.com/aws/amazon-braket-build-tools.git + git+https://github.com/amazon-braket/amazon-braket-build-tools.git commands = - flake8 {posargs} - flake8 --enable-extensions=BCS src + flake8 --extend-exclude src {posargs} + flake8 --enable-extensions=BCS src {posargs} [testenv:isort] basepython = python3 @@ -60,6 +73,14 @@ deps = commands = isort . {posargs} +[testenv:isort_check] +basepython = python3 +skip_install = true +deps = + isort +commands = + isort . -c {posargs} + [testenv:black] basepython = python3 skip_install = true @@ -68,6 +89,14 @@ deps = commands = black ./ {posargs} +[testenv:black_check] +basepython = python3 +skip_install = true +deps = + black +commands = + black --check ./ {posargs} + [testenv:docs] basepython = python3 deps = @@ -94,5 +123,5 @@ commands = [test-deps] deps = # If you need to test on a certain branch, add @ after .git - git+https://github.com/aws/amazon-braket-schemas-python.git - git+https://github.com/aws/amazon-braket-default-simulator-python.git + git+https://github.com/amazon-braket/amazon-braket-schemas-python.git + git+https://github.com/amazon-braket/amazon-braket-default-simulator-python.git From 7d55f063888eaabc214269d31b8c17e89ab72127 Mon Sep 17 00:00:00 2001 From: Jean-Christophe Jaskula Date: Sun, 25 Feb 2024 23:23:30 -0500 Subject: [PATCH 08/26] fix merge --- src/braket/circuits/circuit.py | 12 ++++++------ src/braket/circuits/observable.py | 1 + src/braket/circuits/observables.py | 1 + src/braket/circuits/result_type.py | 1 + src/braket/circuits/result_types.py | 5 ++++- src/braket/parametric/free_parameter_expression.py | 2 -- src/braket/pulse/ast/free_parameters.py | 2 +- 7 files changed, 14 insertions(+), 10 deletions(-) diff --git a/src/braket/circuits/circuit.py b/src/braket/circuits/circuit.py index 1fe6e6803..33f5dc6ee 100644 --- a/src/braket/circuits/circuit.py +++ b/src/braket/circuits/circuit.py @@ -20,6 +20,7 @@ import numpy as np import oqpy +from braket.aws import AwsDevice from braket.circuits import compiler_directives from braket.circuits.ascii_circuit_diagram import AsciiCircuitDiagram from braket.circuits.circuit_pulse_sequence import CircuitPulseSequenceBuilder @@ -1099,17 +1100,16 @@ def diagram(self, circuit_diagram_class: type = AsciiCircuitDiagram) -> str: def pulse_sequence( self, device: AwsDevice, - gate_definitions: Optional[dict[tuple[Gate, QubitSet], PulseSequence]] = None, - pulse_sequence_builder_class: Type = CircuitPulseSequenceBuilder, + gate_definitions: dict[tuple[Gate, QubitSet], PulseSequence] | None = None, + pulse_sequence_builder_class: type = CircuitPulseSequenceBuilder, ) -> PulseSequence: - """ - Get the associated pulse sequence for the current circuit. + """Get the associated pulse sequence for the current circuit. Args: device (AwsDevice): an AWS device. - gate_definitions (Optional[dict[tuple[Gate, QubitSet], PulseSequence]]): Additional + gate_definitions (dict[tuple[Gate, QubitSet], PulseSequence] | None): Additional gate definitions. - pulse_sequence_builder_class (Type): A `CircuitPulseSequenceBuilder` class that builds + pulse_sequence_builder_class (type): A `CircuitPulseSequenceBuilder` class that builds the pulse sequence for this circuit. Default = `CircuitPulseSequenceBuilder`. Returns: diff --git a/src/braket/circuits/observable.py b/src/braket/circuits/observable.py index 7e45b7b41..2eb0e981f 100644 --- a/src/braket/circuits/observable.py +++ b/src/braket/circuits/observable.py @@ -27,6 +27,7 @@ OpenQASMSerializationProperties, SerializationProperties, ) +from braket.pulse.pulse_sequence import PulseSequence from braket.registers.qubit_set import QubitSet diff --git a/src/braket/circuits/observables.py b/src/braket/circuits/observables.py index 7769f6cb7..2fa24a018 100644 --- a/src/braket/circuits/observables.py +++ b/src/braket/circuits/observables.py @@ -30,6 +30,7 @@ verify_quantum_operator_matrix_dimensions, ) from braket.circuits.serialization import IRType, OpenQASMSerializationProperties +from braket.pulse.pulse_sequence import PulseSequence from braket.registers.qubit_set import QubitSet diff --git a/src/braket/circuits/result_type.py b/src/braket/circuits/result_type.py index 751d61c37..891bfb3df 100644 --- a/src/braket/circuits/result_type.py +++ b/src/braket/circuits/result_type.py @@ -23,6 +23,7 @@ OpenQASMSerializationProperties, SerializationProperties, ) +from braket.pulse.pulse_sequence import PulseSequence from braket.registers.qubit import QubitInput from braket.registers.qubit_set import QubitSet, QubitSetInput diff --git a/src/braket/circuits/result_types.py b/src/braket/circuits/result_types.py index c08b999ce..a7a0ef3bd 100644 --- a/src/braket/circuits/result_types.py +++ b/src/braket/circuits/result_types.py @@ -28,6 +28,7 @@ ResultType, ) from braket.circuits.serialization import IRType, OpenQASMSerializationProperties +from braket.pulse.pulse_sequence import PulseSequence from braket.registers.qubit_set import QubitSet, QubitSetInput """ @@ -492,7 +493,9 @@ def _to_openqasm(self, serialization_properties: OpenQASMSerializationProperties ) return f"#pragma braket result expectation {observable_ir}" - def to_pulse_sequence(self, serialization_properties: OpenQASMSerializationProperties) -> str: + def to_pulse_sequence( + self, serialization_properties: OpenQASMSerializationProperties + ) -> PulseSequence: observable_pulse_sequence = self.observable.to_pulse_sequence( target=self.target, ir_type=IRType.OPENQASM, diff --git a/src/braket/parametric/free_parameter_expression.py b/src/braket/parametric/free_parameter_expression.py index 3da8e78a3..d3ad91259 100644 --- a/src/braket/parametric/free_parameter_expression.py +++ b/src/braket/parametric/free_parameter_expression.py @@ -41,7 +41,6 @@ def __init__(self, expression: Union[FreeParameterExpression, Number, sympy.Expr Args: expression (Union[FreeParameterExpression, Number, Expr, str]): The expression to use. - _type (Optional[ClassicalType]): type of the expression Raises: NotImplementedError: Raised if the expression is not of type @@ -58,7 +57,6 @@ def __init__(self, expression: Union[FreeParameterExpression, Number, sympy.Expr ast.Pow: self.__pow__, ast.USub: self.__neg__, } - self._type = _type if _type is not None else FloatType() if isinstance(expression, FreeParameterExpression): self._expression = expression.expression elif isinstance(expression, (Number, sympy.Expr)): diff --git a/src/braket/pulse/ast/free_parameters.py b/src/braket/pulse/ast/free_parameters.py index 020ae9918..41c541da8 100644 --- a/src/braket/pulse/ast/free_parameters.py +++ b/src/braket/pulse/ast/free_parameters.py @@ -37,7 +37,7 @@ def visit_Identifier( using the given parameter values. Args: - identifier (_FreeParameterExpressionIdentifier): The identifier. + identifier (Identifier): The identifier. Returns: Union[Identifier, FloatLiteral]: The transformed identifier. From 96fd4f74adfe5d8674f1459ef867951c6fbc8275 Mon Sep 17 00:00:00 2001 From: Jean-Christophe Jaskula Date: Tue, 19 Mar 2024 11:18:12 -0400 Subject: [PATCH 09/26] revamp pulsegate and result_type --- src/braket/circuits/__init__.py | 1 + src/braket/circuits/circuit.py | 6 +- src/braket/circuits/circuit_pulse_sequence.py | 58 +++----- src/braket/pulse/pulse_sequence.py | 31 ++++ .../circuits/test_circuit_pulse_sequence.py | 140 ++++++++++++++++++ 5 files changed, 198 insertions(+), 38 deletions(-) create mode 100644 test/unit_tests/braket/circuits/test_circuit_pulse_sequence.py diff --git a/src/braket/circuits/__init__.py b/src/braket/circuits/__init__.py index d2788746c..fcf4224cf 100644 --- a/src/braket/circuits/__init__.py +++ b/src/braket/circuits/__init__.py @@ -23,6 +23,7 @@ from braket.circuits.ascii_circuit_diagram import AsciiCircuitDiagram # noqa: F401 from braket.circuits.circuit import Circuit # noqa: F401 from braket.circuits.circuit_diagram import CircuitDiagram # noqa: F401 +from braket.circuits.circuit_pulse_sequence import CircuitPulseSequenceBuilder # noqa: F401 from braket.circuits.compiler_directive import CompilerDirective # noqa: F401 from braket.circuits.free_parameter import FreeParameter # noqa: F401 from braket.circuits.free_parameter_expression import FreeParameterExpression # noqa: F401 diff --git a/src/braket/circuits/circuit.py b/src/braket/circuits/circuit.py index 33f5dc6ee..8c908ddc8 100644 --- a/src/braket/circuits/circuit.py +++ b/src/braket/circuits/circuit.py @@ -15,12 +15,11 @@ from collections.abc import Callable, Iterable from numbers import Number -from typing import Any, Optional, TypeVar, Union +from typing import TYPE_CHECKING, Any, Optional, TypeVar, Union import numpy as np import oqpy -from braket.aws import AwsDevice from braket.circuits import compiler_directives from braket.circuits.ascii_circuit_diagram import AsciiCircuitDiagram from braket.circuits.circuit_pulse_sequence import CircuitPulseSequenceBuilder @@ -64,6 +63,9 @@ from braket.registers.qubit import QubitInput from braket.registers.qubit_set import QubitSet, QubitSetInput +if TYPE_CHECKING: # pragma: no cover + from braket.aws import AwsDevice + SubroutineReturn = TypeVar( "SubroutineReturn", Iterable[Instruction], Instruction, ResultType, Iterable[ResultType] ) diff --git a/src/braket/circuits/circuit_pulse_sequence.py b/src/braket/circuits/circuit_pulse_sequence.py index ae3c87493..8cae14178 100644 --- a/src/braket/circuits/circuit_pulse_sequence.py +++ b/src/braket/circuits/circuit_pulse_sequence.py @@ -19,11 +19,12 @@ from braket.aws.aws_device import AwsDevice import braket.circuits.circuit as cir +from braket.circuits.gate import Gate from braket.circuits.gate_calibrations import GateCalibrations -from braket.circuits.gates import Gate, PulseGate from braket.circuits.qubit_set import QubitSet from braket.circuits.result_type import ResultType from braket.circuits.serialization import IRType +from braket.pulse.frame import Frame from braket.pulse.pulse_sequence import PulseSequence @@ -32,13 +33,16 @@ class CircuitPulseSequenceBuilder: def __init__( self, - device: AwsDevice, - gate_definitions: Optional[Dict[Tuple[Gate, QubitSet], PulseSequence]] = None, + device: AwsDevice | None = None, + gate_definitions: Dict[Tuple[Gate, QubitSet], PulseSequence] | None = None, ) -> None: self._device = device - self._gate_calibrations = GateCalibrations( - gate_definitions if gate_definitions is not None else {} - ) + if gate_definitions is not None: + self._gate_calibrations = GateCalibrations(gate_definitions) + elif device is not None and device.gate_calibrations is not None: + self._gate_calibrations = device.gate_calibrations + else: + self._gate_calibrations = GateCalibrations({}) def build_pulse_sequence(self, circuit: cir.Circuit) -> PulseSequence: """ @@ -66,10 +70,12 @@ def build_pulse_sequence(self, circuit: cir.Circuit) -> PulseSequence: for instruction in circuit.instructions: gate = instruction.operator qubit = instruction.target - if isinstance(gate, PulseGate): + if isinstance(gate, Gate) and gate.name == "PulseGate": gate_pulse_sequence = gate.pulse_sequence elif ( - gate_pulse_sequence := self._gate_calibrations.pulse_sequences[(gate, qubit)] + gate_pulse_sequence := self._gate_calibrations.pulse_sequences.get( + (gate, qubit), None + ) ) is None: raise ValueError( f"No pulse sequence for {gate.name} was provided in the gate" @@ -80,45 +86,25 @@ def build_pulse_sequence(self, circuit: cir.Circuit) -> PulseSequence: # would be applied to everything pulse_sequence += gate_pulse_sequence - if circuit.result_types: - for result_type in circuit.result_types: - pragma_str = result_type.to_ir(IRType.OPENQASM) - if pragma_str[:8] == "#pragma ": - pulse_sequence._program.pragma(pragma_str[8:]) - else: - raise ValueError("Result type cannot be used with pulse sequences.") - else: - for qubit in circuit.qubits: - pulse_sequence.capture_v0(self._readout_frame(qubit)) - # if ( - # measure_pulse_sequence := self._gate_calibrations.pulse_sequences[ - # ("MEASURE", qubit) - # ] - # ) is None: - # raise ValueError( - # "No pulse sequence for the measurement instruction was provided" - # " in the gate calibration set." - # ) - # pulse_sequence += measure_pulse_sequence - # Result type columns ( additional_result_types, - _, + target_result_types, ) = CircuitPulseSequenceBuilder._categorize_result_types(circuit.result_types) + for result_type in target_result_types: + pragma_str = result_type.to_ir(IRType.OPENQASM) + if pragma_str[:8] == "#pragma ": + pulse_sequence._program.pragma(pragma_str[8:]) + else: + raise ValueError("Result type cannot be used with pulse sequences.") + # Additional result types line on bottom if additional_result_types: print(f"\nAdditional result types: {', '.join(additional_result_types)}") return pulse_sequence - def _readout_frame(self, qubit: QubitSet): - if self._device.name == "Aspen-M-3": - return self._device.frames[f"q{int(qubit)}_ro_rx_frame"] - elif self._device.name == "Lucy": - return self._device.frames[f"r{int(qubit)}_measure"] - @staticmethod def _categorize_result_types( result_types: List[ResultType], diff --git a/src/braket/pulse/pulse_sequence.py b/src/braket/pulse/pulse_sequence.py index e6d78f11f..26b8ac592 100644 --- a/src/braket/pulse/pulse_sequence.py +++ b/src/braket/pulse/pulse_sequence.py @@ -423,6 +423,37 @@ def __call__( param_values[str(key)] = val return self.make_bound_pulse_sequence(param_values) + def __iadd__(self, other: PulseSequence) -> PulseSequence: + """In-place concatenation of pulse sequence.""" + if self._capture_v0_count: + raise ValueError( + "Cannot add a pulse sequence to another one that has already measurement" + " instructions." + ) + for frameId, frame in other._frames.items(): + _validate_uniqueness(self._frames, frame) + self._frames[frameId] = frame + for waveformId, waveform in other._waveforms.items(): + _validate_uniqueness(self._waveforms, waveform) + self._waveforms[waveformId] = waveform + for fp in other._free_parameters: + if fp in self._free_parameters: + raise ValueError( + f"A free parameter with the name {fp.name} already exists in the pulse" + " sequence. Please rename this free parameter." + ) + self._free_parameters.add(fp) + + self._program += other._program + return self + + def __add__(self, other: PulseSequence) -> PulseSequence: + """Return concatenation of two programs.""" + assert isinstance(other, PulseSequence) + self_copy = deepcopy(self) + self_copy += other + return self_copy + def __eq__(self, other: PulseSequence): sort_input_parameters = True return ( diff --git a/test/unit_tests/braket/circuits/test_circuit_pulse_sequence.py b/test/unit_tests/braket/circuits/test_circuit_pulse_sequence.py new file mode 100644 index 000000000..a8480c670 --- /dev/null +++ b/test/unit_tests/braket/circuits/test_circuit_pulse_sequence.py @@ -0,0 +1,140 @@ +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"). You +# may not use this file except in compliance with the License. A copy of +# the License is located at +# +# http://aws.amazon.com/apache2.0/ +# +# or in the "license" file accompanying this file. This file is +# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF +# ANY KIND, either express or implied. See the License for the specific +# language governing permissions and limitations under the License. + +import io +import json +from unittest.mock import Mock, patch + +import numpy as np +import pytest + +from braket.aws import AwsDevice +from braket.circuits import ( + Circuit, + CircuitPulseSequenceBuilder, + FreeParameter, + Gate, + Instruction, + Observable, + Operator, +) +from braket.pulse import Frame, Port, PulseSequence + +from ..aws.common_test_utils import RIGETTI_ARN +from ..aws.test_aws_device import MOCK_PULSE_MODEL_QPU_PULSE_CAPABILITIES_JSON_1, get_pulse_model + +# from braket.circuits.gate_calibrations import GateCalibrations + + +# @patch("urllib.request.urlopen") +# @pytest.fixture +# def device(mock_url_request): +# response_data_content = { +# "gates": { +# "0_1": { +# "cphaseshift": [ +# { +# "name": "cphaseshift", +# "qubits": ["0", "1"], +# "arguments": ["-1.5707963267948966"], +# "calibrations": [ +# { +# "name": "play", +# "arguments": [ +# { +# "name": "waveform", +# "value": "wf_drag_gaussian_0", +# "type": "waveform", +# }, +# { +# "name": "frame", +# "value": "q0_q1_cphase_frame", +# "type": "frame", +# }, +# ], +# }, +# ], +# } +# ], +# "rx_12": [{"name": "rx_12", "qubits": ["0"]}], +# }, +# }, +# "waveforms": { +# "wf_drag_gaussian_0": { +# "waveformId": "wf_drag_gaussian_0", +# "name": "drag_gaussian", +# "arguments": [ +# {"name": "length", "value": 6.000000000000001e-8, "type": "float"}, +# {"name": "sigma", "value": 6.369913502160144e-9, "type": "float"}, +# {"name": "amplitude", "value": -0.4549282253548838, "type": "float"}, +# {"name": "beta", "value": 7.494904522022295e-10, "type": "float"}, +# ], +# }, +# }, +# } + +# response_data_stream = io.BytesIO(json.dumps(response_data_content).encode("utf-8")) +# mock_url_request.return_value.__enter__.return_value = response_data_stream +# mock_session = Mock() +# mock_session.get_device.return_value = get_pulse_model( +# MOCK_PULSE_MODEL_QPU_PULSE_CAPABILITIES_JSON_1 +# ) +# device = AwsDevice(RIGETTI_ARN, mock_session) +# return device + + +@pytest.fixture() +def gate_calibrations(device): + return device.gate_calibrations + + +def test_empty_circuit(): + assert CircuitPulseSequenceBuilder(None).build_pulse_sequence(Circuit()) == PulseSequence() + + +def test_non_parametric_defcal(gate_calibrations): + circ = Circuit().rx(0, np.pi) + expected = ( + "OPENQASM 3.0;", + "cal {", + " bit[1] psb;", + " waveform wf_drag_gaussian_3 = drag_gaussian(24.0ns, 2.547965400864ns, 2.3223415460834803e-10, 0.606950905462208, false);", + " barrier $0;", + " shift_frequency(q0_rf_frame, -321047.14178613486);", + " play(q0_rf_frame, wf_drag_gaussian_3);", + " shift_frequency(q0_rf_frame, 321047.14178613486);", + " barrier $0;", + " psb[0] = capture_v0(q0_ro_rx_frame);", + "}", + ) + + assert circ.pulse_sequence(gate_calibrations).to_ir() == expected + + +def test_parametric_defcal(gate_calibrations): + circ = Circuit().xy(0, 1, 0.1) + expected = ( + "OPENQASM 3.0;", + "cal {", + " bit[1] psb;", + " waveform wf_drag_gaussian_3 = drag_gaussian(24.0ns, 2.547965400864ns, 2.3223415460834803e-10, 0.606950905462208, false);", + " barrier $0;", + " shift_frequency(q0_rf_frame, -321047.14178613486);", + " play(q0_rf_frame, wf_drag_gaussian_3);", + " shift_frequency(q0_rf_frame, 321047.14178613486);", + " barrier $0;", + " psb[0] = capture_v0(q0_ro_rx_frame);", + "}", + ) + + assert circ.pulse_sequence(gate_calibrations).to_ir() == expected From 25f27b33b25d1ec13a91ee3c93a2b6abfa051283 Mon Sep 17 00:00:00 2001 From: Jean-Christophe Jaskula Date: Tue, 19 Mar 2024 11:32:30 -0400 Subject: [PATCH 10/26] add readout --- src/braket/circuits/circuit_pulse_sequence.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/braket/circuits/circuit_pulse_sequence.py b/src/braket/circuits/circuit_pulse_sequence.py index 8cae14178..6394f00e8 100644 --- a/src/braket/circuits/circuit_pulse_sequence.py +++ b/src/braket/circuits/circuit_pulse_sequence.py @@ -98,6 +98,8 @@ def build_pulse_sequence(self, circuit: cir.Circuit) -> PulseSequence: pulse_sequence._program.pragma(pragma_str[8:]) else: raise ValueError("Result type cannot be used with pulse sequences.") + for qubit in circuit.qubits: + pulse_sequence.capture_v0(self._readout_frame(qubit)) # Additional result types line on bottom if additional_result_types: @@ -105,6 +107,12 @@ def build_pulse_sequence(self, circuit: cir.Circuit) -> PulseSequence: return pulse_sequence + def _readout_frame(self, qubit: QubitSet) -> Frame: + if self._device.name == "Aspen-M-3": + return self._device.frames[f"q{int(qubit)}_ro_rx_frame"] + elif self._device.name == "Lucy": + return self._device.frames[f"r{int(qubit)}_measure"] + @staticmethod def _categorize_result_types( result_types: List[ResultType], From 04bcce44477ab2488e79a436860b9fa7a27cce80 Mon Sep 17 00:00:00 2001 From: Jean-Christophe Jaskula Date: Tue, 19 Mar 2024 13:06:34 -0400 Subject: [PATCH 11/26] add _to_pulse_sequence skeleton to result types and observables --- src/braket/circuits/circuit_pulse_sequence.py | 19 +- src/braket/circuits/observable.py | 15 +- src/braket/circuits/observables.py | 21 +- src/braket/circuits/result_type.py | 15 +- src/braket/circuits/result_types.py | 8 +- .../circuits/test_circuit_pulse_sequence.py | 241 ++++++++++++------ 6 files changed, 198 insertions(+), 121 deletions(-) diff --git a/src/braket/circuits/circuit_pulse_sequence.py b/src/braket/circuits/circuit_pulse_sequence.py index 6394f00e8..2556680ca 100644 --- a/src/braket/circuits/circuit_pulse_sequence.py +++ b/src/braket/circuits/circuit_pulse_sequence.py @@ -13,7 +13,7 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Dict, List, Optional, Tuple +from typing import TYPE_CHECKING if TYPE_CHECKING: from braket.aws.aws_device import AwsDevice @@ -23,7 +23,6 @@ from braket.circuits.gate_calibrations import GateCalibrations from braket.circuits.qubit_set import QubitSet from braket.circuits.result_type import ResultType -from braket.circuits.serialization import IRType from braket.pulse.frame import Frame from braket.pulse.pulse_sequence import PulseSequence @@ -34,7 +33,7 @@ class CircuitPulseSequenceBuilder: def __init__( self, device: AwsDevice | None = None, - gate_definitions: Dict[Tuple[Gate, QubitSet], PulseSequence] | None = None, + gate_definitions: dict[tuple[Gate, QubitSet], PulseSequence] | None = None, ) -> None: self._device = device if gate_definitions is not None: @@ -93,11 +92,7 @@ def build_pulse_sequence(self, circuit: cir.Circuit) -> PulseSequence: ) = CircuitPulseSequenceBuilder._categorize_result_types(circuit.result_types) for result_type in target_result_types: - pragma_str = result_type.to_ir(IRType.OPENQASM) - if pragma_str[:8] == "#pragma ": - pulse_sequence._program.pragma(pragma_str[8:]) - else: - raise ValueError("Result type cannot be used with pulse sequences.") + pulse_sequence += result_type._to_pulse_sequence() for qubit in circuit.qubits: pulse_sequence.capture_v0(self._readout_frame(qubit)) @@ -115,16 +110,16 @@ def _readout_frame(self, qubit: QubitSet) -> Frame: @staticmethod def _categorize_result_types( - result_types: List[ResultType], - ) -> Tuple[List[str], List[ResultType]]: + result_types: list[ResultType], + ) -> tuple[list[str], list[ResultType]]: """ Categorize result types into result types with target and those without. Args: - result_types (List[ResultType]): list of result types + result_types (list[ResultType]): list of result types Returns: - Tuple[List[str], List[ResultType]]: first element is a list of result types + tuple[list[str], list[ResultType]]: first element is a list of result types without `target` attribute; second element is a list of result types with `target` attribute """ diff --git a/src/braket/circuits/observable.py b/src/braket/circuits/observable.py index 2eb0e981f..d674e669a 100644 --- a/src/braket/circuits/observable.py +++ b/src/braket/circuits/observable.py @@ -106,23 +106,16 @@ def _to_openqasm( """ raise NotImplementedError("to_openqasm has not been implemented yet.") - def _to_pulse_sequence( - self, - serialization_properties: OpenQASMSerializationProperties, - target: QubitSet | None = None, - ) -> PulseSequence: - """ - Returns the openqasm string representation of the result type. + def _to_pulse_sequence(self, target: QubitSet | None = None) -> PulseSequence: + """Returns the pulse sequence of the result type. Args: - serialization_properties (OpenQASMSerializationProperties): The serialization properties - to use while serializing the object to the IR representation. target (QubitSet | None): target qubit(s). Defaults to None. Returns: - PulseSequence: A PulseSequence corresponding to the full circuit. + PulseSequence: A PulseSequence of the basis rotation for the corresponding observable. """ - raise NotImplementedError("to_pulse_sequence has not been implemented yet.") + raise NotImplementedError("_to_pulse_sequence has not been implemented yet.") @property def coefficient(self) -> int: diff --git a/src/braket/circuits/observables.py b/src/braket/circuits/observables.py index 2fa24a018..b95db2200 100644 --- a/src/braket/circuits/observables.py +++ b/src/braket/circuits/observables.py @@ -66,6 +66,9 @@ def to_matrix(self) -> np.ndarray: 1.0 / np.sqrt(2.0) * np.array([[1.0, 1.0], [1.0, -1.0]], dtype=complex) ) + def _to_pulse_sequence(self, target: QubitSet | None = None) -> PulseSequence: + raise NotImplementedError("_to_pulse_sequence has not been implemented yet.") + @property def basis_rotation_gates(self) -> tuple[Gate, ...]: return tuple([Gate.Ry(-math.pi / 4)]) # noqa: C409 @@ -104,6 +107,9 @@ def _to_openqasm( def to_matrix(self) -> np.ndarray: return self.coefficient * np.eye(2, dtype=complex) + def _to_pulse_sequence(self, target: QubitSet | None = None) -> PulseSequence: + raise NotImplementedError("_to_pulse_sequence has not been implemented yet.") + @property def basis_rotation_gates(self) -> tuple[Gate, ...]: return () @@ -151,13 +157,8 @@ def _to_openqasm( else: return f"{coef_prefix}x all" - def _to_pulse_sequence( - self, serialization_properties: OpenQASMSerializationProperties, target: QubitSet = None - ) -> PulseSequence: - pulse_sequence = PulseSequence() - for gate in self.basis_rotation_gates: - pulse_sequence += gate.pulse_sequence - return pulse_sequence + def _to_pulse_sequence(self, target: QubitSet | None = None) -> PulseSequence: + raise NotImplementedError("_to_pulse_sequence has not been implemented yet.") def to_matrix(self) -> np.ndarray: return self.coefficient * np.array([[0.0, 1.0], [1.0, 0.0]], dtype=complex) @@ -200,6 +201,9 @@ def _to_openqasm( def to_matrix(self) -> np.ndarray: return self.coefficient * np.array([[0.0, -1.0j], [1.0j, 0.0]], dtype=complex) + def _to_pulse_sequence(self, target: QubitSet | None = None) -> PulseSequence: + raise NotImplementedError("_to_pulse_sequence has not been implemented yet.") + @property def basis_rotation_gates(self) -> tuple[Gate, ...]: return tuple([Gate.Z(), Gate.S(), Gate.H()]) # noqa: C409 @@ -238,6 +242,9 @@ def _to_openqasm( def to_matrix(self) -> np.ndarray: return self.coefficient * np.array([[1.0, 0.0], [0.0, -1.0]], dtype=complex) + def _to_pulse_sequence(self, target: QubitSet | None = None) -> PulseSequence: + raise NotImplementedError("_to_pulse_sequence has not been implemented yet.") + @property def basis_rotation_gates(self) -> tuple[Gate, ...]: return () diff --git a/src/braket/circuits/result_type.py b/src/braket/circuits/result_type.py index 891bfb3df..805a63ded 100644 --- a/src/braket/circuits/result_type.py +++ b/src/braket/circuits/result_type.py @@ -118,20 +118,13 @@ def _to_openqasm(self, serialization_properties: OpenQASMSerializationProperties """ raise NotImplementedError("to_openqasm has not been implemented yet.") - def to_pulse_sequence( - self, serialization_properties: OpenQASMSerializationProperties - ) -> PulseSequence: - """ - Returns the openqasm string representation of the result type. - - Args: - serialization_properties (OpenQASMSerializationProperties): The serialization properties - to use while serializing the object to the IR representation. + def _to_pulse_sequence(self) -> PulseSequence: + """Returns the pulse sequence of the result type. Returns: - PulseSequence: A PulseSequence corresponding to the full circuit. + PulseSequence: A PulseSequence corresponding to the result type. """ - raise NotImplementedError("to_pulse_sequence has not been implemented yet.") + raise NotImplementedError("_to_pulse_sequence has not been implemented yet.") def copy( self, diff --git a/src/braket/circuits/result_types.py b/src/braket/circuits/result_types.py index a7a0ef3bd..915bcfe9b 100644 --- a/src/braket/circuits/result_types.py +++ b/src/braket/circuits/result_types.py @@ -493,13 +493,9 @@ def _to_openqasm(self, serialization_properties: OpenQASMSerializationProperties ) return f"#pragma braket result expectation {observable_ir}" - def to_pulse_sequence( - self, serialization_properties: OpenQASMSerializationProperties - ) -> PulseSequence: - observable_pulse_sequence = self.observable.to_pulse_sequence( + def _to_pulse_sequence(self) -> PulseSequence: + observable_pulse_sequence = self.observable._to_pulse_sequence( target=self.target, - ir_type=IRType.OPENQASM, - serialization_properties=serialization_properties, ) return observable_pulse_sequence diff --git a/test/unit_tests/braket/circuits/test_circuit_pulse_sequence.py b/test/unit_tests/braket/circuits/test_circuit_pulse_sequence.py index a8480c670..14b698e89 100644 --- a/test/unit_tests/braket/circuits/test_circuit_pulse_sequence.py +++ b/test/unit_tests/braket/circuits/test_circuit_pulse_sequence.py @@ -19,78 +19,169 @@ import pytest from braket.aws import AwsDevice -from braket.circuits import ( - Circuit, - CircuitPulseSequenceBuilder, - FreeParameter, - Gate, - Instruction, - Observable, - Operator, -) -from braket.pulse import Frame, Port, PulseSequence - -from ..aws.common_test_utils import RIGETTI_ARN -from ..aws.test_aws_device import MOCK_PULSE_MODEL_QPU_PULSE_CAPABILITIES_JSON_1, get_pulse_model - -# from braket.circuits.gate_calibrations import GateCalibrations - - -# @patch("urllib.request.urlopen") -# @pytest.fixture -# def device(mock_url_request): -# response_data_content = { -# "gates": { -# "0_1": { -# "cphaseshift": [ -# { -# "name": "cphaseshift", -# "qubits": ["0", "1"], -# "arguments": ["-1.5707963267948966"], -# "calibrations": [ -# { -# "name": "play", -# "arguments": [ -# { -# "name": "waveform", -# "value": "wf_drag_gaussian_0", -# "type": "waveform", -# }, -# { -# "name": "frame", -# "value": "q0_q1_cphase_frame", -# "type": "frame", -# }, -# ], -# }, -# ], -# } -# ], -# "rx_12": [{"name": "rx_12", "qubits": ["0"]}], -# }, -# }, -# "waveforms": { -# "wf_drag_gaussian_0": { -# "waveformId": "wf_drag_gaussian_0", -# "name": "drag_gaussian", -# "arguments": [ -# {"name": "length", "value": 6.000000000000001e-8, "type": "float"}, -# {"name": "sigma", "value": 6.369913502160144e-9, "type": "float"}, -# {"name": "amplitude", "value": -0.4549282253548838, "type": "float"}, -# {"name": "beta", "value": 7.494904522022295e-10, "type": "float"}, -# ], -# }, -# }, -# } - -# response_data_stream = io.BytesIO(json.dumps(response_data_content).encode("utf-8")) -# mock_url_request.return_value.__enter__.return_value = response_data_stream -# mock_session = Mock() -# mock_session.get_device.return_value = get_pulse_model( -# MOCK_PULSE_MODEL_QPU_PULSE_CAPABILITIES_JSON_1 -# ) -# device = AwsDevice(RIGETTI_ARN, mock_session) -# return device +from braket.circuits import Circuit +from braket.circuits.circuit_pulse_sequence import CircuitPulseSequenceBuilder +from braket.device_schema.rigetti import RigettiDeviceCapabilities +from braket.pulse import PulseSequence + +RIGETTI_ARN = "arn:aws:braket:::device/qpu/rigetti/Aspen-10" + +MOCK_PULSE_MODEL_QPU_PULSE_CAPABILITIES_JSON_1 = { + "braketSchemaHeader": { + "name": "braket.device_schema.pulse.pulse_device_action_properties", + "version": "1", + }, + "supportedQhpTemplateWaveforms": {}, + "supportedFunctions": {}, + "ports": { + "q0_ff": { + "portId": "q0_ff", + "direction": "tx", + "portType": "ff", + "dt": 1e-09, + "qubitMappings": None, + "centerFrequencies": [375000000.0], + "qhpSpecificProperties": None, + } + }, + "frames": { + "q0_q1_cphase_frame": { + "frameId": "q0_q1_cphase_frame", + "portId": "q0_ff", + "frequency": 4276236.85736918, + "centerFrequency": 375000000.0, + "phase": 1.0, + "associatedGate": "cphase", + "qubitMappings": [0, 1], + "qhpSpecificProperties": None, + } + }, + "nativeGateCalibrationsRef": "file://hostname/foo/bar", +} + + +def get_pulse_model(capabilities_json): + device_json = { + "braketSchemaHeader": { + "name": "braket.device_schema.rigetti.rigetti_device_capabilities", + "version": "1", + }, + "service": { + "executionWindows": [ + { + "executionDay": "Everyday", + "windowStartHour": "11:00", + "windowEndHour": "12:00", + } + ], + "shotsRange": [1, 10], + }, + "provider": { + "specs": { + "1Q": { + "0": { + "fActiveReset": 0.9715, + "fRO": 0.951, + "f1QRB": 0.997339217568556, + "f1QRB_std_err": 0.00006690422818326937, + "f1Q_simultaneous_RB": 0.9949723201166536, + "f1Q_simultaneous_RB_std_err": 0.00021695233492231294, + "T1": 0.000010019627401991471, + "T2": 0.000018156447816365015, + } + }, + "2Q": { + "0-1": { + "fCZ": 0.9586440436264603, + "fCZ_std_err": 0.007025921432645824, + "fCPHASE": 0.9287330972713645, + "fCPHASE_std_err": 0.009709406809550082, + "fXY": 0.9755179214520402, + "fXY_std_err": 0.0060234488782598536, + }, + }, + } + }, + "action": { + "braket.ir.jaqcd.program": { + "actionType": "braket.ir.jaqcd.program", + "version": ["1"], + "supportedOperations": ["H"], + } + }, + "paradigm": { + "qubitCount": 30, + "nativeGateSet": ["ccnot", "cy"], + "connectivity": {"fullyConnected": False, "connectivityGraph": {"1": ["2", "3"]}}, + }, + "deviceParameters": {}, + "pulse": capabilities_json, + } + device_obj = RigettiDeviceCapabilities.parse_obj(device_json) + return { + "deviceName": "M-2-Pulse", + "deviceType": "QPU", + "providerName": "Rigetti", + "deviceStatus": "OFFLINE", + "deviceCapabilities": device_obj.json(), + } + + +@patch("urllib.request.urlopen") +@pytest.fixture +def device(mock_url_request): + response_data_content = { + "gates": { + "0_1": { + "cphaseshift": [ + { + "name": "cphaseshift", + "qubits": ["0", "1"], + "arguments": ["-1.5707963267948966"], + "calibrations": [ + { + "name": "play", + "arguments": [ + { + "name": "waveform", + "value": "wf_drag_gaussian_0", + "type": "waveform", + }, + { + "name": "frame", + "value": "q0_q1_cphase_frame", + "type": "frame", + }, + ], + }, + ], + } + ], + "rx_12": [{"name": "rx_12", "qubits": ["0"]}], + }, + }, + "waveforms": { + "wf_drag_gaussian_0": { + "waveformId": "wf_drag_gaussian_0", + "name": "drag_gaussian", + "arguments": [ + {"name": "length", "value": 6.000000000000001e-8, "type": "float"}, + {"name": "sigma", "value": 6.369913502160144e-9, "type": "float"}, + {"name": "amplitude", "value": -0.4549282253548838, "type": "float"}, + {"name": "beta", "value": 7.494904522022295e-10, "type": "float"}, + ], + }, + }, + } + + response_data_stream = io.BytesIO(json.dumps(response_data_content).encode("utf-8")) + mock_url_request.return_value.__enter__.return_value = response_data_stream + mock_session = Mock() + mock_session.get_device.return_value = get_pulse_model( + MOCK_PULSE_MODEL_QPU_PULSE_CAPABILITIES_JSON_1 + ) + device = AwsDevice(RIGETTI_ARN, mock_session) + return device @pytest.fixture() @@ -108,7 +199,8 @@ def test_non_parametric_defcal(gate_calibrations): "OPENQASM 3.0;", "cal {", " bit[1] psb;", - " waveform wf_drag_gaussian_3 = drag_gaussian(24.0ns, 2.547965400864ns, 2.3223415460834803e-10, 0.606950905462208, false);", + " waveform wf_drag_gaussian_3 = drag_gaussian(24.0ns, 2.547965400864ns, " + "2.3223415460834803e-10, 0.606950905462208, false);", " barrier $0;", " shift_frequency(q0_rf_frame, -321047.14178613486);", " play(q0_rf_frame, wf_drag_gaussian_3);", @@ -127,7 +219,8 @@ def test_parametric_defcal(gate_calibrations): "OPENQASM 3.0;", "cal {", " bit[1] psb;", - " waveform wf_drag_gaussian_3 = drag_gaussian(24.0ns, 2.547965400864ns, 2.3223415460834803e-10, 0.606950905462208, false);", + " waveform wf_drag_gaussian_3 = drag_gaussian(24.0ns, 2.547965400864ns, " + "2.3223415460834803e-10, 0.606950905462208, false);", " barrier $0;", " shift_frequency(q0_rf_frame, -321047.14178613486);", " play(q0_rf_frame, wf_drag_gaussian_3);", From 9ce84e389dfb0a891f933651dd2a2262ce0c7827 Mon Sep 17 00:00:00 2001 From: Jean-Christophe Jaskula Date: Tue, 19 Mar 2024 19:07:05 -0400 Subject: [PATCH 12/26] add tests --- src/braket/circuits/circuit_pulse_sequence.py | 57 ++- .../circuits/test_circuit_pulse_sequence.py | 340 ++++++++++++++---- 2 files changed, 317 insertions(+), 80 deletions(-) diff --git a/src/braket/circuits/circuit_pulse_sequence.py b/src/braket/circuits/circuit_pulse_sequence.py index 2556680ca..093d79449 100644 --- a/src/braket/circuits/circuit_pulse_sequence.py +++ b/src/braket/circuits/circuit_pulse_sequence.py @@ -23,6 +23,7 @@ from braket.circuits.gate_calibrations import GateCalibrations from braket.circuits.qubit_set import QubitSet from braket.circuits.result_type import ResultType +from braket.parametric.free_parameter import FreeParameter from braket.pulse.frame import Frame from braket.pulse.pulse_sequence import PulseSequence @@ -32,16 +33,20 @@ class CircuitPulseSequenceBuilder: def __init__( self, - device: AwsDevice | None = None, + device: AwsDevice, gate_definitions: dict[tuple[Gate, QubitSet], PulseSequence] | None = None, ) -> None: + assert device is not None, "Device must be set before building pulse sequences." + + gate_definitions = gate_definitions or {} + self._device = device - if gate_definitions is not None: - self._gate_calibrations = GateCalibrations(gate_definitions) - elif device is not None and device.gate_calibrations is not None: - self._gate_calibrations = device.gate_calibrations - else: - self._gate_calibrations = GateCalibrations({}) + self._gate_calibrations = ( + device.gate_calibrations + if device.gate_calibrations is not None + else GateCalibrations({}) + ) + self._gate_calibrations.pulse_sequences.update(gate_definitions) def build_pulse_sequence(self, circuit: cir.Circuit) -> PulseSequence: """ @@ -68,18 +73,9 @@ def build_pulse_sequence(self, circuit: cir.Circuit) -> PulseSequence: for instruction in circuit.instructions: gate = instruction.operator - qubit = instruction.target - if isinstance(gate, Gate) and gate.name == "PulseGate": - gate_pulse_sequence = gate.pulse_sequence - elif ( - gate_pulse_sequence := self._gate_calibrations.pulse_sequences.get( - (gate, qubit), None - ) - ) is None: - raise ValueError( - f"No pulse sequence for {gate.name} was provided in the gate" - " calibration set." - ) + qubits = instruction.target + + gate_pulse_sequence = self._get_pulse_sequence(gate, qubits) # FIXME: this creates a single cal block, we should have defcals because barrier; # would be applied to everything @@ -102,11 +98,34 @@ def build_pulse_sequence(self, circuit: cir.Circuit) -> PulseSequence: return pulse_sequence + def _get_pulse_sequence(self, gate: Gate, qubit: QubitSet) -> PulseSequence: + angle = None + if isinstance(gate, Gate) and gate.name == "PulseGate": + gate_pulse_sequence = gate.pulse_sequence + elif ( + gate_pulse_sequence := self._gate_calibrations.pulse_sequences.get((gate, qubit), None) + ) is None: + if hasattr(gate, "angle"): + angle = gate.angle + gate._parameters[0] = FreeParameter("theta") + if ( + gate_pulse_sequence := self._gate_calibrations.pulse_sequences.get( + (gate, qubit), None + ) + ) is None: + raise ValueError( + f"No pulse sequence for {gate.name} was provided in the gate" + " calibration set." + ) + return gate_pulse_sequence(theta=angle) if angle is not None else gate_pulse_sequence + def _readout_frame(self, qubit: QubitSet) -> Frame: if self._device.name == "Aspen-M-3": return self._device.frames[f"q{int(qubit)}_ro_rx_frame"] elif self._device.name == "Lucy": return self._device.frames[f"r{int(qubit)}_measure"] + else: + raise ValueError(f"Unknown device {self._device.name}") @staticmethod def _categorize_result_types( diff --git a/test/unit_tests/braket/circuits/test_circuit_pulse_sequence.py b/test/unit_tests/braket/circuits/test_circuit_pulse_sequence.py index 14b698e89..71051197f 100644 --- a/test/unit_tests/braket/circuits/test_circuit_pulse_sequence.py +++ b/test/unit_tests/braket/circuits/test_circuit_pulse_sequence.py @@ -19,12 +19,14 @@ import pytest from braket.aws import AwsDevice -from braket.circuits import Circuit +from braket.circuits import Circuit, Gate, QubitSet from braket.circuits.circuit_pulse_sequence import CircuitPulseSequenceBuilder +from braket.circuits.gate_calibrations import GateCalibrations from braket.device_schema.rigetti import RigettiDeviceCapabilities -from braket.pulse import PulseSequence +from braket.pulse.pulse_sequence import PulseSequence +from braket.pulse.waveforms import DragGaussianWaveform -RIGETTI_ARN = "arn:aws:braket:::device/qpu/rigetti/Aspen-10" +RIGETTI_ARN = "arn:aws:braket:::device/qpu/rigetti/Aspen-M-3" MOCK_PULSE_MODEL_QPU_PULSE_CAPABILITIES_JSON_1 = { "braketSchemaHeader": { @@ -42,7 +44,34 @@ "qubitMappings": None, "centerFrequencies": [375000000.0], "qhpSpecificProperties": None, - } + }, + "q0_rf": { + "portId": "q0_rf", + "direction": "tx", + "portType": "rf", + "dt": 1e-09, + "qubitMappings": None, + "centerFrequencies": [4875000000.0], + "qhpSpecificProperties": None, + }, + "q0_ro_rx": { + "portId": "q0_ro_rx", + "direction": "rx", + "portType": "ro_rx", + "dt": 5e-10, + "qubitMappings": None, + "centerFrequencies": [], + "qhpSpecificProperties": None, + }, + "q1_ro_rx": { + "portId": "q1_ro_rx", + "direction": "rx", + "portType": "ro_rx", + "dt": 5e-10, + "qubitMappings": None, + "centerFrequencies": [], + "qhpSpecificProperties": None, + }, }, "frames": { "q0_q1_cphase_frame": { @@ -54,7 +83,37 @@ "associatedGate": "cphase", "qubitMappings": [0, 1], "qhpSpecificProperties": None, - } + }, + "q0_rf_frame": { + "frameId": "q0_rf_frame", + "portId": "q0_rf", + "frequency": 5041233612.58213, + "centerFrequency": 4875000000.0, + "phase": 0.0, + "associatedGate": None, + "qubitMappings": [0], + "qhpSpecificProperties": None, + }, + "q0_ro_rx_frame": { + "frameId": "q0_ro_rx_frame", + "portId": "q0_ro_rx", + "frequency": 7359200001.83969, + "centerFrequency": None, + "phase": 0.0, + "associatedGate": None, + "qubitMappings": [0], + "qhpSpecificProperties": None, + }, + "q1_ro_rx_frame": { + "frameId": "q1_ro_rx_frame", + "portId": "q1_ro_rx", + "frequency": 7228259838.38781, + "centerFrequency": None, + "phase": 0.0, + "associatedGate": None, + "qubitMappings": [1], + "qhpSpecificProperties": None, + }, }, "nativeGateCalibrationsRef": "file://hostname/foo/bar", } @@ -119,7 +178,7 @@ def get_pulse_model(capabilities_json): } device_obj = RigettiDeviceCapabilities.parse_obj(device_json) return { - "deviceName": "M-2-Pulse", + "deviceName": "Aspen-M-3", "deviceType": "QPU", "providerName": "Rigetti", "deviceStatus": "OFFLINE", @@ -127,9 +186,8 @@ def get_pulse_model(capabilities_json): } -@patch("urllib.request.urlopen") @pytest.fixture -def device(mock_url_request): +def device(): response_data_content = { "gates": { "0_1": { @@ -137,14 +195,14 @@ def device(mock_url_request): { "name": "cphaseshift", "qubits": ["0", "1"], - "arguments": ["-1.5707963267948966"], + "arguments": ["theta"], "calibrations": [ { "name": "play", "arguments": [ { "name": "waveform", - "value": "wf_drag_gaussian_0", + "value": "q0_q1_cphase_sqrtCPHASE", "type": "waveform", }, { @@ -154,15 +212,141 @@ def device(mock_url_request): }, ], }, + { + "name": "shift_phase", + "arguments": [ + { + "name": "frame", + "value": "q0_rf_frame", + "type": "frame", + "optional": False, + }, + { + "name": "phase", + "value": "-1.0*theta", + "type": "expr", + "optional": False, + }, + ], + }, ], } ], - "rx_12": [{"name": "rx_12", "qubits": ["0"]}], + }, + "0": { + "rx": [ + { + "name": "rx", + "qubits": ["0"], + "arguments": ["1.5707963267948966"], + "calibrations": [ + { + "name": "barrier", + "arguments": [ + { + "name": "qubit", + "value": "0", + "type": "string", + "optional": False, + } + ], + }, + { + "name": "shift_frequency", + "arguments": [ + { + "name": "frame", + "value": "q0_rf_frame", + "type": "frame", + "optional": False, + }, + { + "name": "frequency", + "value": -321047.14178613486, + "type": "float", + "optional": False, + }, + ], + }, + { + "name": "play", + "arguments": [ + { + "name": "frame", + "value": "q0_rf_frame", + "type": "frame", + "optional": False, + }, + { + "name": "waveform", + "value": "wf_drag_gaussian_1", + "type": "waveform", + "optional": False, + }, + ], + }, + { + "name": "shift_frequency", + "arguments": [ + { + "name": "frame", + "value": "q0_rf_frame", + "type": "frame", + "optional": False, + }, + { + "name": "frequency", + "value": 321047.14178613486, + "type": "float", + "optional": False, + }, + ], + }, + { + "name": "barrier", + "arguments": [ + { + "name": "qubit", + "value": "0", + "type": "string", + "optional": False, + } + ], + }, + ], + }, + ], + "rz": [ + { + "name": "rz", + "qubits": ["0"], + "arguments": ["theta"], + "calibrations": [ + { + "name": "shift_phase", + "arguments": [ + { + "name": "frame", + "value": "q0_rf_frame", + "type": "frame", + "optional": False, + }, + { + "name": "phase", + "value": "-1.0*theta", + "type": "expr", + "optional": False, + }, + ], + }, + ], + }, + ], }, }, "waveforms": { - "wf_drag_gaussian_0": { - "waveformId": "wf_drag_gaussian_0", + "wf_drag_gaussian_1": { + "waveformId": "wf_drag_gaussian_1", "name": "drag_gaussian", "arguments": [ {"name": "length", "value": 6.000000000000001e-8, "type": "float"}, @@ -171,63 +355,97 @@ def device(mock_url_request): {"name": "beta", "value": 7.494904522022295e-10, "type": "float"}, ], }, + "q0_q1_cphase_sqrtCPHASE": { + "waveformId": "q0_q1_cphase_sqrtCPHASE", + "amplitudes": [ + [0.0, 0.0], + [0.0, 0.0], + [0.0, 0.0], + [0.0, 0.0], + ], + }, }, } response_data_stream = io.BytesIO(json.dumps(response_data_content).encode("utf-8")) - mock_url_request.return_value.__enter__.return_value = response_data_stream - mock_session = Mock() - mock_session.get_device.return_value = get_pulse_model( - MOCK_PULSE_MODEL_QPU_PULSE_CAPABILITIES_JSON_1 + with patch("urllib.request.urlopen") as mock_url_request: + mock_url_request.return_value.__enter__.return_value = response_data_stream + mock_session = Mock() + mock_session.get_device.return_value = get_pulse_model( + MOCK_PULSE_MODEL_QPU_PULSE_CAPABILITIES_JSON_1 + ) + yield AwsDevice(RIGETTI_ARN, mock_session) + + +@pytest.fixture +def pulse_sequence(predefined_frame_1): + return ( + PulseSequence() + .set_frequency( + predefined_frame_1, + 6e6, + ) + .play( + predefined_frame_1, + DragGaussianWaveform(length=3e-3, sigma=0.4, beta=0.2, id="drag_gauss_wf"), + ) ) - device = AwsDevice(RIGETTI_ARN, mock_session) - return device @pytest.fixture() -def gate_calibrations(device): - return device.gate_calibrations - - -def test_empty_circuit(): - assert CircuitPulseSequenceBuilder(None).build_pulse_sequence(Circuit()) == PulseSequence() - - -def test_non_parametric_defcal(gate_calibrations): - circ = Circuit().rx(0, np.pi) - expected = ( - "OPENQASM 3.0;", - "cal {", - " bit[1] psb;", - " waveform wf_drag_gaussian_3 = drag_gaussian(24.0ns, 2.547965400864ns, " - "2.3223415460834803e-10, 0.606950905462208, false);", - " barrier $0;", - " shift_frequency(q0_rf_frame, -321047.14178613486);", - " play(q0_rf_frame, wf_drag_gaussian_3);", - " shift_frequency(q0_rf_frame, 321047.14178613486);", - " barrier $0;", - " psb[0] = capture_v0(q0_ro_rx_frame);", - "}", +def user_defined_gate_calibrations(): + calibration_key = (Gate.Z(), QubitSet([0, 1])) + return GateCalibrations( + { + calibration_key: pulse_sequence, + } ) - assert circ.pulse_sequence(gate_calibrations).to_ir() == expected - - -def test_parametric_defcal(gate_calibrations): - circ = Circuit().xy(0, 1, 0.1) - expected = ( - "OPENQASM 3.0;", - "cal {", - " bit[1] psb;", - " waveform wf_drag_gaussian_3 = drag_gaussian(24.0ns, 2.547965400864ns, " - "2.3223415460834803e-10, 0.606950905462208, false);", - " barrier $0;", - " shift_frequency(q0_rf_frame, -321047.14178613486);", - " play(q0_rf_frame, wf_drag_gaussian_3);", - " shift_frequency(q0_rf_frame, 321047.14178613486);", - " barrier $0;", - " psb[0] = capture_v0(q0_ro_rx_frame);", - "}", + +def test_empty_circuit(device): + assert CircuitPulseSequenceBuilder(device).build_pulse_sequence(Circuit()) == PulseSequence() + + +def test_no_device(): + with pytest.raises(AssertionError, match="Device must be set before building pulse sequences."): + CircuitPulseSequenceBuilder(None) + + +def test_non_parametric_defcal(device): + circ = Circuit().rx(0, np.pi / 2) + expected = "\n".join( + [ + "OPENQASM 3.0;", + "cal {", + " bit[1] psb;", + " waveform wf_drag_gaussian_1 = drag_gaussian(60.0ns, 6.36991350216ns, " + "7.494904522022295e-10, -0.4549282253548838, false);", + " barrier $0;", + " shift_frequency(q0_rf_frame, -321047.14178613486);", + " play(q0_rf_frame, wf_drag_gaussian_1);", + " shift_frequency(q0_rf_frame, 321047.14178613486);", + " barrier $0;", + " psb[0] = capture_v0(q0_ro_rx_frame);", + "}", + ] + ) + assert circ.pulse_sequence(device).to_ir() == expected + + +def test_parametric_defcal(device): + circ = Circuit().cphaseshift(0, 1, 0.1) + expected = "\n".join( + [ + "OPENQASM 3.0;", + "cal {", + " bit[2] psb;", + " waveform q0_q1_cphase_sqrtCPHASE = {0.0, 0.0, 0.0, 0.0};", + " play(q0_q1_cphase_frame, q0_q1_cphase_sqrtCPHASE);", + " shift_phase(q0_rf_frame, -0.1);", + " psb[0] = capture_v0(q0_ro_rx_frame);", + " psb[1] = capture_v0(q1_ro_rx_frame);", + "}", + ] ) - assert circ.pulse_sequence(gate_calibrations).to_ir() == expected + assert circ.pulse_sequence(device).to_ir() == expected From 0a3b9872f984a5ce283828866274b34c167bd2ce Mon Sep 17 00:00:00 2001 From: Jean-Christophe Jaskula Date: Wed, 20 Mar 2024 12:57:04 -0400 Subject: [PATCH 13/26] clean pulse sequence retrieval logic --- src/braket/circuits/circuit_pulse_sequence.py | 35 +++++++++++++------ 1 file changed, 25 insertions(+), 10 deletions(-) diff --git a/src/braket/circuits/circuit_pulse_sequence.py b/src/braket/circuits/circuit_pulse_sequence.py index 093d79449..ffd6ec328 100644 --- a/src/braket/circuits/circuit_pulse_sequence.py +++ b/src/braket/circuits/circuit_pulse_sequence.py @@ -99,30 +99,45 @@ def build_pulse_sequence(self, circuit: cir.Circuit) -> PulseSequence: return pulse_sequence def _get_pulse_sequence(self, gate: Gate, qubit: QubitSet) -> PulseSequence: - angle = None if isinstance(gate, Gate) and gate.name == "PulseGate": gate_pulse_sequence = gate.pulse_sequence elif ( gate_pulse_sequence := self._gate_calibrations.pulse_sequences.get((gate, qubit), None) ) is None: - if hasattr(gate, "angle"): - angle = gate.angle - gate._parameters[0] = FreeParameter("theta") if ( - gate_pulse_sequence := self._gate_calibrations.pulse_sequences.get( - (gate, qubit), None + not hasattr(gate, "parameters") + or ( + gate_pulse_sequence := self._find_parametric_gate_calibration( + gate, qubit, len(gate.parameters) + ) ) - ) is None: + is None + ): raise ValueError( f"No pulse sequence for {gate.name} was provided in the gate" " calibration set." ) - return gate_pulse_sequence(theta=angle) if angle is not None else gate_pulse_sequence + + return gate_pulse_sequence( + **{p.name: v for p, v in zip(gate_pulse_sequence.parameters, gate.parameters)} + ) + + def _find_parametric_gate_calibration( + self, gate: Gate, qubitset: QubitSet, number_assignment_values: int + ) -> PulseSequence | None: + for key in self._gate_calibrations.pulse_sequences: + if ( + key[0].name == gate.name + and key[1] == qubitset + and sum(isinstance(param, FreeParameter) for param in key[0].parameters) + == number_assignment_values + ): + return self._gate_calibrations.pulse_sequences[key] def _readout_frame(self, qubit: QubitSet) -> Frame: - if self._device.name == "Aspen-M-3": + if self._device.provider_name == "Rigetti": return self._device.frames[f"q{int(qubit)}_ro_rx_frame"] - elif self._device.name == "Lucy": + elif self._device.provider_name == "Oxford": return self._device.frames[f"r{int(qubit)}_measure"] else: raise ValueError(f"Unknown device {self._device.name}") From b2774f0041dd5994d4db845ef969c88494ea2ecf Mon Sep 17 00:00:00 2001 From: Jean-Christophe Jaskula Date: Wed, 20 Mar 2024 18:15:15 -0400 Subject: [PATCH 14/26] increase coverage --- src/braket/circuits/circuit_pulse_sequence.py | 42 +-- .../circuits/test_circuit_pulse_sequence.py | 316 +++++++++++++++++- .../braket/pulse/test_pulse_sequence.py | 47 +++ 3 files changed, 378 insertions(+), 27 deletions(-) diff --git a/src/braket/circuits/circuit_pulse_sequence.py b/src/braket/circuits/circuit_pulse_sequence.py index ffd6ec328..d8c74e1eb 100644 --- a/src/braket/circuits/circuit_pulse_sequence.py +++ b/src/braket/circuits/circuit_pulse_sequence.py @@ -15,7 +15,7 @@ from typing import TYPE_CHECKING -if TYPE_CHECKING: +if TYPE_CHECKING: # pragma: no cover from braket.aws.aws_device import AwsDevice import braket.circuits.circuit as cir @@ -36,8 +36,7 @@ def __init__( device: AwsDevice, gate_definitions: dict[tuple[Gate, QubitSet], PulseSequence] | None = None, ) -> None: - assert device is not None, "Device must be set before building pulse sequences." - + _validate_device(device) gate_definitions = gate_definitions or {} self._device = device @@ -67,23 +66,20 @@ def build_pulse_sequence(self, circuit: cir.Circuit) -> PulseSequence: if circuit.parameters: raise ValueError("All parameters must be assigned to draw the pulse sequence.") - # circuit needs to be verbatim - # if circuit not verbatim - # raise ValueError("Circuit must be encapsulated in a verbatim box.") - for instruction in circuit.instructions: gate = instruction.operator qubits = instruction.target gate_pulse_sequence = self._get_pulse_sequence(gate, qubits) - # FIXME: this creates a single cal block, we should have defcals because barrier; - # would be applied to everything + # FIXME: this creates a single cal block, "barrier;" in defcal could be either + # global or restricted to the defcal context + # Right they are global pulse_sequence += gate_pulse_sequence # Result type columns ( - additional_result_types, + _, target_result_types, ) = CircuitPulseSequenceBuilder._categorize_result_types(circuit.result_types) @@ -92,13 +88,10 @@ def build_pulse_sequence(self, circuit: cir.Circuit) -> PulseSequence: for qubit in circuit.qubits: pulse_sequence.capture_v0(self._readout_frame(qubit)) - # Additional result types line on bottom - if additional_result_types: - print(f"\nAdditional result types: {', '.join(additional_result_types)}") - return pulse_sequence def _get_pulse_sequence(self, gate: Gate, qubit: QubitSet) -> PulseSequence: + parameters = gate.parameters if hasattr(gate, "parameters") else [] if isinstance(gate, Gate) and gate.name == "PulseGate": gate_pulse_sequence = gate.pulse_sequence elif ( @@ -119,7 +112,7 @@ def _get_pulse_sequence(self, gate: Gate, qubit: QubitSet) -> PulseSequence: ) return gate_pulse_sequence( - **{p.name: v for p, v in zip(gate_pulse_sequence.parameters, gate.parameters)} + **{p.name: v for p, v in zip(gate_pulse_sequence.parameters, parameters)} ) def _find_parametric_gate_calibration( @@ -135,12 +128,12 @@ def _find_parametric_gate_calibration( return self._gate_calibrations.pulse_sequences[key] def _readout_frame(self, qubit: QubitSet) -> Frame: - if self._device.provider_name == "Rigetti": - return self._device.frames[f"q{int(qubit)}_ro_rx_frame"] - elif self._device.provider_name == "Oxford": - return self._device.frames[f"r{int(qubit)}_measure"] - else: - raise ValueError(f"Unknown device {self._device.name}") + readout_frame_names = { + "Rigetti": f"q{int(qubit)}_ro_rx_frame", + "Oxford": f"r{int(qubit)}_measure", + } + frame_name = readout_frame_names[self._device.provider_name] + return self._device.frames[frame_name] @staticmethod def _categorize_result_types( @@ -165,3 +158,10 @@ def _categorize_result_types( else: additional_result_types.extend(result_type.ascii_symbols) return additional_result_types, target_result_types + + +def _validate_device(device: AwsDevice | None) -> None: + if device is None: + raise ValueError("Device must be set before building pulse sequences.") + elif device.provider_name not in ("Rigetti", "Oxford"): + raise ValueError(f"Device {device.name} is not supported.") diff --git a/test/unit_tests/braket/circuits/test_circuit_pulse_sequence.py b/test/unit_tests/braket/circuits/test_circuit_pulse_sequence.py index 71051197f..a61508a2a 100644 --- a/test/unit_tests/braket/circuits/test_circuit_pulse_sequence.py +++ b/test/unit_tests/braket/circuits/test_circuit_pulse_sequence.py @@ -19,14 +19,21 @@ import pytest from braket.aws import AwsDevice -from braket.circuits import Circuit, Gate, QubitSet +from braket.circuits import Circuit, Gate, Observable, QubitSet from braket.circuits.circuit_pulse_sequence import CircuitPulseSequenceBuilder from braket.circuits.gate_calibrations import GateCalibrations +from braket.device_schema.ionq import IonqDeviceCapabilities +from braket.device_schema.oqc import OqcDeviceCapabilities from braket.device_schema.rigetti import RigettiDeviceCapabilities +from braket.parametric.free_parameter import FreeParameter +from braket.pulse.frame import Frame +from braket.pulse.port import Port from braket.pulse.pulse_sequence import PulseSequence from braket.pulse.waveforms import DragGaussianWaveform RIGETTI_ARN = "arn:aws:braket:::device/qpu/rigetti/Aspen-M-3" +OQC_ARN = "arn:aws:braket:::device/qpu/oqc/Lucy" +IONQ_ARN = "arn:aws:braket:::device/qpu/ionq/Harmony" MOCK_PULSE_MODEL_QPU_PULSE_CAPABILITIES_JSON_1 = { "braketSchemaHeader": { @@ -118,8 +125,40 @@ "nativeGateCalibrationsRef": "file://hostname/foo/bar", } +MOCK_PULSE_MODEL_QPU_PULSE_CAPABILITIES_JSON_2 = { + "braketSchemaHeader": { + "name": "braket.device_schema.pulse.pulse_device_action_properties", + "version": "1", + }, + "supportedQhpTemplateWaveforms": {}, + "supportedFunctions": {}, + "ports": { + "channel_14": { + "portId": "channel_14", + "direction": "rx", + "portType": "port_type_1", + "dt": 5e-10, + "qubitMappings": [1], + "centerFrequencies": None, + "qhpSpecificProperties": None, + } + }, + "frames": { + "r1_measure": { + "frameId": "r1_measure", + "portId": "channel_14", + "frequency": 9624889855.38831, + "centerFrequency": 9000000000.0, + "phase": 0.0, + "associatedGate": None, + "qubitMappings": [1], + "qhpSpecificProperties": None, + } + }, +} + -def get_pulse_model(capabilities_json): +def get_rigetti_pulse_model(capabilities_json): device_json = { "braketSchemaHeader": { "name": "braket.device_schema.rigetti.rigetti_device_capabilities", @@ -186,6 +225,87 @@ def get_pulse_model(capabilities_json): } +def get_oqc_pulse_model(capabilities_json): + device_json = { + "braketSchemaHeader": { + "name": "braket.device_schema.oqc.oqc_device_capabilities", + "version": "1", + }, + "service": { + "executionWindows": [ + { + "executionDay": "Everyday", + "windowStartHour": "11:00", + "windowEndHour": "12:00", + } + ], + "shotsRange": [1, 10], + }, + "action": { + "braket.ir.jaqcd.program": { + "actionType": "braket.ir.jaqcd.program", + "version": ["1"], + "supportedOperations": ["H"], + } + }, + "paradigm": { + "qubitCount": 30, + "nativeGateSet": ["ccnot", "cy"], + "connectivity": {"fullyConnected": False, "connectivityGraph": {"1": ["2", "3"]}}, + }, + "deviceParameters": {}, + "pulse": capabilities_json, + } + device_obj = OqcDeviceCapabilities.parse_obj(device_json) + return { + "deviceName": "Lucy", + "deviceType": "QPU", + "providerName": "Oxford", + "deviceStatus": "OFFLINE", + "deviceCapabilities": device_obj.json(), + } + + +def get_ionq_model(): + device_json = { + "braketSchemaHeader": { + "name": "braket.device_schema.ionq.ionq_device_capabilities", + "version": "1", + }, + "service": { + "executionWindows": [ + { + "executionDay": "Everyday", + "windowStartHour": "11:00", + "windowEndHour": "12:00", + } + ], + "shotsRange": [1, 10], + }, + "action": { + "braket.ir.jaqcd.program": { + "actionType": "braket.ir.jaqcd.program", + "version": ["1"], + "supportedOperations": ["H"], + } + }, + "paradigm": { + "qubitCount": 30, + "nativeGateSet": ["ccnot", "cy"], + "connectivity": {"fullyConnected": True, "connectivityGraph": {"1": ["2", "3"]}}, + }, + "deviceParameters": {}, + } + device_obj = IonqDeviceCapabilities.parse_obj(device_json) + return { + "deviceName": "IonqDevice", + "deviceType": "QPU", + "providerName": "IonQ", + "deviceStatus": "OFFLINE", + "deviceCapabilities": device_obj.json(), + } + + @pytest.fixture def device(): response_data_content = { @@ -371,12 +491,48 @@ def device(): with patch("urllib.request.urlopen") as mock_url_request: mock_url_request.return_value.__enter__.return_value = response_data_stream mock_session = Mock() - mock_session.get_device.return_value = get_pulse_model( + mock_session.get_device.return_value = get_rigetti_pulse_model( MOCK_PULSE_MODEL_QPU_PULSE_CAPABILITIES_JSON_1 ) yield AwsDevice(RIGETTI_ARN, mock_session) +@pytest.fixture +def OQC_device(): + response_data_content = {"gates": {}, "waveforms": {}} + response_data_stream = io.BytesIO(json.dumps(response_data_content).encode("utf-8")) + with patch("urllib.request.urlopen") as mock_url_request: + mock_url_request.return_value.__enter__.return_value = response_data_stream + mock_session = Mock() + mock_session.get_device.return_value = get_oqc_pulse_model( + MOCK_PULSE_MODEL_QPU_PULSE_CAPABILITIES_JSON_2 + ) + yield AwsDevice(OQC_ARN, mock_session) + + +@pytest.fixture +def not_supported_device(): + response_data_content = {"gates": {}, "waveforms": {}} + response_data_stream = io.BytesIO(json.dumps(response_data_content).encode("utf-8")) + with patch("urllib.request.urlopen") as mock_url_request: + mock_url_request.return_value.__enter__.return_value = response_data_stream + mock_session = Mock() + mock_session.get_device.return_value = get_ionq_model() + yield AwsDevice(IONQ_ARN, mock_session) + + +@pytest.fixture +def port(): + return Port(port_id="device_port_x0", dt=1e-9, properties={}) + + +@pytest.fixture +def predefined_frame_1(port): + return Frame( + frame_id="predefined_frame_1", frequency=2e9, port=port, phase=0, is_predefined=True + ) + + @pytest.fixture def pulse_sequence(predefined_frame_1): return ( @@ -393,8 +549,8 @@ def pulse_sequence(predefined_frame_1): @pytest.fixture() -def user_defined_gate_calibrations(): - calibration_key = (Gate.Z(), QubitSet([0, 1])) +def user_defined_gate_calibrations(pulse_sequence): + calibration_key = (Gate.Z(), QubitSet([1])) return GateCalibrations( { calibration_key: pulse_sequence, @@ -407,10 +563,23 @@ def test_empty_circuit(device): def test_no_device(): - with pytest.raises(AssertionError, match="Device must be set before building pulse sequences."): + with pytest.raises(ValueError, match="Device must be set before building pulse sequences."): CircuitPulseSequenceBuilder(None) +def test_not_supported_device(not_supported_device): + with pytest.raises(ValueError, match=f"Device {not_supported_device.name} is not supported."): + CircuitPulseSequenceBuilder(not_supported_device) + + +def test_unbound_circuit(device): + circuit = Circuit().rz(0, FreeParameter("theta")) + with pytest.raises( + ValueError, match="All parameters must be assigned to draw the pulse sequence." + ): + circuit.pulse_sequence(device) + + def test_non_parametric_defcal(device): circ = Circuit().rx(0, np.pi / 2) expected = "\n".join( @@ -432,6 +601,46 @@ def test_non_parametric_defcal(device): assert circ.pulse_sequence(device).to_ir() == expected +def test_user_defined_gate_calibrations_extension(device, user_defined_gate_calibrations): + circ = Circuit().z(1) + expected = "\n".join( + [ + "OPENQASM 3.0;", + "cal {", + " bit[1] psb;", + " waveform drag_gauss_wf = drag_gaussian(3.0ms, 400.0ms, 0.2, 1, false);", + " set_frequency(predefined_frame_1, 6000000.0);", + " play(predefined_frame_1, drag_gauss_wf);", + " psb[0] = capture_v0(q1_ro_rx_frame);", + "}", + ] + ) + assert ( + circ.pulse_sequence(device, user_defined_gate_calibrations.pulse_sequences).to_ir() + == expected + ) + + +def test_with_oxford_device(OQC_device, user_defined_gate_calibrations): + circ = Circuit().z(1) + expected = "\n".join( + [ + "OPENQASM 3.0;", + "cal {", + " bit[1] psb;", + " waveform drag_gauss_wf = drag_gaussian(3.0ms, 400.0ms, 0.2, 1, false);", + " set_frequency(predefined_frame_1, 6000000.0);", + " play(predefined_frame_1, drag_gauss_wf);", + " psb[0] = capture_v0(r1_measure);", + "}", + ] + ) + assert ( + circ.pulse_sequence(OQC_device, user_defined_gate_calibrations.pulse_sequences).to_ir() + == expected + ) + + def test_parametric_defcal(device): circ = Circuit().cphaseshift(0, 1, 0.1) expected = "\n".join( @@ -449,3 +658,98 @@ def test_parametric_defcal(device): ) assert circ.pulse_sequence(device).to_ir() == expected + + +def test_pulse_gate(device): + pulse_sequence = PulseSequence().set_frequency(device.frames["q0_rf_frame"], 1e6) + circ = Circuit().pulse_gate(0, pulse_sequence) + expected = "\n".join( + [ + "OPENQASM 3.0;", + "cal {", + " bit[1] psb;", + " set_frequency(q0_rf_frame, 1000000.0);", + " psb[0] = capture_v0(q0_ro_rx_frame);", + "}", + ] + ) + + assert circ.pulse_sequence(device).to_ir() == expected + + +def test_missing_calibration(device): + circuit = Circuit().rz(1, 0.1) + with pytest.raises( + ValueError, match="No pulse sequence for Rz was provided in the gate calibration set." + ): + circuit.pulse_sequence(device) + + +def test_readout_with_not_supported_device(device): + circuit = Circuit().rz(1, 0.1) + with pytest.raises( + ValueError, match="No pulse sequence for Rz was provided in the gate calibration set." + ): + circuit.pulse_sequence(device) + + +def test_expectation_value_result_type_on_one_qubit(device): + circ = ( + Circuit() + .rx(0, np.pi / 2) + .cphaseshift(0, 1, 0.1) + .expectation(observable=Observable.X(), target=[0]) + ) + expected = "\n".join( + [ + "OPENQASM 3.0;", + "cal {", + " bit[2] psb;", + " ...;" " psb[0] = capture_v0(q0_ro_rx_frame);", + " psb[1] = capture_v0(q1_ro_rx_frame);", + "}", + ] + ) + with pytest.raises( + NotImplementedError, match="_to_pulse_sequence has not been implemented yet." + ): + assert circ.pulse_sequence(device).to_ir() == expected + + +def test_expectation_value_result_type_on_all_qubits(device): + circ = Circuit().rx(0, np.pi / 2).cphaseshift(0, 1, 0.1).expectation(observable=Observable.X()) + expected = "\n".join( + [ + "OPENQASM 3.0;", + "cal {", + " bit[2] psb;", + " ...;" " psb[0] = capture_v0(q0_ro_rx_frame);", + " psb[1] = capture_v0(q1_ro_rx_frame);", + "}", + ] + ) + with pytest.raises( + NotImplementedError, match="_to_pulse_sequence has not been implemented yet." + ): + assert circ.pulse_sequence(device).to_ir() == expected + + +def test_no_target_result_type(device): + circ = Circuit().rx(0, np.pi / 2).state_vector() + expected = "\n".join( + [ + "OPENQASM 3.0;", + "cal {", + " bit[1] psb;", + " waveform wf_drag_gaussian_1 = drag_gaussian(60.0ns, 6.36991350216ns, " + "7.494904522022295e-10, -0.4549282253548838, false);", + " barrier $0;", + " shift_frequency(q0_rf_frame, -321047.14178613486);", + " play(q0_rf_frame, wf_drag_gaussian_1);", + " shift_frequency(q0_rf_frame, 321047.14178613486);", + " barrier $0;", + " psb[0] = capture_v0(q0_ro_rx_frame);", + "}", + ] + ) + assert circ.pulse_sequence(device).to_ir() == expected diff --git a/test/unit_tests/braket/pulse/test_pulse_sequence.py b/test/unit_tests/braket/pulse/test_pulse_sequence.py index da8e0a8a3..d5429aef2 100644 --- a/test/unit_tests/braket/pulse/test_pulse_sequence.py +++ b/test/unit_tests/braket/pulse/test_pulse_sequence.py @@ -416,3 +416,50 @@ def test_parse_from_calibration_schema(predefined_frame_1, predefined_frame_2): PulseSequence._parse_from_calibration_schema(calibration_instrs, waveforms, frames) == expected_pulse_sequence ) + + +def test_add_pulse_sequences_with_different_free_parameters(predefined_frame_1): + theta1 = FreeParameter("theta1") + theta2 = FreeParameter("theta2") + pulse_sequence1 = PulseSequence().shift_phase(predefined_frame_1, theta1) + pulse_sequence2 = PulseSequence().shift_phase(predefined_frame_1, theta2) + pulse_sequence = pulse_sequence1 + pulse_sequence2 + + expected_str = "\n".join( + [ + "OPENQASM 3.0;", + "cal {", + *[ + f" input float {parameter};" + for parameter in reversed(list(pulse_sequence.parameters)) + ], + " shift_phase(predefined_frame_1, theta1);", + " shift_phase(predefined_frame_1, theta2);", + "}", + ] + ) + + assert pulse_sequence.to_ir() == expected_str + assert pulse_sequence.parameters == {theta1, theta2} + + +def test_add_pulse_sequences_with_capture_v0(predefined_frame_1): + pulse_sequence1 = PulseSequence().capture_v0(predefined_frame_1) + pulse_sequence2 = PulseSequence() + with pytest.raises( + ValueError, + match="Cannot add a pulse sequence to another" + " one that has already measurement instructions.", + ): + pulse_sequence1 += pulse_sequence2 + + +def test_add_pulse_sequences_with_same_free_parameter(predefined_frame_1): + theta = FreeParameter("theta") + pulse_sequence1 = PulseSequence().shift_phase(predefined_frame_1, theta) + with pytest.raises( + ValueError, + match="A free parameter with the name theta already exists in the pulse" + " sequence. Please rename this free parameter.", + ): + pulse_sequence1 + pulse_sequence1 From 87b922cf6425e7b5e0ad0b8ad00b4c0885545ef5 Mon Sep 17 00:00:00 2001 From: Jean-Christophe Jaskula Date: Wed, 20 Mar 2024 18:42:36 -0400 Subject: [PATCH 15/26] add comments to xfail tests --- .../circuits/test_circuit_pulse_sequence.py | 22 +++++++++++-------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/test/unit_tests/braket/circuits/test_circuit_pulse_sequence.py b/test/unit_tests/braket/circuits/test_circuit_pulse_sequence.py index a61508a2a..902d76d0c 100644 --- a/test/unit_tests/braket/circuits/test_circuit_pulse_sequence.py +++ b/test/unit_tests/braket/circuits/test_circuit_pulse_sequence.py @@ -694,18 +694,17 @@ def test_readout_with_not_supported_device(device): def test_expectation_value_result_type_on_one_qubit(device): - circ = ( - Circuit() - .rx(0, np.pi / 2) - .cphaseshift(0, 1, 0.1) - .expectation(observable=Observable.X(), target=[0]) - ) + circ = Circuit().cphaseshift(0, 1, 0.1).expectation(observable=Observable.X(), target=[1]) expected = "\n".join( [ "OPENQASM 3.0;", "cal {", " bit[2] psb;", - " ...;" " psb[0] = capture_v0(q0_ro_rx_frame);", + " waveform q0_q1_cphase_sqrtCPHASE = {0.0, 0.0, 0.0, 0.0};", + " play(q0_q1_cphase_frame, q0_q1_cphase_sqrtCPHASE);", + " shift_phase(q0_rf_frame, -0.1);", + " rx(pi/2) $1;", # FIXME: this need the right basis rotation + " psb[0] = capture_v0(q0_ro_rx_frame);", " psb[1] = capture_v0(q1_ro_rx_frame);", "}", ] @@ -717,13 +716,18 @@ def test_expectation_value_result_type_on_one_qubit(device): def test_expectation_value_result_type_on_all_qubits(device): - circ = Circuit().rx(0, np.pi / 2).cphaseshift(0, 1, 0.1).expectation(observable=Observable.X()) + circ = Circuit().cphaseshift(0, 1, 0.1).expectation(observable=Observable.X()) expected = "\n".join( [ "OPENQASM 3.0;", "cal {", " bit[2] psb;", - " ...;" " psb[0] = capture_v0(q0_ro_rx_frame);", + " waveform q0_q1_cphase_sqrtCPHASE = {0.0, 0.0, 0.0, 0.0};", + " play(q0_q1_cphase_frame, q0_q1_cphase_sqrtCPHASE);", + " shift_phase(q0_rf_frame, -0.1);", + " rx(pi/2) $0;", # FIXME: this need the right basis rotation + " rx(pi/2) $1;", # FIXME: this need the right basis rotation + " psb[0] = capture_v0(q0_ro_rx_frame);", " psb[1] = capture_v0(q1_ro_rx_frame);", "}", ] From 6be014095cf4ea3c9d2f7faf6f2dc1336c644be4 Mon Sep 17 00:00:00 2001 From: Jean-Christophe Jaskula Date: Wed, 20 Mar 2024 19:01:50 -0400 Subject: [PATCH 16/26] remove unnecessary test --- .../braket/circuits/test_circuit_pulse_sequence.py | 8 -------- 1 file changed, 8 deletions(-) diff --git a/test/unit_tests/braket/circuits/test_circuit_pulse_sequence.py b/test/unit_tests/braket/circuits/test_circuit_pulse_sequence.py index 902d76d0c..6272cb417 100644 --- a/test/unit_tests/braket/circuits/test_circuit_pulse_sequence.py +++ b/test/unit_tests/braket/circuits/test_circuit_pulse_sequence.py @@ -685,14 +685,6 @@ def test_missing_calibration(device): circuit.pulse_sequence(device) -def test_readout_with_not_supported_device(device): - circuit = Circuit().rz(1, 0.1) - with pytest.raises( - ValueError, match="No pulse sequence for Rz was provided in the gate calibration set." - ): - circuit.pulse_sequence(device) - - def test_expectation_value_result_type_on_one_qubit(device): circ = Circuit().cphaseshift(0, 1, 0.1).expectation(observable=Observable.X(), target=[1]) expected = "\n".join( From 0edfd67c2d3ef10a3a87026ac3aa406d7b82b30b Mon Sep 17 00:00:00 2001 From: Jean-Christophe Jaskula Date: Wed, 20 Mar 2024 19:37:11 -0400 Subject: [PATCH 17/26] add warning --- src/braket/circuits/circuit_pulse_sequence.py | 25 ++++++++++--------- .../circuits/test_circuit_pulse_sequence.py | 6 ++++- 2 files changed, 18 insertions(+), 13 deletions(-) diff --git a/src/braket/circuits/circuit_pulse_sequence.py b/src/braket/circuits/circuit_pulse_sequence.py index d8c74e1eb..6ecfca5a7 100644 --- a/src/braket/circuits/circuit_pulse_sequence.py +++ b/src/braket/circuits/circuit_pulse_sequence.py @@ -13,6 +13,7 @@ from __future__ import annotations +import warnings from typing import TYPE_CHECKING if TYPE_CHECKING: # pragma: no cover @@ -29,7 +30,7 @@ class CircuitPulseSequenceBuilder: - """Builds ASCII string circuit diagrams.""" + """Builds a pulse sequence from circuits.""" def __init__( self, @@ -78,13 +79,13 @@ def build_pulse_sequence(self, circuit: cir.Circuit) -> PulseSequence: pulse_sequence += gate_pulse_sequence # Result type columns - ( - _, - target_result_types, - ) = CircuitPulseSequenceBuilder._categorize_result_types(circuit.result_types) + target_result_types = CircuitPulseSequenceBuilder._categorize_result_types( + circuit.result_types + ) for result_type in target_result_types: pulse_sequence += result_type._to_pulse_sequence() + for qubit in circuit.qubits: pulse_sequence.capture_v0(self._readout_frame(qubit)) @@ -138,7 +139,7 @@ def _readout_frame(self, qubit: QubitSet) -> Frame: @staticmethod def _categorize_result_types( result_types: list[ResultType], - ) -> tuple[list[str], list[ResultType]]: + ) -> list[ResultType]: """ Categorize result types into result types with target and those without. @@ -146,18 +147,18 @@ def _categorize_result_types( result_types (list[ResultType]): list of result types Returns: - tuple[list[str], list[ResultType]]: first element is a list of result types - without `target` attribute; second element is a list of result types with - `target` attribute + list[ResultType]: a list of result types with `target` attribute """ - additional_result_types = [] target_result_types = [] for result_type in result_types: if hasattr(result_type, "target"): target_result_types.append(result_type) else: - additional_result_types.extend(result_type.ascii_symbols) - return additional_result_types, target_result_types + warnings.warn( + f"{result_type} does not have have a pulse representation" + " and it is ignored." + ) + return target_result_types def _validate_device(device: AwsDevice | None) -> None: diff --git a/test/unit_tests/braket/circuits/test_circuit_pulse_sequence.py b/test/unit_tests/braket/circuits/test_circuit_pulse_sequence.py index 6272cb417..c1411f952 100644 --- a/test/unit_tests/braket/circuits/test_circuit_pulse_sequence.py +++ b/test/unit_tests/braket/circuits/test_circuit_pulse_sequence.py @@ -748,4 +748,8 @@ def test_no_target_result_type(device): "}", ] ) - assert circ.pulse_sequence(device).to_ir() == expected + with pytest.warns( + UserWarning, + match=r"StateVector\(\) does not have have a pulse representation and it is ignored.", + ): + assert circ.pulse_sequence(device).to_ir() == expected From 8f7354f02b3014eed85c494e6a0499a000fb4e18 Mon Sep 17 00:00:00 2001 From: Jean-Christophe Jaskula Date: Wed, 20 Mar 2024 19:37:59 -0400 Subject: [PATCH 18/26] remove comment --- src/braket/circuits/circuit_pulse_sequence.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/braket/circuits/circuit_pulse_sequence.py b/src/braket/circuits/circuit_pulse_sequence.py index 6ecfca5a7..d41a0180c 100644 --- a/src/braket/circuits/circuit_pulse_sequence.py +++ b/src/braket/circuits/circuit_pulse_sequence.py @@ -63,7 +63,6 @@ def build_pulse_sequence(self, circuit: cir.Circuit) -> PulseSequence: if not circuit.instructions: return pulse_sequence - # A list of parameters in the circuit to the currently assigned values. if circuit.parameters: raise ValueError("All parameters must be assigned to draw the pulse sequence.") From d108e074a39d3ab71ebcb384f4f539465788ebbd Mon Sep 17 00:00:00 2001 From: Jean-Christophe Jaskula Date: Wed, 20 Mar 2024 19:39:18 -0400 Subject: [PATCH 19/26] rename var --- src/braket/circuits/circuit_pulse_sequence.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/braket/circuits/circuit_pulse_sequence.py b/src/braket/circuits/circuit_pulse_sequence.py index d41a0180c..7fda35e0a 100644 --- a/src/braket/circuits/circuit_pulse_sequence.py +++ b/src/braket/circuits/circuit_pulse_sequence.py @@ -116,14 +116,14 @@ def _get_pulse_sequence(self, gate: Gate, qubit: QubitSet) -> PulseSequence: ) def _find_parametric_gate_calibration( - self, gate: Gate, qubitset: QubitSet, number_assignment_values: int + self, gate: Gate, qubitset: QubitSet, number_assigned_values: int ) -> PulseSequence | None: for key in self._gate_calibrations.pulse_sequences: if ( key[0].name == gate.name and key[1] == qubitset and sum(isinstance(param, FreeParameter) for param in key[0].parameters) - == number_assignment_values + == number_assigned_values ): return self._gate_calibrations.pulse_sequences[key] @@ -154,8 +154,7 @@ def _categorize_result_types( target_result_types.append(result_type) else: warnings.warn( - f"{result_type} does not have have a pulse representation" - " and it is ignored." + f"{result_type} does not have have a pulse representation" " and it is ignored." ) return target_result_types From d56471b9b4e216bd14c011af84cb8619962d0e50 Mon Sep 17 00:00:00 2001 From: Jean-Christophe Jaskula Date: Wed, 20 Mar 2024 19:43:02 -0400 Subject: [PATCH 20/26] fix linter --- src/braket/circuits/result_types.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/braket/circuits/result_types.py b/src/braket/circuits/result_types.py index 915bcfe9b..796aa1113 100644 --- a/src/braket/circuits/result_types.py +++ b/src/braket/circuits/result_types.py @@ -494,10 +494,7 @@ def _to_openqasm(self, serialization_properties: OpenQASMSerializationProperties return f"#pragma braket result expectation {observable_ir}" def _to_pulse_sequence(self) -> PulseSequence: - observable_pulse_sequence = self.observable._to_pulse_sequence( - target=self.target, - ) - return observable_pulse_sequence + return self.observable._to_pulse_sequence(target=self.target) @staticmethod @circuit.subroutine(register=True) From 67cd632b143e6429c2ad6434092e7eb65cb7d517 Mon Sep 17 00:00:00 2001 From: Jean-Christophe Jaskula Date: Wed, 20 Mar 2024 22:27:14 -0400 Subject: [PATCH 21/26] use a user defined frame --- .../circuits/test_circuit_pulse_sequence.py | 91 +++++-------------- 1 file changed, 25 insertions(+), 66 deletions(-) diff --git a/test/unit_tests/braket/circuits/test_circuit_pulse_sequence.py b/test/unit_tests/braket/circuits/test_circuit_pulse_sequence.py index c1411f952..20146bea5 100644 --- a/test/unit_tests/braket/circuits/test_circuit_pulse_sequence.py +++ b/test/unit_tests/braket/circuits/test_circuit_pulse_sequence.py @@ -165,52 +165,21 @@ def get_rigetti_pulse_model(capabilities_json): "version": "1", }, "service": { - "executionWindows": [ - { - "executionDay": "Everyday", - "windowStartHour": "11:00", - "windowEndHour": "12:00", - } - ], + "executionWindows": [], "shotsRange": [1, 10], }, - "provider": { - "specs": { - "1Q": { - "0": { - "fActiveReset": 0.9715, - "fRO": 0.951, - "f1QRB": 0.997339217568556, - "f1QRB_std_err": 0.00006690422818326937, - "f1Q_simultaneous_RB": 0.9949723201166536, - "f1Q_simultaneous_RB_std_err": 0.00021695233492231294, - "T1": 0.000010019627401991471, - "T2": 0.000018156447816365015, - } - }, - "2Q": { - "0-1": { - "fCZ": 0.9586440436264603, - "fCZ_std_err": 0.007025921432645824, - "fCPHASE": 0.9287330972713645, - "fCPHASE_std_err": 0.009709406809550082, - "fXY": 0.9755179214520402, - "fXY_std_err": 0.0060234488782598536, - }, - }, - } - }, + "provider": {"specs": {}}, "action": { "braket.ir.jaqcd.program": { "actionType": "braket.ir.jaqcd.program", "version": ["1"], - "supportedOperations": ["H"], + "supportedOperations": [], } }, "paradigm": { - "qubitCount": 30, - "nativeGateSet": ["ccnot", "cy"], - "connectivity": {"fullyConnected": False, "connectivityGraph": {"1": ["2", "3"]}}, + "qubitCount": 1, + "nativeGateSet": [], + "connectivity": {"fullyConnected": False, "connectivityGraph": {}}, }, "deviceParameters": {}, "pulse": capabilities_json, @@ -232,26 +201,20 @@ def get_oqc_pulse_model(capabilities_json): "version": "1", }, "service": { - "executionWindows": [ - { - "executionDay": "Everyday", - "windowStartHour": "11:00", - "windowEndHour": "12:00", - } - ], + "executionWindows": [], "shotsRange": [1, 10], }, "action": { "braket.ir.jaqcd.program": { "actionType": "braket.ir.jaqcd.program", "version": ["1"], - "supportedOperations": ["H"], + "supportedOperations": [], } }, "paradigm": { "qubitCount": 30, - "nativeGateSet": ["ccnot", "cy"], - "connectivity": {"fullyConnected": False, "connectivityGraph": {"1": ["2", "3"]}}, + "nativeGateSet": [], + "connectivity": {"fullyConnected": False, "connectivityGraph": {}}, }, "deviceParameters": {}, "pulse": capabilities_json, @@ -273,26 +236,20 @@ def get_ionq_model(): "version": "1", }, "service": { - "executionWindows": [ - { - "executionDay": "Everyday", - "windowStartHour": "11:00", - "windowEndHour": "12:00", - } - ], + "executionWindows": [], "shotsRange": [1, 10], }, "action": { "braket.ir.jaqcd.program": { "actionType": "braket.ir.jaqcd.program", "version": ["1"], - "supportedOperations": ["H"], + "supportedOperations": [], } }, "paradigm": { "qubitCount": 30, - "nativeGateSet": ["ccnot", "cy"], - "connectivity": {"fullyConnected": True, "connectivityGraph": {"1": ["2", "3"]}}, + "nativeGateSet": [], + "connectivity": {"fullyConnected": True, "connectivityGraph": {}}, }, "deviceParameters": {}, } @@ -527,22 +484,22 @@ def port(): @pytest.fixture -def predefined_frame_1(port): +def frame_1(port): return Frame( - frame_id="predefined_frame_1", frequency=2e9, port=port, phase=0, is_predefined=True + frame_id="frame_1", frequency=2e9, port=port, phase=0, is_predefined=False ) @pytest.fixture -def pulse_sequence(predefined_frame_1): +def pulse_sequence(frame_1): return ( PulseSequence() .set_frequency( - predefined_frame_1, + frame_1, 6e6, ) .play( - predefined_frame_1, + frame_1, DragGaussianWaveform(length=3e-3, sigma=0.4, beta=0.2, id="drag_gauss_wf"), ) ) @@ -608,9 +565,10 @@ def test_user_defined_gate_calibrations_extension(device, user_defined_gate_cali "OPENQASM 3.0;", "cal {", " bit[1] psb;", + " frame frame_1 = newframe(device_port_x0, 2000000000.0, 0);", " waveform drag_gauss_wf = drag_gaussian(3.0ms, 400.0ms, 0.2, 1, false);", - " set_frequency(predefined_frame_1, 6000000.0);", - " play(predefined_frame_1, drag_gauss_wf);", + " set_frequency(frame_1, 6000000.0);", + " play(frame_1, drag_gauss_wf);", " psb[0] = capture_v0(q1_ro_rx_frame);", "}", ] @@ -628,9 +586,10 @@ def test_with_oxford_device(OQC_device, user_defined_gate_calibrations): "OPENQASM 3.0;", "cal {", " bit[1] psb;", + " frame frame_1 = newframe(device_port_x0, 2000000000.0, 0);", " waveform drag_gauss_wf = drag_gaussian(3.0ms, 400.0ms, 0.2, 1, false);", - " set_frequency(predefined_frame_1, 6000000.0);", - " play(predefined_frame_1, drag_gauss_wf);", + " set_frequency(frame_1, 6000000.0);", + " play(frame_1, drag_gauss_wf);", " psb[0] = capture_v0(r1_measure);", "}", ] From 4bd2e574a41626c4dcccbbd24a1a26b59cbfdff7 Mon Sep 17 00:00:00 2001 From: Jean-Christophe Jaskula Date: Wed, 20 Mar 2024 22:30:01 -0400 Subject: [PATCH 22/26] fix linters --- .../unit_tests/braket/circuits/test_circuit_pulse_sequence.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/test/unit_tests/braket/circuits/test_circuit_pulse_sequence.py b/test/unit_tests/braket/circuits/test_circuit_pulse_sequence.py index 20146bea5..892e56bf0 100644 --- a/test/unit_tests/braket/circuits/test_circuit_pulse_sequence.py +++ b/test/unit_tests/braket/circuits/test_circuit_pulse_sequence.py @@ -485,9 +485,7 @@ def port(): @pytest.fixture def frame_1(port): - return Frame( - frame_id="frame_1", frequency=2e9, port=port, phase=0, is_predefined=False - ) + return Frame(frame_id="frame_1", frequency=2e9, port=port, phase=0, is_predefined=False) @pytest.fixture From 4490c18617173119679b982b6008e238057f4251 Mon Sep 17 00:00:00 2001 From: Jean-Christophe Jaskula Date: Fri, 22 Mar 2024 10:18:11 -0400 Subject: [PATCH 23/26] fix typo --- .../braket/circuits/test_circuit_pulse_sequence.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/unit_tests/braket/circuits/test_circuit_pulse_sequence.py b/test/unit_tests/braket/circuits/test_circuit_pulse_sequence.py index 892e56bf0..18285322c 100644 --- a/test/unit_tests/braket/circuits/test_circuit_pulse_sequence.py +++ b/test/unit_tests/braket/circuits/test_circuit_pulse_sequence.py @@ -652,7 +652,7 @@ def test_expectation_value_result_type_on_one_qubit(device): " waveform q0_q1_cphase_sqrtCPHASE = {0.0, 0.0, 0.0, 0.0};", " play(q0_q1_cphase_frame, q0_q1_cphase_sqrtCPHASE);", " shift_phase(q0_rf_frame, -0.1);", - " rx(pi/2) $1;", # FIXME: this need the right basis rotation + " rx(pi/2) $1;", # FIXME: this needs the right basis rotation " psb[0] = capture_v0(q0_ro_rx_frame);", " psb[1] = capture_v0(q1_ro_rx_frame);", "}", @@ -674,8 +674,8 @@ def test_expectation_value_result_type_on_all_qubits(device): " waveform q0_q1_cphase_sqrtCPHASE = {0.0, 0.0, 0.0, 0.0};", " play(q0_q1_cphase_frame, q0_q1_cphase_sqrtCPHASE);", " shift_phase(q0_rf_frame, -0.1);", - " rx(pi/2) $0;", # FIXME: this need the right basis rotation - " rx(pi/2) $1;", # FIXME: this need the right basis rotation + " rx(pi/2) $0;", # FIXME: this needs the right basis rotation + " rx(pi/2) $1;", # FIXME: this needs the right basis rotation " psb[0] = capture_v0(q0_ro_rx_frame);", " psb[1] = capture_v0(q1_ro_rx_frame);", "}", From 853715089b8d820be3761da94570543596b14051 Mon Sep 17 00:00:00 2001 From: Jean-Christophe Jaskula Date: Fri, 22 Mar 2024 17:29:06 -0400 Subject: [PATCH 24/26] fix error message --- src/braket/circuits/circuit_pulse_sequence.py | 6 ++++-- .../braket/circuits/test_circuit_pulse_sequence.py | 4 +++- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/src/braket/circuits/circuit_pulse_sequence.py b/src/braket/circuits/circuit_pulse_sequence.py index 7fda35e0a..56c972efa 100644 --- a/src/braket/circuits/circuit_pulse_sequence.py +++ b/src/braket/circuits/circuit_pulse_sequence.py @@ -106,9 +106,11 @@ def _get_pulse_sequence(self, gate: Gate, qubit: QubitSet) -> PulseSequence: ) is None ): + parameter_str = ", ".join(str(p) for p in parameters) + qubit_str = ", ".join(str(int(q)) for q in qubit) raise ValueError( - f"No pulse sequence for {gate.name} was provided in the gate" - " calibration set." + f"No pulse sequence for {gate.name}({parameter_str}) on qubit {qubit_str} was" + " provided in the gate calibration set." ) return gate_pulse_sequence( diff --git a/test/unit_tests/braket/circuits/test_circuit_pulse_sequence.py b/test/unit_tests/braket/circuits/test_circuit_pulse_sequence.py index 18285322c..90cc85c76 100644 --- a/test/unit_tests/braket/circuits/test_circuit_pulse_sequence.py +++ b/test/unit_tests/braket/circuits/test_circuit_pulse_sequence.py @@ -637,7 +637,9 @@ def test_pulse_gate(device): def test_missing_calibration(device): circuit = Circuit().rz(1, 0.1) with pytest.raises( - ValueError, match="No pulse sequence for Rz was provided in the gate calibration set." + ValueError, + match=r"No pulse sequence for Rz\(0.1\) on qubit 1 was provided" + " in the gate calibration set.", ): circuit.pulse_sequence(device) From aa5395cce3bfdf6346180d26712220a43cd3c872 Mon Sep 17 00:00:00 2001 From: Jean-Christophe Jaskula Date: Fri, 22 Mar 2024 17:44:35 -0400 Subject: [PATCH 25/26] clean tests --- .../circuits/test_circuit_pulse_sequence.py | 85 ++++++++++--------- 1 file changed, 43 insertions(+), 42 deletions(-) diff --git a/test/unit_tests/braket/circuits/test_circuit_pulse_sequence.py b/test/unit_tests/braket/circuits/test_circuit_pulse_sequence.py index 90cc85c76..e8dfa75cd 100644 --- a/test/unit_tests/braket/circuits/test_circuit_pulse_sequence.py +++ b/test/unit_tests/braket/circuits/test_circuit_pulse_sequence.py @@ -35,7 +35,7 @@ OQC_ARN = "arn:aws:braket:::device/qpu/oqc/Lucy" IONQ_ARN = "arn:aws:braket:::device/qpu/ionq/Harmony" -MOCK_PULSE_MODEL_QPU_PULSE_CAPABILITIES_JSON_1 = { +MOCK_RIGETTI_PULSE_CAPABILITIES_JSON = { "braketSchemaHeader": { "name": "braket.device_schema.pulse.pulse_device_action_properties", "version": "1", @@ -125,7 +125,7 @@ "nativeGateCalibrationsRef": "file://hostname/foo/bar", } -MOCK_PULSE_MODEL_QPU_PULSE_CAPABILITIES_JSON_2 = { +MOCK_OQC_PULSE_CAPABILITIES_JSON = { "braketSchemaHeader": { "name": "braket.device_schema.pulse.pulse_device_action_properties", "version": "1", @@ -264,7 +264,7 @@ def get_ionq_model(): @pytest.fixture -def device(): +def rigetti_device(): response_data_content = { "gates": { "0_1": { @@ -449,21 +449,19 @@ def device(): mock_url_request.return_value.__enter__.return_value = response_data_stream mock_session = Mock() mock_session.get_device.return_value = get_rigetti_pulse_model( - MOCK_PULSE_MODEL_QPU_PULSE_CAPABILITIES_JSON_1 + MOCK_RIGETTI_PULSE_CAPABILITIES_JSON ) yield AwsDevice(RIGETTI_ARN, mock_session) @pytest.fixture -def OQC_device(): +def oqc_device(): response_data_content = {"gates": {}, "waveforms": {}} response_data_stream = io.BytesIO(json.dumps(response_data_content).encode("utf-8")) with patch("urllib.request.urlopen") as mock_url_request: mock_url_request.return_value.__enter__.return_value = response_data_stream mock_session = Mock() - mock_session.get_device.return_value = get_oqc_pulse_model( - MOCK_PULSE_MODEL_QPU_PULSE_CAPABILITIES_JSON_2 - ) + mock_session.get_device.return_value = get_oqc_pulse_model(MOCK_OQC_PULSE_CAPABILITIES_JSON) yield AwsDevice(OQC_ARN, mock_session) @@ -484,20 +482,20 @@ def port(): @pytest.fixture -def frame_1(port): - return Frame(frame_id="frame_1", frequency=2e9, port=port, phase=0, is_predefined=False) +def frame(port): + return Frame(frame_id="frame", frequency=2e9, port=port, phase=0, is_predefined=False) @pytest.fixture -def pulse_sequence(frame_1): +def pulse_sequence(frame): return ( PulseSequence() .set_frequency( - frame_1, + frame, 6e6, ) .play( - frame_1, + frame, DragGaussianWaveform(length=3e-3, sigma=0.4, beta=0.2, id="drag_gauss_wf"), ) ) @@ -513,8 +511,11 @@ def user_defined_gate_calibrations(pulse_sequence): ) -def test_empty_circuit(device): - assert CircuitPulseSequenceBuilder(device).build_pulse_sequence(Circuit()) == PulseSequence() +def test_empty_circuit(rigetti_device): + assert ( + CircuitPulseSequenceBuilder(rigetti_device).build_pulse_sequence(Circuit()) + == PulseSequence() + ) def test_no_device(): @@ -527,15 +528,15 @@ def test_not_supported_device(not_supported_device): CircuitPulseSequenceBuilder(not_supported_device) -def test_unbound_circuit(device): +def test_unbound_circuit(rigetti_device): circuit = Circuit().rz(0, FreeParameter("theta")) with pytest.raises( ValueError, match="All parameters must be assigned to draw the pulse sequence." ): - circuit.pulse_sequence(device) + circuit.pulse_sequence(rigetti_device) -def test_non_parametric_defcal(device): +def test_non_parametric_defcal(rigetti_device): circ = Circuit().rx(0, np.pi / 2) expected = "\n".join( [ @@ -553,52 +554,52 @@ def test_non_parametric_defcal(device): "}", ] ) - assert circ.pulse_sequence(device).to_ir() == expected + assert circ.pulse_sequence(rigetti_device).to_ir() == expected -def test_user_defined_gate_calibrations_extension(device, user_defined_gate_calibrations): +def test_user_defined_gate_calibrations_extension(rigetti_device, user_defined_gate_calibrations): circ = Circuit().z(1) expected = "\n".join( [ "OPENQASM 3.0;", "cal {", " bit[1] psb;", - " frame frame_1 = newframe(device_port_x0, 2000000000.0, 0);", + " frame frame = newframe(device_port_x0, 2000000000.0, 0);", " waveform drag_gauss_wf = drag_gaussian(3.0ms, 400.0ms, 0.2, 1, false);", - " set_frequency(frame_1, 6000000.0);", - " play(frame_1, drag_gauss_wf);", + " set_frequency(frame, 6000000.0);", + " play(frame, drag_gauss_wf);", " psb[0] = capture_v0(q1_ro_rx_frame);", "}", ] ) assert ( - circ.pulse_sequence(device, user_defined_gate_calibrations.pulse_sequences).to_ir() + circ.pulse_sequence(rigetti_device, user_defined_gate_calibrations.pulse_sequences).to_ir() == expected ) -def test_with_oxford_device(OQC_device, user_defined_gate_calibrations): +def test_with_oxford_device(oqc_device, user_defined_gate_calibrations): circ = Circuit().z(1) expected = "\n".join( [ "OPENQASM 3.0;", "cal {", " bit[1] psb;", - " frame frame_1 = newframe(device_port_x0, 2000000000.0, 0);", + " frame frame = newframe(device_port_x0, 2000000000.0, 0);", " waveform drag_gauss_wf = drag_gaussian(3.0ms, 400.0ms, 0.2, 1, false);", - " set_frequency(frame_1, 6000000.0);", - " play(frame_1, drag_gauss_wf);", + " set_frequency(frame, 6000000.0);", + " play(frame, drag_gauss_wf);", " psb[0] = capture_v0(r1_measure);", "}", ] ) assert ( - circ.pulse_sequence(OQC_device, user_defined_gate_calibrations.pulse_sequences).to_ir() + circ.pulse_sequence(oqc_device, user_defined_gate_calibrations.pulse_sequences).to_ir() == expected ) -def test_parametric_defcal(device): +def test_parametric_defcal(rigetti_device): circ = Circuit().cphaseshift(0, 1, 0.1) expected = "\n".join( [ @@ -614,11 +615,11 @@ def test_parametric_defcal(device): ] ) - assert circ.pulse_sequence(device).to_ir() == expected + assert circ.pulse_sequence(rigetti_device).to_ir() == expected -def test_pulse_gate(device): - pulse_sequence = PulseSequence().set_frequency(device.frames["q0_rf_frame"], 1e6) +def test_pulse_gate(rigetti_device): + pulse_sequence = PulseSequence().set_frequency(rigetti_device.frames["q0_rf_frame"], 1e6) circ = Circuit().pulse_gate(0, pulse_sequence) expected = "\n".join( [ @@ -631,20 +632,20 @@ def test_pulse_gate(device): ] ) - assert circ.pulse_sequence(device).to_ir() == expected + assert circ.pulse_sequence(rigetti_device).to_ir() == expected -def test_missing_calibration(device): +def test_missing_calibration(rigetti_device): circuit = Circuit().rz(1, 0.1) with pytest.raises( ValueError, match=r"No pulse sequence for Rz\(0.1\) on qubit 1 was provided" " in the gate calibration set.", ): - circuit.pulse_sequence(device) + circuit.pulse_sequence(rigetti_device) -def test_expectation_value_result_type_on_one_qubit(device): +def test_expectation_value_result_type_on_one_qubit(rigetti_device): circ = Circuit().cphaseshift(0, 1, 0.1).expectation(observable=Observable.X(), target=[1]) expected = "\n".join( [ @@ -663,10 +664,10 @@ def test_expectation_value_result_type_on_one_qubit(device): with pytest.raises( NotImplementedError, match="_to_pulse_sequence has not been implemented yet." ): - assert circ.pulse_sequence(device).to_ir() == expected + assert circ.pulse_sequence(rigetti_device).to_ir() == expected -def test_expectation_value_result_type_on_all_qubits(device): +def test_expectation_value_result_type_on_all_qubits(rigetti_device): circ = Circuit().cphaseshift(0, 1, 0.1).expectation(observable=Observable.X()) expected = "\n".join( [ @@ -686,10 +687,10 @@ def test_expectation_value_result_type_on_all_qubits(device): with pytest.raises( NotImplementedError, match="_to_pulse_sequence has not been implemented yet." ): - assert circ.pulse_sequence(device).to_ir() == expected + assert circ.pulse_sequence(rigetti_device).to_ir() == expected -def test_no_target_result_type(device): +def test_no_target_result_type(rigetti_device): circ = Circuit().rx(0, np.pi / 2).state_vector() expected = "\n".join( [ @@ -711,4 +712,4 @@ def test_no_target_result_type(device): UserWarning, match=r"StateVector\(\) does not have have a pulse representation and it is ignored.", ): - assert circ.pulse_sequence(device).to_ir() == expected + assert circ.pulse_sequence(rigetti_device).to_ir() == expected From 3a9fdf505230374f3ce05193689a512b06a9f768 Mon Sep 17 00:00:00 2001 From: Jean-Christophe Jaskula Date: Mon, 25 Mar 2024 15:22:05 -0400 Subject: [PATCH 26/26] fix typo --- src/braket/circuits/circuit_pulse_sequence.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/braket/circuits/circuit_pulse_sequence.py b/src/braket/circuits/circuit_pulse_sequence.py index 56c972efa..8ea2424f5 100644 --- a/src/braket/circuits/circuit_pulse_sequence.py +++ b/src/braket/circuits/circuit_pulse_sequence.py @@ -74,7 +74,7 @@ def build_pulse_sequence(self, circuit: cir.Circuit) -> PulseSequence: # FIXME: this creates a single cal block, "barrier;" in defcal could be either # global or restricted to the defcal context - # Right they are global + # Right now they are global pulse_sequence += gate_pulse_sequence # Result type columns