diff --git a/.github/workflows/check-format.yml b/.github/workflows/check-format.yml index dbe9599c1..1795135e7 100644 --- a/.github/workflows/check-format.yml +++ b/.github/workflows/check-format.yml @@ -18,7 +18,7 @@ jobs: steps: - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 - name: Set up Python - uses: actions/setup-python@65d7f2d534ac1bc67fcd62888c5f4f3d2cb2b236 # v4.7.1 + uses: actions/setup-python@0a5c61591373683505ea898e09a3ea4f39ef2b9c # v5.0.0 with: python-version: '3.9' - name: Install dependencies diff --git a/.github/workflows/code-freeze.yml b/.github/workflows/code-freeze.yml new file mode 100644 index 000000000..d19f5b48d --- /dev/null +++ b/.github/workflows/code-freeze.yml @@ -0,0 +1,49 @@ +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 it's not a critical fix + if ! [[ "$PR_TITLE" == fix\(critical\):* ]]; then + # and there's an unfrozen prefix + if ! [[ -z $UNFROZEN_PREFIX ]]; then + # check if the branch matches unfrozen prefix + if [[ "$BRANCH_NAME" != $UNFROZEN_PREFIX* ]]; then + echo "Error: You can only merge from branches that start with '$UNFROZEN_PREFIX', or PRs titled with prefix 'fix(critical): '." + exit 1 + fi + # repo is fully frozen + else + echo "Error: You can only merge PRs titled with prefix 'fix(critical): '." + exit 1 + fi + fi diff --git a/.github/workflows/dependent-tests.yml b/.github/workflows/dependent-tests.yml index cfd395241..aca79d599 100644 --- a/.github/workflows/dependent-tests.yml +++ b/.github/workflows/dependent-tests.yml @@ -23,7 +23,7 @@ jobs: steps: - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@65d7f2d534ac1bc67fcd62888c5f4f3d2cb2b236 # v4.7.1 + uses: actions/setup-python@0a5c61591373683505ea898e09a3ea4f39ef2b9c # v5.0.0 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 10aef9c29..b12cde750 100644 --- a/.github/workflows/publish-to-pypi.yml +++ b/.github/workflows/publish-to-pypi.yml @@ -14,7 +14,7 @@ jobs: steps: - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 - name: Set up Python - uses: actions/setup-python@65d7f2d534ac1bc67fcd62888c5f4f3d2cb2b236 # v4.7.1 + uses: actions/setup-python@0a5c61591373683505ea898e09a3ea4f39ef2b9c # v5.0.0 with: python-version: '3.x' - name: Install wheel @@ -26,6 +26,6 @@ jobs: - name: Build a binary wheel and a source tarball run: python setup.py sdist bdist_wheel - name: Publish distribution to PyPI - uses: pypa/gh-action-pypi-publish@b7f401de30cb6434a1e19f805ff006643653240e # release/v1 + uses: pypa/gh-action-pypi-publish@2f6f737ca5f74c637829c0f5c3acd0e29ea5e8bf # release/v1 with: password: ${{ secrets.pypi_token }} diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml index 0c02a7561..42dda2020 100644 --- a/.github/workflows/python-package.yml +++ b/.github/workflows/python-package.yml @@ -26,13 +26,12 @@ jobs: steps: - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@65d7f2d534ac1bc67fcd62888c5f4f3d2cb2b236 # v4.7.1 + uses: actions/setup-python@0a5c61591373683505ea898e09a3ea4f39ef2b9c # v5.0.0 with: python-version: ${{ matrix.python-version }} - name: Install dependencies run: | - pip install --upgrade pip - pip install -e .[test] + pip install tox - name: Run unit tests run: | tox -e unit-tests diff --git a/.github/workflows/twine-check.yml b/.github/workflows/twine-check.yml index f8e01fd72..2c30b8ec0 100644 --- a/.github/workflows/twine-check.yml +++ b/.github/workflows/twine-check.yml @@ -16,7 +16,7 @@ jobs: steps: - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 - name: Set up Python - uses: actions/setup-python@65d7f2d534ac1bc67fcd62888c5f4f3d2cb2b236 # v4.7.1 + uses: actions/setup-python@0a5c61591373683505ea898e09a3ea4f39ef2b9c # v5.0.0 with: python-version: '3.x' - name: Install wheel diff --git a/CHANGELOG.md b/CHANGELOG.md index 19b0fba87..37503c397 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,54 @@ # Changelog +## v1.65.0 (2023-12-21) + +### Features + + * add U and GPhase gates + +## v1.64.2 (2023-12-19) + +### Bug Fixes and Other Changes + + * treating OpenQASM builtin types as constants + +## v1.64.1 (2023-12-12) + +### Bug Fixes and Other Changes + + * make filter more convenient + +## v1.64.0 (2023-12-07) + +### Features + + * add str, repr and getitem to BasisState + +### Bug Fixes and Other Changes + + * update: adding a test to check for circular imports + +## v1.63.0 (2023-12-05) + +### Features + + * Allow reservation ARN in task and job creation + +### Bug Fixes and Other Changes + + * Add Forte 1 device + +## 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 diff --git a/doc/examples-hybrid-jobs.rst b/doc/examples-hybrid-jobs.rst index af873407c..56a1d9ecf 100644 --- a/doc/examples-hybrid-jobs.rst +++ b/doc/examples-hybrid-jobs.rst @@ -8,7 +8,7 @@ Learn more about hybrid jobs on Amazon Braket. :maxdepth: 2 ************************************************************************************************************************************************************************************************ -`Creating your first Hybrid Job `_ +`Creating your first Hybrid Job `_ ************************************************************************************************************************************************************************************************ This tutorial shows how to run your first Amazon Braket Hybrid Job. diff --git a/examples/hybrid_job.py b/examples/hybrid_job.py index 2e09d6463..7f54c9955 100644 --- a/examples/hybrid_job.py +++ b/examples/hybrid_job.py @@ -18,7 +18,14 @@ from braket.jobs.metrics import log_metric -@hybrid_job(device=Devices.Amazon.SV1, wait_until_complete=True) +@hybrid_job( + device=Devices.Amazon.SV1, + wait_until_complete=True, + # If you want to run the job in a device reservation window, + # change the device to the one you've reserved, + # uncomment the following line and fill in your reservation ARN + # reservation_arn="" +) def run_hybrid_job(num_tasks=1): # declare AwsDevice within the hybrid job device = AwsDevice(get_job_device_arn()) diff --git a/examples/reservation.py b/examples/reservation.py new file mode 100644 index 000000000..682f71f50 --- /dev/null +++ b/examples/reservation.py @@ -0,0 +1,24 @@ +# 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 +from braket.devices import Devices + +bell = Circuit().h(0).cnot(0, 1) +device = AwsDevice(Devices.IonQ.Aria1) + +# To run a task in a device reservation, change the device to the one you reserved +# and fill in your reservation ARN +task = device.run(bell, shots=100, reservation_arn="reservation ARN") +print(task.result().measurement_counts) diff --git a/setup.cfg b/setup.cfg index 94f28ec7d..3bad103a3 100644 --- a/setup.cfg +++ b/setup.cfg @@ -5,8 +5,13 @@ test=pytest xfail_strict = true # https://pytest-xdist.readthedocs.io/en/latest/known-limitations.html addopts = - --verbose -n auto --durations=0 --durations-min=1 + --verbose -n logical --durations=0 --durations-min=1 testpaths = test/unit_tests +filterwarnings= + # Issue #557 in `pytest-cov` (currently v4.x) has not moved for a while now, + # but once a resolution has been adopted we can drop this "ignore". + # Ref: https://github.com/pytest-dev/pytest-cov/issues/557 + ignore:The --rsyncdir command line argument and rsyncdirs config variable are deprecated.:DeprecationWarning [isort] line_length = 100 diff --git a/setup.py b/setup.py index f15777485..86ec939e3 100644 --- a/setup.py +++ b/setup.py @@ -55,7 +55,7 @@ "pytest", "pytest-cov", "pytest-rerunfailures", - "pytest-xdist", + "pytest-xdist[psutil]", "sphinx", "sphinx-rtd-theme", "sphinxcontrib-apidoc", diff --git a/src/braket/_sdk/_version.py b/src/braket/_sdk/_version.py index 8cc1a9a43..ec7335e82 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.62.1.dev0" +__version__ = "1.65.1.dev0" diff --git a/src/braket/aws/aws_device.py b/src/braket/aws/aws_device.py index c9a208411..503834dd8 100644 --- a/src/braket/aws/aws_device.py +++ b/src/braket/aws/aws_device.py @@ -121,6 +121,7 @@ def run( poll_interval_seconds: Optional[float] = None, inputs: Optional[dict[str, float]] = None, gate_definitions: Optional[dict[tuple[Gate, QubitSet], PulseSequence]] = None, + reservation_arn: str | None = None, *aws_quantum_task_args, **aws_quantum_task_kwargs, ) -> AwsQuantumTask: @@ -150,6 +151,11 @@ def run( The calibration is defined for a particular `Gate` on a particular `QubitSet` and is represented by a `PulseSequence`. Default: None. + reservation_arn (str | None): The reservation ARN provided by Braket Direct + to reserve exclusive usage for the device to run the quantum task on. + Note: If you are creating tasks in a job that itself was created reservation ARN, + those tasks do not need to be created with the reservation ARN. + Default: None. Returns: AwsQuantumTask: An AwsQuantumTask that tracks the execution on the device. @@ -199,6 +205,7 @@ def run( poll_interval_seconds=poll_interval_seconds or self._poll_interval_seconds, inputs=inputs, gate_definitions=gate_definitions, + reservation_arn=reservation_arn, *aws_quantum_task_args, **aws_quantum_task_kwargs, ) @@ -233,6 +240,7 @@ def run_batch( 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, + reservation_arn: Optional[str] = None, *aws_quantum_task_args, **aws_quantum_task_kwargs, ) -> AwsQuantumTaskBatch: @@ -263,7 +271,11 @@ def run_batch( 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`. + and is represented by a `PulseSequence`. Default: None. + reservation_arn (Optional[str]): The reservation ARN provided by Braket Direct + to reserve exclusive usage for the device to run the quantum task on. + Note: If you are creating tasks in a job that itself was created reservation ARN, + those tasks do not need to be created with the reservation ARN. Default: None. Returns: @@ -290,6 +302,7 @@ def run_batch( poll_interval_seconds=poll_interval_seconds or self._poll_interval_seconds, inputs=inputs, gate_definitions=gate_definitions, + reservation_arn=reservation_arn, *aws_quantum_task_args, **aws_quantum_task_kwargs, ) @@ -564,13 +577,15 @@ 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. diff --git a/src/braket/aws/aws_quantum_job.py b/src/braket/aws/aws_quantum_job.py index 8ae4bc129..1e929e857 100644 --- a/src/braket/aws/aws_quantum_job.py +++ b/src/braket/aws/aws_quantum_job.py @@ -81,6 +81,7 @@ def create( aws_session: AwsSession | None = None, tags: dict[str, str] | None = None, logger: Logger = getLogger(__name__), + reservation_arn: str | None = None, ) -> AwsQuantumJob: """Creates a hybrid job by invoking the Braket CreateJob API. @@ -175,6 +176,10 @@ def create( while waiting for quantum task to be in a terminal state. Default is `getLogger(__name__)` + reservation_arn (str | None): the reservation window arn provided by Braket + Direct to reserve exclusive usage for the device to run the hybrid job on. + Default: None. + Returns: AwsQuantumJob: Hybrid job tracking the execution on Amazon Braket. @@ -201,6 +206,7 @@ def create( checkpoint_config=checkpoint_config, aws_session=aws_session, tags=tags, + reservation_arn=reservation_arn, ) job_arn = aws_session.create_job(**create_job_kwargs) diff --git a/src/braket/aws/aws_quantum_task.py b/src/braket/aws/aws_quantum_task.py index fa58d911d..c490a4190 100644 --- a/src/braket/aws/aws_quantum_task.py +++ b/src/braket/aws/aws_quantum_task.py @@ -105,6 +105,7 @@ def create( tags: dict[str, str] | None = None, inputs: dict[str, float] | None = None, gate_definitions: Optional[dict[tuple[Gate, QubitSet], PulseSequence]] | None = None, + reservation_arn: str | None = None, *args, **kwargs, ) -> AwsQuantumTask: @@ -151,6 +152,12 @@ def create( a `PulseSequence`. Default: None. + reservation_arn (str | None): The reservation ARN provided by Braket Direct + to reserve exclusive usage for the device to run the quantum task on. + Note: If you are creating tasks in a job that itself was created reservation ARN, + those tasks do not need to be created with the reservation ARN. + Default: None. + Returns: AwsQuantumTask: AwsQuantumTask tracking the quantum task execution on the device. @@ -179,6 +186,18 @@ def create( create_task_kwargs.update({"tags": tags}) inputs = inputs or {} + if reservation_arn: + create_task_kwargs.update( + { + "associations": [ + { + "arn": reservation_arn, + "type": "RESERVATION_TIME_WINDOW_ARN", + } + ] + } + ) + if isinstance(task_specification, Circuit): param_names = {param.name for param in task_specification.parameters} unbounded_parameters = param_names - set(inputs.keys()) @@ -477,6 +496,12 @@ async def _wait_for_completion( self._result = None return None + def _has_reservation_arn_from_metadata(self, current_metadata: dict[str, Any]) -> bool: + for association in current_metadata.get("associations", []): + if association.get("type") == "RESERVATION_TIME_WINDOW_ARN": + return True + return False + def _download_result( self, ) -> Union[ @@ -488,7 +513,12 @@ def _download_result( current_metadata["outputS3Directory"] + f"/{AwsQuantumTask.RESULTS_FILENAME}", ) self._result = _format_result(BraketSchemaBase.parse_raw_schema(result_string)) - task_event = {"arn": self.id, "status": self.state(), "execution_duration": None} + task_event = { + "arn": self.id, + "status": self.state(), + "execution_duration": None, + "has_reservation_arn": self._has_reservation_arn_from_metadata(current_metadata), + } try: task_event[ "execution_duration" diff --git a/src/braket/aws/aws_quantum_task_batch.py b/src/braket/aws/aws_quantum_task_batch.py index adf15bda3..24ff15530 100644 --- a/src/braket/aws/aws_quantum_task_batch.py +++ b/src/braket/aws/aws_quantum_task_batch.py @@ -61,6 +61,7 @@ def __init__( 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 = None, + reservation_arn: str | None = None, *aws_quantum_task_args, **aws_quantum_task_kwargs, ): @@ -91,6 +92,11 @@ def __init__( 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: {}. + reservation_arn (str | None): The reservation ARN provided by Braket Direct + to reserve exclusive usage for the device to run the quantum task on. + Note: If you are creating tasks in a job that itself was created reservation ARN, + those tasks do not need to be created with the reservation ARN. + Default: None. """ self._tasks = AwsQuantumTaskBatch._execute( aws_session, @@ -103,6 +109,7 @@ def __init__( poll_timeout_seconds, poll_interval_seconds, inputs, + reservation_arn, *aws_quantum_task_args, **aws_quantum_task_kwargs, ) @@ -120,6 +127,7 @@ def __init__( self._poll_timeout_seconds = poll_timeout_seconds self._poll_interval_seconds = poll_interval_seconds self._inputs = inputs + self._reservation_arn = reservation_arn self._aws_quantum_task_args = aws_quantum_task_args self._aws_quantum_task_kwargs = aws_quantum_task_kwargs @@ -197,6 +205,7 @@ def _execute( 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, + reservation_arn: str | None = None, *args, **kwargs, ) -> list[AwsQuantumTask]: @@ -217,6 +226,7 @@ def _execute( poll_timeout_seconds=poll_timeout_seconds, poll_interval_seconds=poll_interval_seconds, inputs=input_map, + reservation_arn=reservation_arn, *args, **kwargs, ) @@ -248,6 +258,7 @@ def _create_task( shots: int, poll_interval_seconds: float = AwsQuantumTask.DEFAULT_RESULTS_POLL_INTERVAL, inputs: dict[str, float] = None, + reservation_arn: str | None = None, *args, **kwargs, ) -> AwsQuantumTask: @@ -259,6 +270,7 @@ def _create_task( shots, poll_interval_seconds=poll_interval_seconds, inputs=inputs, + reservation_arn=reservation_arn, *args, **kwargs, ) @@ -347,6 +359,7 @@ def retry_unsuccessful_tasks(self) -> bool: self._max_workers, self._poll_timeout_seconds, self._poll_interval_seconds, + self._reservation_arn, *self._aws_quantum_task_args, **self._aws_quantum_task_kwargs, ) diff --git a/src/braket/aws/aws_session.py b/src/braket/aws/aws_session.py index 9523f0573..2ebc3c8d2 100644 --- a/src/braket/aws/aws_session.py +++ b/src/braket/aws/aws_session.py @@ -622,10 +622,12 @@ def search_devices( 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`. + 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: @@ -645,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) diff --git a/src/braket/circuits/ascii_circuit_diagram.py b/src/braket/circuits/ascii_circuit_diagram.py index daef01793..2c7024574 100644 --- a/src/braket/circuits/ascii_circuit_diagram.py +++ b/src/braket/circuits/ascii_circuit_diagram.py @@ -21,8 +21,10 @@ from braket.circuits.compiler_directive import CompilerDirective from braket.circuits.gate import Gate from braket.circuits.instruction import Instruction +from braket.circuits.moments import MomentType from braket.circuits.noise import Noise from braket.circuits.result_type import ResultType +from braket.registers.qubit import Qubit from braket.registers.qubit_set import QubitSet @@ -44,23 +46,26 @@ def build_diagram(circuit: cir.Circuit) -> str: if not circuit.instructions: return "" + if all(m.moment_type == MomentType.GLOBAL_PHASE for m in circuit._moments): + return f"Global phase: {circuit.global_phase}" + circuit_qubits = circuit.qubits circuit_qubits.sort() - # Y Axis Column - y_axis_width = len(str(int(max(circuit_qubits)))) - y_axis_str = "{0:{width}} : |\n".format("T", width=y_axis_width + 1) - for qubit in circuit_qubits: - y_axis_str += "{0:{width}}\n".format(" ", width=y_axis_width + 5) - y_axis_str += "q{0:{width}} : -\n".format(str(int(qubit)), width=y_axis_width) + y_axis_str, global_phase = AsciiCircuitDiagram._prepare_diagram_vars( + circuit, circuit_qubits + ) time_slices = circuit.moments.time_slices() column_strs = [] # Moment columns for time, instructions in time_slices.items(): + global_phase = AsciiCircuitDiagram._compute_moment_global_phase( + global_phase, instructions + ) moment_str = AsciiCircuitDiagram._ascii_diagram_column_set( - str(time), circuit_qubits, instructions + str(time), circuit_qubits, instructions, global_phase ) column_strs.append(moment_str) @@ -71,7 +76,7 @@ def build_diagram(circuit: cir.Circuit) -> str: if target_result_types: column_strs.append( AsciiCircuitDiagram._ascii_diagram_column_set( - "Result Types", circuit_qubits, target_result_types + "Result Types", circuit_qubits, target_result_types, global_phase ) ) @@ -84,6 +89,9 @@ def build_diagram(circuit: cir.Circuit) -> str: # Time on top and bottom lines.append(lines[0]) + if global_phase: + lines.append(f"\nGlobal phase: {global_phase}") + # Additional result types line on bottom if additional_result_types: lines.append(f"\nAdditional result types: {', '.join(additional_result_types)}") @@ -97,6 +105,49 @@ def build_diagram(circuit: cir.Circuit) -> str: return "\n".join(lines) + @staticmethod + def _prepare_diagram_vars( + circuit: cir.Circuit, circuit_qubits: QubitSet + ) -> tuple[str, float | None]: + # Y Axis Column + y_axis_width = len(str(int(max(circuit_qubits)))) + y_axis_str = "{0:{width}} : |\n".format("T", width=y_axis_width + 1) + + global_phase = None + if any(m.moment_type == MomentType.GLOBAL_PHASE for m in circuit._moments): + y_axis_str += "{0:{width}} : |\n".format("GP", width=y_axis_width) + global_phase = 0 + + for qubit in circuit_qubits: + y_axis_str += "{0:{width}}\n".format(" ", width=y_axis_width + 5) + y_axis_str += "q{0:{width}} : -\n".format(str(int(qubit)), width=y_axis_width) + + return y_axis_str, global_phase + + @staticmethod + def _compute_moment_global_phase( + global_phase: float | None, items: list[Instruction] + ) -> float | None: + """ + Compute the integrated phase at a certain moment. + + Args: + global_phase (float | None): The integrated phase up to the computed moment + items (list[Instruction]): list of instructions + + Returns: + float | None: The updated integrated phase. + """ + moment_phase = 0 + for item in items: + if ( + isinstance(item, Instruction) + and isinstance(item.operator, Gate) + and item.operator.name == "GPhase" + ): + moment_phase += item.operator.angle + return global_phase + moment_phase if global_phase is not None else None + @staticmethod def _ascii_group_items( circuit_qubits: QubitSet, @@ -120,7 +171,15 @@ def _ascii_group_items( ): continue - if (isinstance(item, ResultType) and not item.target) or ( + # As a zero-qubit gate, GPhase can be grouped with anything. We set qubit_range + # to an empty list and we just add it to the first group below. + if ( + isinstance(item, Instruction) + and isinstance(item.operator, Gate) + and item.operator.name == "GPhase" + ): + qubit_range = QubitSet() + elif (isinstance(item, ResultType) and not item.target) or ( isinstance(item, Instruction) and isinstance(item.operator, CompilerDirective) ): qubit_range = circuit_qubits @@ -175,7 +234,10 @@ 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]], + global_phase: float | None, ) -> str: """ Return a set of columns in the ASCII string diagram of the circuit for a list of items. @@ -184,6 +246,7 @@ def _ascii_diagram_column_set( col_title (str): title of column set circuit_qubits (QubitSet): qubits in circuit items (list[Union[Instruction, ResultType]]): list of instructions or result types + global_phase (float | None): the integrated global phase up to this set Returns: str: An ASCII string diagram for the column set. @@ -193,7 +256,7 @@ def _ascii_diagram_column_set( groupings = AsciiCircuitDiagram._ascii_group_items(circuit_qubits, items) column_strs = [ - AsciiCircuitDiagram._ascii_diagram_column(circuit_qubits, grouping[1]) + AsciiCircuitDiagram._ascii_diagram_column(circuit_qubits, grouping[1], global_phase) for grouping in groupings ] @@ -220,7 +283,9 @@ 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]], + global_phase: float | None = None, ) -> str: """ Return a column in the ASCII string diagram of the circuit for a given list of items. @@ -228,9 +293,10 @@ def _ascii_diagram_column( Args: circuit_qubits (QubitSet): qubits in circuit items (list[Union[Instruction, ResultType]]): list of instructions or result types + global_phase (float | None): the integrated global phase up to this column Returns: - str: An ASCII string diagram for the specified moment in time for a column. + str: an ASCII string diagram for the specified moment in time for a column. """ symbols = {qubit: "-" for qubit in circuit_qubits} margins = {qubit: " " for qubit in circuit_qubits} @@ -252,12 +318,26 @@ def _ascii_diagram_column( num_after = len(circuit_qubits) - 1 after = ["|"] * (num_after - 1) + ([marker] if num_after else []) ascii_symbols = [ascii_symbol] + after + elif ( + isinstance(item, Instruction) + and isinstance(item.operator, Gate) + and item.operator.name == "GPhase" + ): + target_qubits = circuit_qubits + control_qubits = QubitSet() + target_and_control = QubitSet() + qubits = circuit_qubits + ascii_symbols = "-" * len(circuit_qubits) else: if isinstance(item.target, list): target_qubits = reduce(QubitSet.union, map(QubitSet, item.target), QubitSet()) else: target_qubits = item.target control_qubits = getattr(item, "control", QubitSet()) + map_control_qubit_states = AsciiCircuitDiagram._build_map_control_qubits( + item, control_qubits + ) + target_and_control = target_qubits.union(control_qubits) qubits = QubitSet(range(min(target_and_control), max(target_and_control) + 1)) @@ -288,20 +368,54 @@ def _ascii_diagram_column( else ascii_symbols[item_qubit_index] ) elif qubit in control_qubits: - symbols[qubit] = "C" + symbols[qubit] = "C" if map_control_qubit_states[qubit] else "N" else: symbols[qubit] = "|" # Set the margin to be a connector if not on the first qubit - if qubit != min(target_and_control): + if target_and_control and qubit != min(target_and_control): margins[qubit] = "|" - symbols_width = max([len(symbol) for symbol in symbols.values()]) + output = AsciiCircuitDiagram._create_output(symbols, margins, circuit_qubits, global_phase) + return output + @staticmethod + def _create_output( + symbols: dict[Qubit, str], + margins: dict[Qubit, str], + qubits: QubitSet, + global_phase: float | None, + ) -> str: + symbols_width = max([len(symbol) for symbol in symbols.values()]) output = "" - for qubit in circuit_qubits: + + if global_phase is not None: + global_phase_str = ( + f"{global_phase:.2f}" if isinstance(global_phase, float) else str(global_phase) + ) + symbols_width = max([symbols_width, len(global_phase_str)]) + output += "{0:{fill}{align}{width}}|\n".format( + global_phase_str, + fill=" ", + align="^", + width=symbols_width, + ) + + for qubit in qubits: output += "{0:{width}}\n".format(margins[qubit], width=symbols_width + 1) output += "{0:{fill}{align}{width}}\n".format( symbols[qubit], fill="-", align="<", width=symbols_width + 1 ) return output + + @staticmethod + def _build_map_control_qubits(item: Instruction, control_qubits: QubitSet) -> dict(Qubit, int): + control_state = getattr(item, "control_state", None) + if control_state is not None: + map_control_qubit_states = { + qubit: state for qubit, state in zip(control_qubits, control_state) + } + else: + map_control_qubit_states = {qubit: 1 for qubit in control_qubits} + + return map_control_qubit_states diff --git a/src/braket/circuits/basis_state.py b/src/braket/circuits/basis_state.py index 814444e75..b6ce11bc8 100644 --- a/src/braket/circuits/basis_state.py +++ b/src/braket/circuits/basis_state.py @@ -33,6 +33,18 @@ def __iter__(self): def __eq__(self, other): return self.state == other.state + def __bool__(self): + return any(self.state) + + def __str__(self): + return self.as_string + + def __repr__(self): + return f'BasisState("{self.as_string}")' + + def __getitem__(self, item): + return BasisState(self.state[item]) + BasisStateInput = Union[int, list[int], str, BasisState] diff --git a/src/braket/circuits/braket_program_context.py b/src/braket/circuits/braket_program_context.py index 9d8c69afe..863513565 100644 --- a/src/braket/circuits/braket_program_context.py +++ b/src/braket/circuits/braket_program_context.py @@ -14,7 +14,7 @@ from typing import Optional, Union import numpy as np -from sympy import Expr +from sympy import Expr, Number from braket.circuits import Circuit, Instruction from braket.circuits.gates import Unitary @@ -56,8 +56,15 @@ 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: - raise NotImplementedError + def add_phase_instruction(self, target: tuple[int], phase_value: float) -> None: + """Add a global phase to the circuit. + + Args: + target (tuple[int]): Unused + phase_value (float): The phase value to be applied + """ + instruction = Instruction(BRAKET_GATES["gphase"](phase_value)) + self._circuit.add_instruction(instruction) def add_gate_instruction( self, gate_name: str, target: tuple[int], *params, ctrl_modifiers: list[int], power: float @@ -147,5 +154,8 @@ def handle_parameter_value( otherwise wraps the symbolic expression as a `FreeParameterExpression`. """ if isinstance(value, Expr): - return FreeParameterExpression(value) + evaluated_value = value.evalf() + if isinstance(evaluated_value, Number): + return evaluated_value + return FreeParameterExpression(evaluated_value) return value diff --git a/src/braket/circuits/circuit.py b/src/braket/circuits/circuit.py index e9c4bcd51..1f1e54225 100644 --- a/src/braket/circuits/circuit.py +++ b/src/braket/circuits/circuit.py @@ -26,7 +26,7 @@ from braket.circuits.free_parameter_expression import FreeParameterExpression from braket.circuits.gate import Gate from braket.circuits.instruction import Instruction -from braket.circuits.moments import Moments +from braket.circuits.moments import Moments, MomentType from braket.circuits.noise import Noise from braket.circuits.noise_helpers import ( apply_noise_to_gates, @@ -162,6 +162,17 @@ def depth(self) -> int: """int: Get the circuit depth.""" return self._moments.depth + @property + def global_phase(self) -> float: + """float: Get the global phase of the circuit.""" + return sum( + [ + instr.operator.angle + for moment, instr in self._moments.items() + if moment.moment_type == MomentType.GLOBAL_PHASE + ] + ) + @property def instructions(self) -> list[Instruction]: """Iterable[Instruction]: Get an `iterable` of instructions in the circuit.""" diff --git a/src/braket/circuits/gate.py b/src/braket/circuits/gate.py index 3c59409e5..2183a2329 100644 --- a/src/braket/circuits/gate.py +++ b/src/braket/circuits/gate.py @@ -198,7 +198,7 @@ def _to_openqasm( return ( f"{inv_prefix}{power_prefix}{control_prefix}" - f"{self._qasm_name}{param_string} {', '.join(qubits)};" + f"{self._qasm_name}{param_string}{','.join([f' {qubit}' for qubit in qubits])};" ) @property diff --git a/src/braket/circuits/gate_calibrations.py b/src/braket/circuits/gate_calibrations.py index 6cbdd97d1..57013df4a 100644 --- a/src/braket/circuits/gate_calibrations.py +++ b/src/braket/circuits/gate_calibrations.py @@ -14,7 +14,7 @@ from __future__ import annotations from copy import deepcopy -from typing import Any, Optional +from typing import Any from braket.circuits.gate import Gate from braket.circuits.serialization import ( @@ -91,35 +91,40 @@ def __len__(self): return len(self._pulse_sequences) def filter( - self, gates: Optional[list[Gate]] = None, qubits: Optional[QubitSet] = None - ) -> Optional[GateCalibrations]: + self, + gates: list[Gate] | None = None, + qubits: QubitSet | list[QubitSet] | None = None, + ) -> 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. - qubits (Optional[QubitSet]): An optional `QubitSet` to filter on. + gates (list[Gate] | None): An optional list of gates to filter on. + qubits (QubitSet | list[QubitSet] | None): An optional `QubitSet` or + list of `QubitSet` to filter on. Returns: - Optional[GateCalibrations]: A filtered GateCalibrations object. Otherwise, returns - none if no matches are found. + GateCalibrations: A filtered GateCalibrations object. """ # noqa: E501 keys = self.pulse_sequences.keys() + if isinstance(qubits, QubitSet): + qubits = [qubits] filtered_calibration_keys = [ tup for tup in keys - if (gates is None or tup[0] in gates) and (qubits is None or qubits.issubset(tup[1])) + if (gates is None or tup[0] in gates) + and (qubits is None or any(qset.issubset(tup[1]) for qset in qubits)) ] return GateCalibrations( {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: tuple[Gate, QubitSet] | None = 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 (tuple[Gate, QubitSet] | None): An optional key to get a specific defcal. Default: None Returns: diff --git a/src/braket/circuits/gates.py b/src/braket/circuits/gates.py index 782aeda90..fd6d6254e 100644 --- a/src/braket/circuits/gates.py +++ b/src/braket/circuits/gates.py @@ -30,7 +30,7 @@ angled_ascii_characters, get_angle, ) -from braket.circuits.basis_state import BasisStateInput +from braket.circuits.basis_state import BasisState, BasisStateInput from braket.circuits.free_parameter import FreeParameter from braket.circuits.free_parameter_expression import FreeParameterExpression from braket.circuits.gate import Gate @@ -60,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"]) @@ -91,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) @@ -125,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"]) @@ -156,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,8 +215,128 @@ def i( Gate.register_gate(I) +class GPhase(AngledGate): + r"""Global phase gate. + + Unitary matrix: + + .. math:: \mathtt{gphase}(\gamma) = e^(i \gamma) I_1. + + Args: + angle (Union[FreeParameterExpression, float]): angle in radians. + """ + + def __init__(self, angle: Union[FreeParameterExpression, float]): + # Avoid parent constructor because _qubit_count must be zero + self._qubit_count = self.fixed_qubit_count() + self._ascii_symbols = [] + + if angle is None: + raise ValueError("angle must not be None") + if isinstance(angle, FreeParameterExpression): + self._parameters = [angle] + else: + self._parameters = [float(angle)] # explicit casting in case angle is e.g. np.float32 + + @property + def _qasm_name(self) -> str: + return "gphase" + + def adjoint(self) -> list[Gate]: + return [GPhase(-self.angle)] + + def to_matrix(self) -> np.ndarray: + return np.exp(1j * self.angle) * np.eye(1, dtype=complex) + + def bind_values(self, **kwargs) -> AngledGate: + return get_angle(self, **kwargs) + + @staticmethod + def fixed_qubit_count() -> int: + return 0 + + @staticmethod + @circuit.subroutine(register=True) + def gphase( + angle: Union[FreeParameterExpression, float], + *, + control: Optional[QubitSetInput] = None, + control_state: Optional[BasisStateInput] = None, + power: float = 1, + ) -> Instruction | Iterable[Instruction]: + r"""Global phase gate. + + If the gate is applied with control/negative control modifiers, it is translated in an + equivalent gate using the following definition: `phaseshift(λ) = ctrl @ gphase(λ)`. + The rightmost control qubit is used for the translation. If the polarity of the rightmost + control modifier is negative, the following identity is used: + `negctrl @ gphase(λ) q = x q; ctrl @ gphase(λ) q; x q`. + + Unitary matrix: + + .. math:: \mathtt{gphase}(\gamma) = e^(i \gamma) I_1. + + Args: + angle (Union[FreeParameterExpression, float]): Phase in radians. + control (Optional[QubitSetInput]): Control qubit(s). Default 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 + string, list, or int. 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 + in the \\|1⟩ state. Default "1" * len(control). + power (float): Integer or fractional power to raise the gate to. Negative + powers will be split into an inverse, accompanied by the positive power. + Default 1. + + Returns: + Instruction | Iterable[Instruction]: GPhase instruction. + + Examples: + >>> circ = Circuit().gphase(0.45) + """ + if control is not None: + control_qubits = QubitSet(control) + + control_state = ( + control_state if control_state is not None else (1,) * len(control_qubits) + ) + control_basis_state = BasisState(control_state, len(control_qubits)) + + phaseshift_target = control_qubits[-1] + phaseshift_instruction = PhaseShift.phaseshift( + phaseshift_target, + angle, + control=control_qubits[:-1], + control_state=control_basis_state[:-1], + power=power, + ) + return ( + phaseshift_instruction + if control_basis_state[-1] + else [ + X.x(phaseshift_target), + phaseshift_instruction, + X.x(phaseshift_target), + ] + ) + + return Instruction(GPhase(angle), power=power) + + +Gate.register_gate(GPhase) + + 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"]) @@ -221,7 +367,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) @@ -255,7 +408,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"]) @@ -286,7 +447,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) @@ -320,7 +488,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"]) @@ -351,7 +527,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) @@ -385,7 +566,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"]) @@ -416,7 +605,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) @@ -450,7 +644,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"]) @@ -481,7 +683,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) @@ -515,7 +722,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"]) @@ -546,7 +761,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) @@ -580,7 +800,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"]) @@ -611,7 +839,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) @@ -645,7 +878,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"]) @@ -676,7 +917,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) @@ -710,7 +956,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"]) @@ -741,7 +995,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) @@ -778,7 +1037,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. @@ -799,7 +1065,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. """ @@ -824,7 +1090,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). @@ -858,7 +1129,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. @@ -879,7 +1157,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. """ @@ -904,7 +1182,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). @@ -923,6 +1206,7 @@ def ry( Returns: Iterable[Instruction]: Rx instruction. + Examples: >>> circ = Circuit().ry(0, 0.15) """ @@ -938,7 +1222,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. @@ -980,7 +1271,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). @@ -1014,7 +1310,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. @@ -1054,7 +1357,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). @@ -1091,11 +1399,144 @@ def phaseshift( Gate.register_gate(PhaseShift) +class U(TripleAngledGate): + r"""Generalized single-qubit rotation gate. + + Unitary matrix: + + .. math:: \mathtt{U}(\theta, \phi, \lambda) = \begin{bmatrix} + \cos{(\theta/2)} & -e^{i \lambda} \sin{(\theta/2)} \\ + e^{i \phi} \sin{(\theta/2)} & -e^{i (\phi + \lambda)} \cos{(\theta/2)} + \end{bmatrix}. + + Args: + angle_1 (Union[FreeParameterExpression, float]): theta angle in radians. + angle_2 (Union[FreeParameterExpression, float]): phi angle in radians. + angle_3 (Union[FreeParameterExpression, float]): lambda angle in radians. + """ + + def __init__( + self, + angle_1: Union[FreeParameterExpression, float], + angle_2: Union[FreeParameterExpression, float], + angle_3: Union[FreeParameterExpression, float], + ): + super().__init__( + angle_1=angle_1, + angle_2=angle_2, + angle_3=angle_3, + qubit_count=None, + ascii_symbols=[_multi_angled_ascii_characters("U", angle_1, angle_2, angle_3)], + ) + + @property + def _qasm_name(self) -> str: + return "U" + + def to_matrix(self) -> np.ndarray: + r"""Returns a matrix representation of this gate. + Returns: + ndarray: The matrix representation of this gate. + """ + _theta = self.angle_1 + _phi = self.angle_2 + _lambda = self.angle_3 + return np.array( + [ + [ + np.cos(_theta / 2), + -np.exp(1j * _lambda) * np.sin(_theta / 2), + ], + [ + np.exp(1j * _phi) * np.sin(_theta / 2), + np.exp(1j * (_phi + _lambda)) * np.cos(_theta / 2), + ], + ] + ) + + def adjoint(self) -> list[Gate]: + return [U(-self.angle_1, -self.angle_3, -self.angle_2)] + + @staticmethod + def fixed_qubit_count() -> int: + return 1 + + def bind_values(self, **kwargs) -> TripleAngledGate: + return _get_angles(self, **kwargs) + + @staticmethod + @circuit.subroutine(register=True) + def u( + target: QubitSetInput, + angle_1: Union[FreeParameterExpression, float], + angle_2: Union[FreeParameterExpression, float], + angle_3: Union[FreeParameterExpression, float], + *, + control: Optional[QubitSetInput] = None, + control_state: Optional[BasisStateInput] = None, + power: float = 1, + ) -> Iterable[Instruction]: + r"""Generalized single-qubit rotation gate. + + Unitary matrix: + + .. math:: \mathtt{U}(\theta, \phi, \lambda) = \begin{bmatrix} + \cos{(\theta/2)} & -e^{i \lambda} \sin{(\theta/2)} \\ + e^{i \phi} \sin{(\theta/2)} & -e^{i (\phi + \lambda)} \cos{(\theta/2)} + \end{bmatrix}. + + Args: + target (QubitSetInput): Target qubit(s) + angle_1 (Union[FreeParameterExpression, float]): theta angle in radians. + angle_2 (Union[FreeParameterExpression, float]): phi angle in radians. + angle_3 (Union[FreeParameterExpression, float]): lambda angle in radians. + control (Optional[QubitSetInput]): Control qubit(s). Default 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 + string, list, or int. 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 + in the \\|1⟩ state. Default "1" * len(control). + power (float): Integer or fractional power to raise the gate to. Negative + powers will be split into an inverse, accompanied by the positive power. + Default 1. + + Returns: + Iterable[Instruction]: U instruction. + + Examples: + >>> circ = Circuit().u(0, 0.15, 0.34, 0.52) + """ + return [ + Instruction( + U(angle_1, angle_2, angle_3), + target=qubit, + control=control, + control_state=control_state, + power=power, + ) + for qubit in QubitSet(target) + ] + + +Gate.register_gate(U) + + # Two qubit gates # 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"]) @@ -1128,7 +1569,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 @@ -1155,7 +1603,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"]) @@ -1195,7 +1653,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. @@ -1230,7 +1695,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"]) @@ -1270,7 +1745,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. @@ -1305,7 +1787,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. @@ -1357,7 +1848,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. @@ -1393,10 +1891,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. """ @@ -1419,7 +1927,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. """ @@ -1453,7 +1961,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. @@ -1489,7 +2004,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. @@ -1527,7 +2051,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 @@ -1558,7 +2089,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. @@ -1596,7 +2136,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 @@ -1627,7 +2174,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. @@ -1665,7 +2221,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 @@ -1696,7 +2259,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. @@ -1734,7 +2306,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 @@ -1765,7 +2344,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"]) @@ -1798,7 +2387,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 @@ -1825,7 +2421,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"]) @@ -1858,7 +2464,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 @@ -1885,7 +2498,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"]) @@ -1910,7 +2533,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 @@ -1937,7 +2567,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"]) @@ -1976,7 +2616,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. @@ -2011,7 +2658,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 @@ -2037,7 +2693,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. """ @@ -2071,7 +2727,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. @@ -2107,7 +2770,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 @@ -2133,7 +2805,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. """ @@ -2167,7 +2839,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. @@ -2203,7 +2882,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 @@ -2257,7 +2945,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. @@ -2296,7 +2991,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"]) @@ -2341,7 +3050,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. @@ -2379,7 +3099,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"]) @@ -2421,7 +3155,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 @@ -2452,7 +3197,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. @@ -2497,7 +3249,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). @@ -2531,7 +3288,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. @@ -2576,7 +3340,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). @@ -2610,12 +3379,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__( @@ -2689,7 +3472,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. @@ -2729,7 +3523,7 @@ def ms( class Unitary(Gate): - """Arbitrary unitary gate + """Arbitrary unitary gate. Args: matrix (numpy.ndarray): Unitary matrix which defines the gate. @@ -2792,7 +3586,7 @@ def _transform_matrix_to_ir(matrix: np.ndarray) -> list: @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. @@ -2849,7 +3643,7 @@ def pulse_sequence(self) -> PulseSequence: @property def parameters(self) -> list[FreeParameter]: - """Returns the list of `FreeParameter` s associated with the gate.""" + 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/moments.py b/src/braket/circuits/moments.py index 7cd8e0a9d..6e87db78d 100644 --- a/src/braket/circuits/moments.py +++ b/src/braket/circuits/moments.py @@ -19,6 +19,7 @@ from typing import Any, NamedTuple, Union from braket.circuits.compiler_directive import CompilerDirective +from braket.circuits.gate import Gate from braket.circuits.instruction import Instruction from braket.circuits.noise import Noise from braket.registers.qubit import Qubit @@ -42,6 +43,7 @@ class MomentType(str, Enum): INITIALIZATION_NOISE = "initialization_noise" READOUT_NOISE = "readout_noise" COMPILER_DIRECTIVE = "compiler_directive" + GLOBAL_PHASE = "global_phase" class MomentsKey(NamedTuple): @@ -59,6 +61,7 @@ class MomentsKey(NamedTuple): qubits: QubitSet moment_type: MomentType noise_index: int + subindex: int = 0 class Moments(Mapping[MomentsKey, Instruction]): @@ -106,6 +109,7 @@ def __init__(self, instructions: Iterable[Instruction] | None = None): self._qubits = QubitSet() self._depth = 0 self._time_all_qubits = -1 + self._number_gphase_in_current_moment = 0 self.add(instructions or []) @@ -181,6 +185,17 @@ def _add(self, instruction: Instruction, noise_index: int = 0) -> None: self._time_all_qubits = time elif isinstance(operator, Noise): self.add_noise(instruction) + elif isinstance(operator, Gate) and operator.name == "GPhase": + time = self._get_qubit_times(self._max_times.keys()) + 1 + self._number_gphase_in_current_moment += 1 + key = MomentsKey( + time, + QubitSet([]), + MomentType.GLOBAL_PHASE, + 0, + self._number_gphase_in_current_moment, + ) + self._moments[key] = instruction else: qubit_range = instruction.target.union(instruction.control) time = self._update_qubit_times(qubit_range) @@ -188,14 +203,15 @@ def _add(self, instruction: Instruction, noise_index: int = 0) -> None: self._qubits.update(qubit_range) self._depth = max(self._depth, time + 1) + def _get_qubit_times(self, qubits: QubitSet) -> int: + return max([self._max_time_for_qubit(qubit) for qubit in qubits] + [self._time_all_qubits]) + def _update_qubit_times(self, qubits: QubitSet) -> int: - qubit_max_times = [self._max_time_for_qubit(qubit) for qubit in qubits] + [ - self._time_all_qubits - ] - time = max(qubit_max_times) + 1 + time = self._get_qubit_times(qubits) + 1 # Update time for all specified qubits for qubit in qubits: self._max_times[qubit] = time + self._number_gphase_in_current_moment = 0 return time def add_noise( diff --git a/src/braket/circuits/translations.py b/src/braket/circuits/translations.py index ba536594f..bbb194be3 100644 --- a/src/braket/circuits/translations.py +++ b/src/braket/circuits/translations.py @@ -31,6 +31,7 @@ from braket.ir.jaqcd.program_v1 import Results BRAKET_GATES = { + "gphase": braket_gates.GPhase, "i": braket_gates.I, "h": braket_gates.H, "x": braket_gates.X, @@ -55,6 +56,7 @@ "rx": braket_gates.Rx, "ry": braket_gates.Ry, "rz": braket_gates.Rz, + "U": braket_gates.U, "swap": braket_gates.Swap, "iswap": braket_gates.ISwap, "pswap": braket_gates.PSwap, diff --git a/src/braket/devices/devices.py b/src/braket/devices/devices.py index 8f8730336..f4fe8ff8d 100644 --- a/src/braket/devices/devices.py +++ b/src/braket/devices/devices.py @@ -31,6 +31,7 @@ class _IonQ(str, Enum): Harmony = "arn:aws:braket:us-east-1::device/qpu/ionq/Harmony" Aria1 = "arn:aws:braket:us-east-1::device/qpu/ionq/Aria-1" Aria2 = "arn:aws:braket:us-east-1::device/qpu/ionq/Aria-2" + Forte1 = "arn:aws:braket:us-east-1::device/qpu/ionq/Forte-1" class _OQC(str, Enum): Lucy = "arn:aws:braket:eu-west-2::device/qpu/oqc/Lucy" diff --git a/src/braket/jobs/hybrid_job.py b/src/braket/jobs/hybrid_job.py index ae17c2715..707f18fd5 100644 --- a/src/braket/jobs/hybrid_job.py +++ b/src/braket/jobs/hybrid_job.py @@ -63,6 +63,7 @@ def hybrid_job( aws_session: AwsSession | None = None, tags: dict[str, str] | None = None, logger: Logger = getLogger(__name__), + reservation_arn: str | None = None, ) -> Callable: """Defines a hybrid job by decorating the entry point function. The job will be created when the decorated function is called. @@ -152,6 +153,10 @@ def hybrid_job( 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__)` + reservation_arn (str | None): the reservation window arn provided by Braket + Direct to reserve exclusive usage for the device to run the hybrid job on. + Default: None. + Returns: Callable: the callable for creating a Hybrid Job. """ @@ -205,6 +210,7 @@ def job_wrapper(*args, **kwargs) -> Callable: "output_data_config": output_data_config, "aws_session": aws_session, "tags": tags, + "reservation_arn": reservation_arn, } for key, value in optional_args.items(): if value is not None: diff --git a/src/braket/jobs/quantum_job_creation.py b/src/braket/jobs/quantum_job_creation.py index b935de7eb..657ed0829 100644 --- a/src/braket/jobs/quantum_job_creation.py +++ b/src/braket/jobs/quantum_job_creation.py @@ -55,6 +55,7 @@ def prepare_quantum_job( checkpoint_config: CheckpointConfig | None = None, aws_session: AwsSession | None = None, tags: dict[str, str] | None = None, + reservation_arn: str | None = None, ) -> dict: """Creates a hybrid job by invoking the Braket CreateJob API. @@ -140,6 +141,10 @@ def prepare_quantum_job( hybrid job. Default: {}. + reservation_arn (str | None): the reservation window arn provided by Braket + Direct to reserve exclusive usage for the device to run the hybrid job on. + Default: None. + Returns: dict: Hybrid job tracking the execution on Amazon Braket. @@ -174,6 +179,7 @@ def prepare_quantum_job( job_name, "script", ) + if AwsSession.is_s3_uri(source_module): _process_s3_source_module(source_module, entry_point, aws_session, code_location) else: @@ -230,6 +236,18 @@ def prepare_quantum_job( "tags": tags, } + if reservation_arn: + create_job_kwargs.update( + { + "associations": [ + { + "arn": reservation_arn, + "type": "RESERVATION_TIME_WINDOW_ARN", + } + ] + } + ) + return create_job_kwargs diff --git a/src/braket/tracking/tracker.py b/src/braket/tracking/tracker.py index c30fc774e..84b294ad2 100644 --- a/src/braket/tracking/tracker.py +++ b/src/braket/tracking/tracker.py @@ -159,8 +159,12 @@ def quantum_tasks_statistics(self) -> dict[str, dict[str, Any]]: + details["execution_duration"] ) billed_duration = ( - device_stats.get("billed_execution_duration", timedelta(0)) - + details["billed_duration"] + timedelta(0) + if details.get("has_reservation_arn") + else ( + device_stats.get("billed_execution_duration", timedelta(0)) + + details["billed_duration"] + ) ) device_stats["execution_duration"] = duration @@ -196,14 +200,20 @@ def _(self, event: _TaskCompletionEvent) -> None: # Update task completion data corresponding to the arn only if it exists in resources if event.arn in resources: resources[event.arn]["status"] = event.status + has_reservation_arn = event.has_reservation_arn + resources[event.arn]["has_reservation_arn"] = has_reservation_arn if event.execution_duration: duration = timedelta(milliseconds=event.execution_duration) resources[event.arn]["execution_duration"] = duration - resources[event.arn]["billed_duration"] = max(duration, MIN_SIMULATOR_DURATION) + resources[event.arn]["billed_duration"] = ( + timedelta(milliseconds=0) + if has_reservation_arn + else max(duration, MIN_SIMULATOR_DURATION) + ) def _get_qpu_task_cost(task_arn: str, details: dict) -> Decimal: - if details["status"] in ["FAILED", "CANCELLED"]: + if details["status"] in ["FAILED", "CANCELLED"] or details.get("has_reservation_arn"): return Decimal(0) task_region = task_arn.split(":")[3] diff --git a/src/braket/tracking/tracking_events.py b/src/braket/tracking/tracking_events.py index 6f37183c0..793ff038c 100644 --- a/src/braket/tracking/tracking_events.py +++ b/src/braket/tracking/tracking_events.py @@ -33,6 +33,7 @@ class _TaskCreationEvent(_TrackingEvent): class _TaskCompletionEvent(_TrackingEvent): execution_duration: Optional[float] status: str + has_reservation_arn: bool = False @dataclass diff --git a/test/integ_tests/test_cost_tracking.py b/test/integ_tests/test_cost_tracking.py index 60ddc1b52..7e97c6f78 100644 --- a/test/integ_tests/test_cost_tracking.py +++ b/test/integ_tests/test_cost_tracking.py @@ -20,9 +20,12 @@ from braket.aws import AwsDevice, AwsSession from braket.circuits import Circuit +from braket.devices import Devices from braket.tracking import Tracker from braket.tracking.tracker import MIN_SIMULATOR_DURATION +_RESERVATION_ONLY_DEVICES = {Devices.IonQ.Forte1} + @pytest.mark.parametrize( "qpu", @@ -91,7 +94,7 @@ def test_all_devices_price_search(): tasks = {} for region in AwsDevice.REGIONS: s = AwsSession(boto3.Session(region_name=region)) - for device in devices: + for device in [device for device in devices if device.arn not in _RESERVATION_ONLY_DEVICES]: try: s.get_device(device.arn) 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_reservation_arn.py b/test/integ_tests/test_reservation_arn.py new file mode 100644 index 000000000..e0736f802 --- /dev/null +++ b/test/integ_tests/test_reservation_arn.py @@ -0,0 +1,91 @@ +# 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 sys + +import pytest +from botocore.exceptions import ClientError +from test_create_quantum_job import decorator_python_version + +from braket.aws import AwsDevice +from braket.aws.aws_quantum_job import AwsQuantumJob +from braket.circuits import Circuit +from braket.devices import Devices +from braket.jobs import get_job_device_arn, hybrid_job + + +@pytest.fixture +def reservation_arn(aws_session): + return ( + f"arn:aws:braket:{aws_session.region}" + f":{aws_session.account_id}:reservation/a1b123cd-45e6-789f-gh01-i234567jk8l9" + ) + + +def test_create_task_via_invalid_reservation_arn_on_qpu(reservation_arn): + circuit = Circuit().h(0) + device = AwsDevice(Devices.IonQ.Harmony) + + with pytest.raises(ClientError, match="Reservation arn is invalid"): + device.run( + circuit, + shots=10, + reservation_arn=reservation_arn, + ) + + +def test_create_task_via_reservation_arn_on_simulator(reservation_arn): + circuit = Circuit().h(0) + device = AwsDevice(Devices.Amazon.SV1) + + with pytest.raises(ClientError, match="Braket Direct is not supported for"): + device.run( + circuit, + shots=10, + reservation_arn=reservation_arn, + ) + + +def test_create_job_via_invalid_reservation_arn_on_qpu(aws_session, reservation_arn): + with pytest.raises(ClientError, match="Reservation arn is invalid"): + AwsQuantumJob.create( + device=Devices.IonQ.Harmony, + source_module="test/integ_tests/job_test_script.py", + entry_point="job_test_script:start_here", + wait_until_complete=True, + aws_session=aws_session, + hyperparameters={"test_case": "completed"}, + reservation_arn=reservation_arn, + ) + + +@pytest.mark.xfail( + (sys.version_info.major, sys.version_info.minor) != decorator_python_version(), + raises=RuntimeError, + reason="Python version mismatch", +) +def test_create_job_with_decorator_via_invalid_reservation_arn(reservation_arn): + with pytest.raises(ClientError, match="Reservation arn is invalid"): + + @hybrid_job( + device=Devices.IonQ.Aria1, + reservation_arn=reservation_arn, + ) + def hello_job(): + device = AwsDevice(get_job_device_arn()) + bell = Circuit().h(0).cnot(0, 1) + task = device.run(bell, shots=10) + measurements = task.result().measurements + return measurements + + hello_job() diff --git a/test/unit_tests/braket/aws/common_test_utils.py b/test/unit_tests/braket/aws/common_test_utils.py index 5dcec5fbc..2975912f1 100644 --- a/test/unit_tests/braket/aws/common_test_utils.py +++ b/test/unit_tests/braket/aws/common_test_utils.py @@ -201,6 +201,7 @@ def run_and_assert( poll_interval_seconds, # Treated as positional arg inputs, # Treated as positional arg gate_definitions, # Treated as positional arg + reservation_arn, # Treated as positional arg extra_args, extra_kwargs, ): @@ -222,6 +223,8 @@ def run_and_assert( run_args.append(gate_definitions) run_args += extra_args if extra_args else [] run_kwargs = extra_kwargs or {} + if reservation_arn: + run_kwargs.update({"reservation_arn": reservation_arn}) task = device.run(circuit, *run_args, **run_kwargs) assert task == task_mock @@ -237,6 +240,7 @@ def run_and_assert( poll_interval_seconds, inputs, gate_definitions, + reservation_arn, extra_args, extra_kwargs, ) @@ -263,6 +267,7 @@ def run_batch_and_assert( poll_interval_seconds, inputs, gate_definitions, + reservation_arn, extra_args, extra_kwargs, ): @@ -291,6 +296,8 @@ def run_batch_and_assert( run_args.append(gate_definitions) run_args += extra_args if extra_args else [] run_kwargs = extra_kwargs or {} + if reservation_arn: + run_kwargs.update({"reservation_arn": reservation_arn}) batch = device.run_batch(circuits, *run_args, **run_kwargs) assert batch.tasks == [task_mock for _ in range(len(circuits))] @@ -306,6 +313,7 @@ def run_batch_and_assert( poll_interval_seconds, inputs, gate_definitions, + reservation_arn, extra_args, extra_kwargs, ) @@ -333,6 +341,7 @@ def _create_task_args_and_kwargs( poll_interval_seconds, inputs, gate_definitions, + reservation_arn, extra_args, extra_kwargs, ): @@ -352,6 +361,7 @@ def _create_task_args_and_kwargs( else default_poll_interval, "inputs": inputs, "gate_definitions": gate_definitions, + "reservation_arn": reservation_arn, } ) return create_args, create_kwargs diff --git a/test/unit_tests/braket/aws/test_aws_device.py b/test/unit_tests/braket/aws/test_aws_device.py index 3770d38a2..29adf2517 100644 --- a/test/unit_tests/braket/aws/test_aws_device.py +++ b/test/unit_tests/braket/aws/test_aws_device.py @@ -987,6 +987,16 @@ def test_run_no_extra(aws_quantum_task_mock, device, circuit): ) +@patch("braket.aws.aws_quantum_task.AwsQuantumTask.create") +def test_run_with_reservation_arn(aws_quantum_task_mock, device, circuit): + _run_and_assert( + aws_quantum_task_mock, + device, + circuit, + reservation_arn="arn:aws:braket:us-west-2:123456789123:reservation/a1b123cd-45e6-789f-gh01-i234567jk8l9", + ) + + @patch("braket.aws.aws_quantum_task.AwsQuantumTask") def test_run_param_circuit_with_no_inputs( aws_quantum_task_mock, single_circuit_input, device, s3_destination_folder @@ -1024,6 +1034,38 @@ def test_run_param_circuit_with_inputs( ) +@patch("braket.aws.aws_session.boto3.Session") +@patch("braket.aws.aws_session.AwsSession") +@patch("braket.aws.aws_quantum_task.AwsQuantumTask.create") +def test_run_param_circuit_with_reservation_arn_batch_task( + aws_quantum_task_mock, + aws_session_mock, + boto_session_mock, + single_circuit_input, + device, + s3_destination_folder, +): + inputs = {"theta": 0.2} + circ_1 = Circuit().rx(angle=0.2, target=0) + circuits = [circ_1, single_circuit_input] + + _run_batch_and_assert( + aws_quantum_task_mock, + aws_session_mock, + device, + circuits, + s3_destination_folder, + 10, + 20, + 50, + 43200, + 0.25, + inputs, + None, + reservation_arn="arn:aws:braket:us-west-2:123456789123:reservation/a1b123cd-45e6-789f-gh01-i234567jk8l9", + ) + + @patch("braket.aws.aws_session.boto3.Session") @patch("braket.aws.aws_session.AwsSession") @patch("braket.aws.aws_quantum_task.AwsQuantumTask.create") @@ -1308,13 +1350,14 @@ def test_default_bucket_not_called(aws_quantum_task_mock, device, circuit, s3_de AwsQuantumTask.DEFAULT_RESULTS_POLL_INTERVAL, circuit, s3_destination_folder, - None, - None, - None, - None, - None, - None, - None, + shots=None, + poll_timeout_seconds=None, + poll_interval_seconds=None, + inputs=None, + gate_definitions=None, + reservation_arn=None, + extra_args=None, + extra_kwargs=None, ) device._aws_session.default_bucket.assert_not_called() @@ -1375,6 +1418,8 @@ def test_run_with_positional_args_and_kwargs( 0.25, {}, ["foo"], + "arn:aws:braket:us-west-2:123456789123:reservation/a1b123cd-45e6-789f-gh01-i234567jk8l9", + None, {"bar": 1, "baz": 2}, ) @@ -1489,6 +1534,7 @@ def _run_and_assert( poll_interval_seconds=None, # Treated as positional arg inputs=None, # Treated as positional arg gate_definitions=None, # Treated as positional arg + reservation_arn=None, # Treated as positional arg extra_args=None, extra_kwargs=None, ): @@ -1506,6 +1552,7 @@ def _run_and_assert( poll_interval_seconds, inputs, gate_definitions, + reservation_arn, extra_args, extra_kwargs, ) @@ -1524,6 +1571,7 @@ def _run_batch_and_assert( poll_interval_seconds=None, # Treated as positional arg inputs=None, # Treated as positional arg gate_definitions=None, # Treated as positional arg + reservation_arn=None, # Treated as positional arg extra_args=None, extra_kwargs=None, ): @@ -1544,6 +1592,7 @@ def _run_batch_and_assert( poll_interval_seconds, inputs, gate_definitions, + reservation_arn, extra_args, extra_kwargs, ) 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 19b46d72b..3a36d8e75 100644 --- a/test/unit_tests/braket/aws/test_aws_quantum_job.py +++ b/test/unit_tests/braket/aws/test_aws_quantum_job.py @@ -516,7 +516,12 @@ def device_arn(request): @pytest.fixture -def prepare_job_args(aws_session, device_arn): +def reservation_arn(): + return "arn:aws:braket:us-west-2:123456789123:reservation/a1b123cd-45e6-789f-gh01-i234567jk8l9" + + +@pytest.fixture +def prepare_job_args(aws_session, device_arn, reservation_arn): return { "device": device_arn, "source_module": Mock(), @@ -535,6 +540,7 @@ def prepare_job_args(aws_session, device_arn): "checkpoint_config": Mock(), "aws_session": aws_session, "tags": Mock(), + "reservation_arn": reservation_arn, } @@ -1027,7 +1033,7 @@ def test_initialize_session_local_device(mock_new_session, aws_session): assert AwsQuantumJob._initialize_session(None, device, logger) == mock_new_session() -def test_bad_arn_format(aws_session): +def test_bad_device_arn_format(aws_session): logger = logging.getLogger(__name__) device_not_found = ( "Device ARN is not a valid format: bad-arn-format. For valid Braket ARNs, " 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 99270ad25..656c37dcf 100644 --- a/test/unit_tests/braket/aws/test_aws_quantum_task.py +++ b/test/unit_tests/braket/aws/test_aws_quantum_task.py @@ -203,6 +203,29 @@ def test_metadata_call_if_none(quantum_task): quantum_task._aws_session.get_quantum_task.assert_called_with(quantum_task.id) +def test_has_reservation_arn_from_metadata(quantum_task): + metadata_true = { + "associations": [ + { + "arn": "123", + "type": "RESERVATION_TIME_WINDOW_ARN", + } + ] + } + assert quantum_task._has_reservation_arn_from_metadata(metadata_true) + + metadata_false = { + "status": "RUNNING", + "associations": [ + { + "arn": "123", + "type": "other", + } + ], + } + assert not quantum_task._has_reservation_arn_from_metadata(metadata_false) + + def test_queue_position(quantum_task): state_1 = "QUEUED" _mock_metadata(quantum_task._aws_session, state_1) @@ -560,6 +583,31 @@ def test_create_ahs_problem(aws_session, arn, ahs_problem): ) +def test_create_task_with_reservation_arn(aws_session, arn, ahs_problem): + aws_session.create_quantum_task.return_value = arn + shots = 21 + reservation_arn = ( + "arn:aws:braket:us-west-2:123456789123:reservation/a1b123cd-45e6-789f-gh01-i234567jk8l9" + ) + AwsQuantumTask.create( + aws_session, + SIMULATOR_ARN, + ahs_problem, + S3_TARGET, + shots, + reservation_arn=reservation_arn, + ) + + _assert_create_quantum_task_called_with( + aws_session, + SIMULATOR_ARN, + ahs_problem.to_ir().json(), + S3_TARGET, + shots, + reservation_arn=reservation_arn, + ) + + def test_create_pulse_sequence(aws_session, arn, pulse_sequence): expected_openqasm = "\n".join( [ @@ -1098,6 +1146,7 @@ def _assert_create_quantum_task_called_with( shots, device_parameters=None, tags=None, + reservation_arn=None, ): test_kwargs = { "deviceArn": arn, @@ -1111,6 +1160,17 @@ def _assert_create_quantum_task_called_with( test_kwargs.update({"deviceParameters": device_parameters.json(exclude_none=True)}) if tags is not None: test_kwargs.update({"tags": tags}) + if reservation_arn: + test_kwargs.update( + { + "associations": [ + { + "arn": reservation_arn, + "type": "RESERVATION_TIME_WINDOW_ARN", + } + ] + } + ) aws_session.create_quantum_task.assert_called_with(**test_kwargs) diff --git a/test/unit_tests/braket/aws/test_aws_quantum_task_batch.py b/test/unit_tests/braket/aws/test_aws_quantum_task_batch.py index 5802db110..2c748ecea 100644 --- a/test/unit_tests/braket/aws/test_aws_quantum_task_batch.py +++ b/test/unit_tests/braket/aws/test_aws_quantum_task_batch.py @@ -34,7 +34,16 @@ def test_creation(mock_create): batch_size = 10 batch = AwsQuantumTaskBatch( - Mock(), "foo", _circuits(batch_size), S3_TARGET, 1000, max_parallel=10 + Mock(), + "foo", + _circuits(batch_size), + S3_TARGET, + 1000, + max_parallel=10, + reservaion_arn=( + "arn:aws:braket:us-west-2:123456789123:" + "reservation/a1b123cd-45e6-789f-gh01-i234567jk8l9" + ), ) assert batch.size == batch_size assert batch.tasks == [task_mock for _ in range(batch_size)] @@ -53,7 +62,16 @@ def test_successful(mock_create): batch_size = 15 batch = AwsQuantumTaskBatch( - Mock(), "foo", _circuits(batch_size), S3_TARGET, 1000, max_parallel=10 + Mock(), + "foo", + _circuits(batch_size), + S3_TARGET, + 1000, + max_parallel=10, + reservaion_arn=( + "arn:aws:braket:us-west-2:123456789123:" + "reservation/a1b123cd-45e6-789f-gh01-i234567jk8l9" + ), ) assert batch.size == batch_size assert not batch.unfinished @@ -71,7 +89,16 @@ def test_unsuccessful(mock_create): mock_create.return_value = task_mock batch = AwsQuantumTaskBatch( - Mock(), "foo", [Circuit().h(0).cnot(0, 1)], S3_TARGET, 1000, max_parallel=10 + Mock(), + "foo", + [Circuit().h(0).cnot(0, 1)], + S3_TARGET, + 1000, + max_parallel=10, + reservaion_arn=( + "arn:aws:braket:us-west-2:123456789123:" + "reservation/a1b123cd-45e6-789f-gh01-i234567jk8l9" + ), ) assert not batch.unfinished assert batch.unsuccessful == {task_id} @@ -106,6 +133,10 @@ def test_retry(mock_create): S3_TARGET, 1000, max_parallel=10, + reservaion_arn=( + "arn:aws:braket:us-west-2:123456789123:" + "reservation/a1b123cd-45e6-789f-gh01-i234567jk8l9" + ), ) assert not batch.unfinished assert batch.results(max_retries=0) == [None, result] @@ -142,6 +173,10 @@ def test_abort(mock_threadpool): S3_TARGET, 1000, max_parallel=num_workers, + reservaion_arn=( + "arn:aws:braket:us-west-2:123456789123:" + "reservation/a1b123cd-45e6-789f-gh01-i234567jk8l9" + ), ) @@ -153,7 +188,16 @@ def test_early_abort(mock_submit): with pytest.raises(KeyboardInterrupt): AwsQuantumTaskBatch( - Mock(), "foo", _circuits(batch_size), S3_TARGET, 1000, max_parallel=num_workers + Mock(), + "foo", + _circuits(batch_size), + S3_TARGET, + 1000, + max_parallel=num_workers, + reservaion_arn=( + "arn:aws:braket:us-west-2:123456789123:" + "reservation/a1b123cd-45e6-789f-gh01-i234567jk8l9" + ), ) diff --git a/test/unit_tests/braket/aws/test_aws_session.py b/test/unit_tests/braket/aws/test_aws_session.py index d2ee45a3a..c61d22606 100644 --- a/test/unit_tests/braket/aws/test_aws_session.py +++ b/test/unit_tests/braket/aws/test_aws_session.py @@ -673,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"]}, [ @@ -745,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", + }, ] } ] diff --git a/test/unit_tests/braket/circuits/test_angled_gate.py b/test/unit_tests/braket/circuits/test_angled_gate.py index e76756e29..4e093e5b4 100644 --- a/test/unit_tests/braket/circuits/test_angled_gate.py +++ b/test/unit_tests/braket/circuits/test_angled_gate.py @@ -31,7 +31,7 @@ def test_is_operator(angled_gate): def test_angle_is_none(): - with pytest.raises(ValueError): + with pytest.raises(ValueError, match="angle must not be None"): AngledGate(qubit_count=1, ascii_symbols=["foo"], angle=None) diff --git a/test/unit_tests/braket/circuits/test_ascii_circuit_diagram.py b/test/unit_tests/braket/circuits/test_ascii_circuit_diagram.py index 3f3268f83..916bfb050 100644 --- a/test/unit_tests/braket/circuits/test_ascii_circuit_diagram.py +++ b/test/unit_tests/braket/circuits/test_ascii_circuit_diagram.py @@ -12,6 +12,7 @@ # language governing permissions and limitations under the License. import numpy as np +import pytest from braket.circuits import ( AsciiCircuitDiagram, @@ -29,6 +30,10 @@ def test_empty_circuit(): assert AsciiCircuitDiagram.build_diagram(Circuit()) == "" +def test_only_gphase_circuit(): + assert AsciiCircuitDiagram.build_diagram(Circuit().gphase(0.1)) == "Global phase: 0.1" + + def test_one_gate_one_qubit(): circ = Circuit().h(0) expected = ("T : |0|", " ", "q0 : -H-", "", "T : |0|") @@ -58,6 +63,35 @@ def test_one_gate_one_qubit_rotation_with_parameter(): _assert_correct_diagram(circ, expected) +@pytest.mark.parametrize("target", [0, 1]) +def test_one_gate_with_global_phase(target): + circ = Circuit().x(target=target).gphase(0.15) + expected = ( + "T : |0| 1 |", + "GP : |0|0.15|", + " ", + f"q{target} : -X------", + "", + "T : |0| 1 |", + "", + "Global phase: 0.15", + ) + _assert_correct_diagram(circ, expected) + + +def test_one_gate_with_zero_global_phase(): + circ = Circuit().gphase(-0.15).x(target=0).gphase(0.15) + expected = ( + "T : | 0 | 1 |", + "GP : |-0.15|0.00|", + " ", + "q0 : -X----------", + "", + "T : | 0 | 1 |", + ) + _assert_correct_diagram(circ, expected) + + def test_one_gate_one_qubit_rotation_with_unicode(): theta = FreeParameter("\u03B8") circ = Circuit().rx(angle=theta, target=0) @@ -74,6 +108,24 @@ def test_one_gate_one_qubit_rotation_with_unicode(): _assert_correct_diagram(circ, expected) +def test_one_gate_with_parametric_expression_global_phase_(): + theta = FreeParameter("\u03B8") + circ = Circuit().x(target=0).gphase(2 * theta).x(0).gphase(1) + expected = ( + "T : |0| 1 | 2 |", + "GP : |0|2*θ|2*θ + 1.0|", + " ", + "q0 : -X-X-------------", + "", + "T : |0| 1 | 2 |", + "", + "Global phase: 2*θ + 1.0", + "", + "Unassigned parameters: [θ].", + ) + _assert_correct_diagram(circ, expected) + + def test_one_gate_one_qubit_rotation_with_parameter_assigned(): theta = FreeParameter("theta") circ = Circuit().rx(angle=theta, target=0) @@ -187,6 +239,38 @@ def test_connector_across_two_qubits(): _assert_correct_diagram(circ, expected) +def test_neg_control_qubits(): + circ = Circuit().x(2, control=[0, 1], control_state=[0, 1]) + expected = ( + "T : |0|", + " ", + "q0 : -N-", + " | ", + "q1 : -C-", + " | ", + "q2 : -X-", + "", + "T : |0|", + ) + _assert_correct_diagram(circ, expected) + + +def test_only_neg_control_qubits(): + circ = Circuit().x(2, control=[0, 1], control_state=0) + expected = ( + "T : |0|", + " ", + "q0 : -N-", + " | ", + "q1 : -N-", + " | ", + "q2 : -X-", + "", + "T : |0|", + ) + _assert_correct_diagram(circ, expected) + + def test_connector_across_three_qubits(): circ = Circuit().x(control=(3, 4), target=5).h(range(2, 6)) expected = ( diff --git a/test/unit_tests/braket/circuits/test_basis_state.py b/test/unit_tests/braket/circuits/test_basis_state.py index 023494fae..166e7c8fc 100644 --- a/test/unit_tests/braket/circuits/test_basis_state.py +++ b/test/unit_tests/braket/circuits/test_basis_state.py @@ -51,6 +51,58 @@ ), ) def test_as_props(basis_state_input, size, as_tuple, as_int, as_string): - assert BasisState(basis_state_input, size).as_tuple == as_tuple - assert BasisState(basis_state_input, size).as_int == as_int - assert BasisState(basis_state_input, size).as_string == as_string + basis_state = BasisState(basis_state_input, size) + assert basis_state.as_tuple == as_tuple + assert basis_state.as_int == as_int + assert basis_state.as_string == as_string == str(basis_state) + assert repr(basis_state) == f'BasisState("{as_string}")' + + +@pytest.mark.parametrize( + "basis_state_input, index, substate_input", + ( + ( + "1001", + slice(None), + "1001", + ), + ( + "1001", + 3, + "1", + ), + ( + "1010", + slice(None, None, 2), + "11", + ), + ( + "1010", + slice(1, None, 2), + "00", + ), + ( + "1010", + slice(None, -2), + "10", + ), + ( + "1010", + -1, + "0", + ), + ), +) +def test_indexing(basis_state_input, index, substate_input): + assert BasisState(basis_state_input)[index] == BasisState(substate_input) + + +def test_bool(): + assert all( + [ + BasisState("100"), + BasisState("111"), + BasisState("1"), + ] + ) + assert not BasisState("0") diff --git a/test/unit_tests/braket/circuits/test_circuit.py b/test/unit_tests/braket/circuits/test_circuit.py index 5824967b8..928cff757 100644 --- a/test/unit_tests/braket/circuits/test_circuit.py +++ b/test/unit_tests/braket/circuits/test_circuit.py @@ -1718,6 +1718,52 @@ def foo( inputs={}, ), ), + ( + Circuit().rx(0, np.pi), + OpenQasmProgram( + source="\n".join( + [ + "OPENQASM 3.0;", + "bit[1] b;", + "qubit[1] q;", + "rx(π) q[0];", + "b[0] = measure q[0];", + ] + ), + inputs={}, + ), + ), + ( + Circuit().rx(0, 2 * np.pi), + OpenQasmProgram( + source="\n".join( + [ + "OPENQASM 3.0;", + "bit[1] b;", + "qubit[1] q;", + "rx(τ) q[0];", + "b[0] = measure q[0];", + ] + ), + inputs={}, + ), + ), + ( + Circuit().gphase(0.15).x(0), + OpenQasmProgram( + source="\n".join( + [ + "OPENQASM 3.0;", + "bit[1] b;", + "qubit[1] q;", + "gphase(0.15);", + "x q[0];", + "b[0] = measure q[0];", + ] + ), + inputs={}, + ), + ), ], ) def test_from_ir(expected_circuit, ir): @@ -1997,6 +2043,14 @@ def test_to_unitary_with_compiler_directives_returns_expected_unitary(): ) +def test_to_unitary_with_global_phase(): + circuit = Circuit().x(0) + circuit_unitary = np.array([[0, 1], [1, 0]]) + assert np.allclose(circuit.to_unitary(), circuit_unitary) + circuit = circuit.gphase(np.pi / 2) + assert np.allclose(circuit.to_unitary(), 1j * circuit_unitary) + + @pytest.mark.parametrize( "circuit,expected_unitary", [ @@ -2016,6 +2070,8 @@ def test_to_unitary_with_compiler_directives_returns_expected_unitary(): (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().u(0, 0.15, 0.16, 0.17), gates.U(0.15, 0.16, 0.17).to_matrix()), + (Circuit().gphase(0.15), gates.GPhase(0.15).to_matrix()), (Circuit().phaseshift(0, 0.15), gates.PhaseShift(0.15).to_matrix()), (Circuit().cnot(0, 1), gates.CNot().to_matrix()), (Circuit().cnot(0, 1).add_result_type(ResultType.StateVector()), gates.CNot().to_matrix()), @@ -3104,3 +3160,23 @@ def test_parametrized_pulse_circuit(user_defined_frame): def test_free_param_float_mix(): Circuit().ms(0, 1, 0.1, FreeParameter("theta")) + + +def test_circuit_with_global_phase(): + circuit = Circuit().gphase(0.15).x(0) + assert circuit.global_phase == 0.15 + + assert circuit.to_ir( + ir_type=IRType.OPENQASM, + serialization_properties=OpenQASMSerializationProperties( + qubit_reference_type=QubitReferenceType.PHYSICAL + ), + ).source == "\n".join( + [ + "OPENQASM 3.0;", + "bit[1] b;", + "gphase(0.15);", + "x $0;", + "b[0] = measure $0;", + ] + ) diff --git a/test/unit_tests/braket/circuits/test_gate_calibration.py b/test/unit_tests/braket/circuits/test_gate_calibration.py index 31c2384db..c95ce74a3 100644 --- a/test/unit_tests/braket/circuits/test_gate_calibration.py +++ b/test/unit_tests/braket/circuits/test_gate_calibration.py @@ -57,19 +57,30 @@ def test_gc_copy(pulse_sequence): def test_filter(pulse_sequence): - calibration_key = (Gate.Z(), QubitSet([0, 1])) - calibration_key_2 = (Gate.H(), QubitSet([0, 1])) + calibration_key = (Gate.Z(), QubitSet([0])) + calibration_key_2 = (Gate.H(), QubitSet([1])) + calibration_key_3 = (Gate.CZ(), QubitSet([0, 1])) calibration = GateCalibrations( - {calibration_key: pulse_sequence, calibration_key_2: pulse_sequence} + { + calibration_key: pulse_sequence, + calibration_key_2: pulse_sequence, + calibration_key_3: pulse_sequence, + } ) expected_calibration_1 = GateCalibrations({calibration_key: pulse_sequence}) expected_calibration_2 = GateCalibrations( - {calibration_key: pulse_sequence, calibration_key_2: pulse_sequence} + {calibration_key: pulse_sequence, calibration_key_3: pulse_sequence} ) expected_calibration_3 = GateCalibrations({calibration_key_2: pulse_sequence}) + expected_calibration_4 = GateCalibrations({}) + expected_calibration_5 = calibration + expected_calibration_6 = GateCalibrations({calibration_key_3: pulse_sequence}) assert expected_calibration_1 == calibration.filter(gates=[Gate.Z()]) assert expected_calibration_2 == calibration.filter(qubits=QubitSet(0)) assert expected_calibration_3 == calibration.filter(gates=[Gate.H()], qubits=QubitSet(1)) + assert expected_calibration_4 == calibration.filter(gates=[Gate.Z()], qubits=QubitSet(1)) + assert expected_calibration_5 == calibration.filter(qubits=[QubitSet(0), QubitSet(1)]) + assert expected_calibration_6 == calibration.filter(qubits=QubitSet([0, 1])) def test_to_ir(pulse_sequence): diff --git a/test/unit_tests/braket/circuits/test_gates.py b/test/unit_tests/braket/circuits/test_gates.py index 05b98ade4..fc8fe7787 100644 --- a/test/unit_tests/braket/circuits/test_gates.py +++ b/test/unit_tests/braket/circuits/test_gates.py @@ -35,12 +35,21 @@ from braket.pulse import ArbitraryWaveform, Frame, Port, PulseSequence +class NoTarget: + pass + + class TripleAngle: pass +class SingleNegControlModifier: + pass + + testdata = [ (Gate.H, "h", ir.H, [SingleTarget], {}), + (Gate.GPhase, "gphase", None, [NoTarget, Angle], {}), (Gate.I, "i", ir.I, [SingleTarget], {}), (Gate.X, "x", ir.X, [SingleTarget], {}), (Gate.Y, "y", ir.Y, [SingleTarget], {}), @@ -54,6 +63,7 @@ class TripleAngle: (Gate.Rx, "rx", ir.Rx, [SingleTarget, Angle], {}), (Gate.Ry, "ry", ir.Ry, [SingleTarget, Angle], {}), (Gate.Rz, "rz", ir.Rz, [SingleTarget, Angle], {}), + (Gate.U, "u", None, [SingleTarget, TripleAngle], {}), (Gate.CNot, "cnot", ir.CNot, [SingleTarget, SingleControl], {}), (Gate.CV, "cv", ir.CV, [SingleTarget, SingleControl], {}), (Gate.CCNot, "ccnot", ir.CCNot, [SingleTarget, DoubleControl], {}), @@ -118,9 +128,11 @@ class TripleAngle: ] parameterizable_gates = [ + Gate.GPhase, Gate.Rx, Gate.Ry, Gate.Rz, + Gate.U, Gate.PhaseShift, Gate.PSwap, Gate.XX, @@ -147,6 +159,10 @@ class TripleAngle: ] +def no_target_valid_input(**kwargs): + return {} + + def single_target_valid_input(**kwargs): return {"target": 2} @@ -171,6 +187,10 @@ def single_control_valid_input(**kwargs): return {"control": 0} +def single_neg_control_valid_input(**kwargs): + return {"control": [0], "control_state": [0]} + + def double_control_valid_ir_input(**kwargs): return {"controls": [0, 1]} @@ -193,11 +213,13 @@ def two_dimensional_matrix_valid_input(**kwargs): valid_ir_switcher = { + "NoTarget": no_target_valid_input, "SingleTarget": single_target_valid_input, "DoubleTarget": double_target_valid_ir_input, "Angle": angle_valid_input, "TripleAngle": triple_angle_valid_input, "SingleControl": single_control_valid_input, + "SingleNegControlModifier": single_neg_control_valid_input, "DoubleControl": double_control_valid_ir_input, "MultiTarget": multi_target_valid_input, "TwoDimensionalMatrix": two_dimensional_matrix_valid_ir_input, @@ -232,9 +254,13 @@ def create_valid_subroutine_input(irsubclasses, **kwargs): def create_valid_target_input(irsubclasses): input = {} qubit_set = [] + control_qubit_set = [] + control_state = None # based on the concept that control goes first in target input for subclass in irsubclasses: - if subclass == SingleTarget: + if subclass == NoTarget: + qubit_set.extend(list(no_target_valid_input().values())) + elif subclass == SingleTarget: qubit_set.extend(list(single_target_valid_input().values())) elif subclass == DoubleTarget: qubit_set.extend(list(double_target_valid_ir_input().values())) @@ -242,6 +268,9 @@ def create_valid_target_input(irsubclasses): qubit_set.extend(list(multi_target_valid_input().values())) elif subclass == SingleControl: qubit_set = list(single_control_valid_input().values()) + qubit_set + elif subclass == SingleNegControlModifier: + control_qubit_set = list(single_neg_control_valid_input()["control"]) + control_state = list(single_neg_control_valid_input()["control_state"]) elif subclass == DoubleControl: qubit_set = list(double_control_valid_ir_input().values()) + qubit_set elif subclass in (Angle, TwoDimensionalMatrix, TripleAngle): @@ -249,6 +278,8 @@ def create_valid_target_input(irsubclasses): else: raise ValueError("Invalid subclass") input["target"] = QubitSet(qubit_set) + input["control"] = QubitSet(control_qubit_set) + input["control_state"] = control_state return input @@ -282,7 +313,7 @@ def calculate_qubit_count(irsubclasses): qubit_count += 2 elif subclass == MultiTarget: qubit_count += 3 - elif subclass in (Angle, TwoDimensionalMatrix, TripleAngle): + elif subclass in (NoTarget, Angle, TwoDimensionalMatrix, TripleAngle): pass else: raise ValueError("Invalid subclass") @@ -434,6 +465,18 @@ def test_ir_gate_level(testclass, subroutine_name, irclass, irsubclasses, kwargs OpenQASMSerializationProperties(qubit_reference_type=QubitReferenceType.PHYSICAL), "rz(0.17) $4;", ), + ( + Gate.U(angle_1=0.17, angle_2=3.45, angle_3=5.21), + [4], + OpenQASMSerializationProperties(qubit_reference_type=QubitReferenceType.VIRTUAL), + "U(0.17, 3.45, 5.21) q[4];", + ), + ( + Gate.U(angle_1=0.17, angle_2=3.45, angle_3=5.21), + [4], + OpenQASMSerializationProperties(qubit_reference_type=QubitReferenceType.PHYSICAL), + "U(0.17, 3.45, 5.21) $4;", + ), ( Gate.XX(angle=0.17), [4, 5], @@ -859,9 +902,53 @@ def test_gate_subroutine(testclass, subroutine_name, irclass, irsubclasses, kwar subroutine_input = {"target": multi_targets} if Angle in irsubclasses: subroutine_input.update(angle_valid_input()) + if TripleAngle in irsubclasses: + subroutine_input.update(triple_angle_valid_input()) assert subroutine(**subroutine_input) == Circuit(instruction_list) +@pytest.mark.parametrize( + "control, control_state, instruction_set", + [ + ( + 2, + None, + Instruction(**create_valid_instruction_input(Gate.PhaseShift, [SingleTarget, Angle])), + ), + ( + 2, + [0], + [ + Instruction(operator=Gate.X(), target=2), + Instruction( + **create_valid_instruction_input(Gate.PhaseShift, [SingleTarget, Angle]) + ), + Instruction(operator=Gate.X(), target=2), + ], + ), + ( + [0, 2], + [0, 1], + Instruction( + **create_valid_instruction_input( + Gate.PhaseShift, [SingleTarget, SingleNegControlModifier, Angle] + ) + ), + ), + ], +) +def test_control_gphase_subroutine(control, control_state, instruction_set): + subroutine = getattr(Circuit(), "gphase") + assert subroutine(angle=0.123, control=control, control_state=control_state) == Circuit( + instruction_set + ) + + +def test_angle_gphase_is_none(): + with pytest.raises(ValueError, match="angle must not be None"): + Gate.GPhase(angle=None) + + @pytest.mark.parametrize("testclass,subroutine_name,irclass,irsubclasses,kwargs", testdata) def test_gate_adjoint_expansion_correct(testclass, subroutine_name, irclass, irsubclasses, kwargs): gate = testclass(**create_valid_gate_class_input(irsubclasses, **kwargs)) @@ -925,7 +1012,7 @@ def test_large_unitary(): @pytest.mark.parametrize("gate", parameterizable_gates) def test_bind_values(gate): - triple_angled = gate.__name__ in ("MS",) + triple_angled = gate.__name__ in ("MS", "U") num_params = 3 if triple_angled else 1 thetas = [FreeParameter(f"theta_{i}") for i in range(num_params)] mapping = {f"theta_{i}": i for i in range(num_params)} @@ -1070,6 +1157,13 @@ def test_pulse_gate_to_matrix(): "10", "negctrl @ ctrl @ negctrl @ z q[1], q[2], q[3], q[0];", ), + ( + Gate.GPhase(0.3), + QubitSet([]), + QubitSet([1]), + "1", + "ctrl @ gphase(0.3) q[1];", + ), ), ) def test_gate_control(gate, target, control, control_state, expected_ir): diff --git a/test/unit_tests/braket/jobs/test_hybrid_job.py b/test/unit_tests/braket/jobs/test_hybrid_job.py index b7b7485d7..e757c6a69 100644 --- a/test/unit_tests/braket/jobs/test_hybrid_job.py +++ b/test/unit_tests/braket/jobs/test_hybrid_job.py @@ -105,6 +105,9 @@ def test_decorator_non_defaults( output_data_config = OutputDataConfig(s3Path="s3") aws_session = MagicMock() tags = {"my_tag": "my_value"} + reservation_arn = ( + "arn:aws:braket:us-west-2:123456789123:reservation/a1b123cd-45e6-789f-gh01-i234567jk8l9" + ) logger = getLogger(__name__) with tempfile.TemporaryDirectory() as tempdir: @@ -135,6 +138,7 @@ def test_decorator_non_defaults( output_data_config=output_data_config, aws_session=aws_session, tags=tags, + reservation_arn=reservation_arn, logger=logger, ) def my_entry(a, b: int, c=0, d: float = 1.0, **extras) -> str: @@ -184,6 +188,7 @@ def my_entry(a, b: int, c=0, d: float = 1.0, **extras) -> str: aws_session=aws_session, tags=tags, logger=logger, + reservation_arn=reservation_arn, ) included_module = importlib.import_module("job_module") mock_register.assert_called_with(included_module) 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 1b2c6b5b4..bef4fd643 100644 --- a/test/unit_tests/braket/jobs/test_quantum_job_creation.py +++ b/test/unit_tests/braket/jobs/test_quantum_job_creation.py @@ -175,6 +175,11 @@ def checkpoint_config(bucket, s3_prefix): ) +@pytest.fixture +def reservation_arn(): + return "arn:aws:braket:us-west-2:123456789123:reservation/a1b123cd-45e6-789f-gh01-i234567jk8l9" + + @pytest.fixture def generate_get_job_response(): def _get_job_response(**kwargs): @@ -247,6 +252,7 @@ def create_job_args( output_data_config, checkpoint_config, tags, + reservation_arn, ): if request.param == "fixtures": return dict( @@ -268,6 +274,7 @@ def create_job_args( "checkpoint_config": checkpoint_config, "aws_session": aws_session, "tags": tags, + "reservation_arn": reservation_arn, }.items() if value is not None ) @@ -325,6 +332,7 @@ def _translate_creation_args(create_job_args): hyperparameters = {str(key): str(value) for key, value in hyperparameters.items()} input_data = create_job_args["input_data"] or {} instance_config = create_job_args["instance_config"] or InstanceConfig() + reservation_arn = create_job_args["reservation_arn"] if create_job_args["distribution"] == "data_parallel": distributed_hyperparams = { "sagemaker_distributed_dataparallel_enabled": "true", @@ -367,6 +375,18 @@ def _translate_creation_args(create_job_args): "tags": tags, } + if reservation_arn: + test_kwargs.update( + { + "associations": [ + { + "arn": reservation_arn, + "type": "RESERVATION_TIME_WINDOW_ARN", + } + ] + } + ) + return test_kwargs diff --git a/test/unit_tests/braket/test_imports.py b/test/unit_tests/braket/test_imports.py new file mode 100644 index 000000000..bd545d943 --- /dev/null +++ b/test/unit_tests/braket/test_imports.py @@ -0,0 +1,48 @@ +import importlib +import multiprocessing +import os +import pathlib + +import pytest + + +def test_for_import_cycles(): + # Note, because of all the multiprocessing in this test, when running 'tox', the process + # threads may be made to wait as other tests are running in parallel, making it seems like + # this test is much slower than it actually is. However, splitting the test into a + # parameterized version wasn't able to correctly detect some circular imports when running tox. + modules = get_modules_to_test() + processes = [] + multiprocessing.set_start_method("spawn") + for module in modules: + # We create a separate process to make sure the imports do not interfere with each-other. + process = multiprocessing.Process(target=import_module, args=(module,)) + processes.append(process) + process.start() + + for index, process in enumerate(processes): + process.join() + if process.exitcode != 0: + pytest.fail( + f"Unable to import '{modules[index]}'." + " If all other tests are passing, check for cyclical dependencies." + ) + + +def get_modules_to_test(): + curr_path = pathlib.Path(__file__).resolve() + while "test" in str(curr_path): + curr_path = curr_path.parent + curr_path = curr_path.joinpath("src") + curr_path_len = len(str(curr_path)) + len(os.sep) + modules = [] + for dir_, temp, files in os.walk(curr_path): + # Rather than testing every single python file we just test modules, for now. + if "__init__.py" in files: + braket_module = dir_[curr_path_len:].replace(os.sep, ".") + modules.append(braket_module) + return modules + + +def import_module(module): + importlib.import_module(module) diff --git a/test/unit_tests/braket/tracking/test_tracker.py b/test/unit_tests/braket/tracking/test_tracker.py index fc660b809..a97ce57bc 100644 --- a/test/unit_tests/braket/tracking/test_tracker.py +++ b/test/unit_tests/braket/tracking/test_tracker.py @@ -85,6 +85,18 @@ def test_receive_fake_event(empty_tracker): _TaskCreationEvent( arn="no_price:::region", shots=1000, is_job_task=False, device="something_else" ), + _TaskCreationEvent( + arn="unbilled_task0:::region", + shots=100, + is_job_task=True, + device="qpu/foo", + ), + _TaskCreationEvent( + arn="unbilled_task1:::region", + shots=100, + is_job_task=True, + device="qpu/foo", + ), ] GET_EVENTS = [ @@ -101,6 +113,18 @@ def test_receive_fake_event(empty_tracker): ), _TaskCompletionEvent(arn="task_fail:::region", execution_duration=12345, status="FAILED"), _TaskCompletionEvent(arn="task_cancel:::region", execution_duration=None, status="CANCELLED"), + _TaskCompletionEvent( + arn="unbilled_task0:::region", + execution_duration=123, + status="COMPLETED", + has_reservation_arn=True, + ), + _TaskCompletionEvent( + arn="unbilled_task1:::region", + execution_duration=123, + status="COMPLETED", + has_reservation_arn=True, + ), ] @@ -174,7 +198,12 @@ def test_simulator_task_cost(price_mock, completed_tracker): def test_quantum_task_statistics(completed_tracker): stats = completed_tracker.quantum_tasks_statistics() expected = { - "qpu/foo": {"shots": 200, "tasks": {"COMPLETED": 1, "FAILED": 1}}, + "qpu/foo": { + "shots": 400, + "tasks": {"COMPLETED": 3, "FAILED": 1}, + "execution_duration": timedelta(microseconds=246000), + "billed_execution_duration": timedelta(0), + }, "simulator/bar": { "shots": 1000, "tasks": {"COMPLETED": 2, "CREATED": 1},