diff --git a/.coveragerc b/.coveragerc index fe9662c0a..22cb4caeb 100644 --- a/.coveragerc +++ b/.coveragerc @@ -32,6 +32,11 @@ exclude_lines = # Avoid situation where system version causes coverage issues if sys.version_info.minor == 9: + # Avoid type checking import conditionals + if TYPE_CHECKING: + + + [html] directory = build/coverage diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 04595aed1..4f575c3e2 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -7,6 +7,6 @@ updates: directory: "/" schedule: # Check for updates to GitHub Actions every week - interval: "weekly" + interval: "monthly" commit-message: prefix: infra diff --git a/.github/pr-title-checker-config.json b/.github/pr-title-checker-config.json new file mode 100644 index 000000000..a1c37ac33 --- /dev/null +++ b/.github/pr-title-checker-config.json @@ -0,0 +1,17 @@ +{ + "LABEL": { + "name": "title needs formatting", + "color": "EEEEEE" + }, + "CHECKS": { + "prefixes": ["fix: ", "feat: ", "test: ", "infra: ", "doc: ", "change: ", "break: ", "breaking: ", "deprecation: ", "feature: ", "depr: ", "documentation: "], + "regexp": "docs\\(v[0-9]\\): ", + "regexpFlags": "i", + "ignoreLabels" : ["dont-check-PRs-with-this-label", "meta"] + }, + "MESSAGES": { + "success": "All OK", + "failure": "Failing CI test", + "notice": "" + } +} diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 87dda8a1f..d4fb6616f 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -11,7 +11,7 @@ _Put an `x` in the boxes that apply. You can also fill these out after creating #### General - [ ] I have read the [CONTRIBUTING](https://github.com/amazon-braket/amazon-braket-sdk-python/blob/main/CONTRIBUTING.md) doc -- [ ] I used the commit message format described in [CONTRIBUTING](https://github.com/amazon-braket/amazon-braket-sdk-python/blob/main/CONTRIBUTING.md#commit-your-change) +- [ ] I used the PR title format described in [CONTRIBUTING](https://github.com/amazon-braket/amazon-braket-sdk-python/blob/main/CONTRIBUTING.md#PR-title-format) - [ ] I have updated any necessary documentation, including [READMEs](https://github.com/amazon-braket/amazon-braket-sdk-python/blob/main/README.md) and [API docs](https://github.com/amazon-braket/amazon-braket-sdk-python/blob/main/CONTRIBUTING.md#documentation-guidelines) (if appropriate) #### Tests diff --git a/.github/workflows/check-format.yml b/.github/workflows/check-format.yml index a6106b2d7..e047484ad 100644 --- a/.github/workflows/check-format.yml +++ b/.github/workflows/check-format.yml @@ -16,9 +16,9 @@ jobs: check-code-format: runs-on: ubuntu-latest steps: - - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 - name: Set up Python - uses: actions/setup-python@0a5c61591373683505ea898e09a3ea4f39ef2b9c # v5.0.0 + uses: actions/setup-python@82c7e631bb3cdc910f68e0081d67478d79c6982d # v5.1.0 with: python-version: '3.9' - name: Install dependencies diff --git a/.github/workflows/dependent-tests.yml b/.github/workflows/dependent-tests.yml index aca79d599..ab2656bc0 100644 --- a/.github/workflows/dependent-tests.yml +++ b/.github/workflows/dependent-tests.yml @@ -21,9 +21,9 @@ jobs: - amazon-braket-pennylane-plugin-python steps: - - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@0a5c61591373683505ea898e09a3ea4f39ef2b9c # v5.0.0 + uses: actions/setup-python@82c7e631bb3cdc910f68e0081d67478d79c6982d # v5.1.0 with: python-version: ${{ matrix.python-version }} - name: Install dependencies diff --git a/.github/workflows/pr-title-checker.yml b/.github/workflows/pr-title-checker.yml new file mode 100644 index 000000000..78126bdce --- /dev/null +++ b/.github/workflows/pr-title-checker.yml @@ -0,0 +1,31 @@ +name: "PR Title Checker" + +on: + push: + branches: + - main + pull_request: + branches: + - main + - feature/** + pull_request_target: + types: + - opened + - edited + - synchronize + - labeled + - unlabeled + +permissions: + pull-requests: write + +jobs: + check: + runs-on: ubuntu-latest + steps: + - name: "Check PR Title" + uses: thehanimo/pr-title-checker@v1.4.2 + with: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + pass_on_octokit_error: false + configuration_path: .github/pr-title-checker-config.json #(optional. defaults to .github/pr-title-checker-config.json) diff --git a/.github/workflows/publish-to-pypi.yml b/.github/workflows/publish-to-pypi.yml index a6a359e93..00d746acb 100644 --- a/.github/workflows/publish-to-pypi.yml +++ b/.github/workflows/publish-to-pypi.yml @@ -12,9 +12,9 @@ jobs: name: Build and publish distribution to PyPi runs-on: ubuntu-latest steps: - - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 - name: Set up Python - uses: actions/setup-python@0a5c61591373683505ea898e09a3ea4f39ef2b9c # v5.0.0 + uses: actions/setup-python@82c7e631bb3cdc910f68e0081d67478d79c6982d # v5.1.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@81e9d935c883d0b210363ab89cf05f3894778450 # release/v1 + uses: pypa/gh-action-pypi-publish@8a08d616893759ef8e1aa1f2785787c0b97e20d6 # release/v1 with: password: ${{ secrets.pypi_token }} diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml index f4ac4e916..8ce80816f 100644 --- a/.github/workflows/python-package.yml +++ b/.github/workflows/python-package.yml @@ -24,9 +24,9 @@ jobs: python-version: ["3.9", "3.10", "3.11"] steps: - - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@0a5c61591373683505ea898e09a3ea4f39ef2b9c # v5.0.0 + uses: actions/setup-python@82c7e631bb3cdc910f68e0081d67478d79c6982d # v5.1.0 with: python-version: ${{ matrix.python-version }} - name: Install dependencies @@ -36,7 +36,7 @@ jobs: run: | tox -e unit-tests - name: Upload coverage report to Codecov - uses: codecov/codecov-action@54bcd8715eee62d40e33596ef5e8f0f48dbbccab # v4.1.0 + uses: codecov/codecov-action@e28ff129e5465c2c0dcc6f003fc735cb6ae0c673 # v4.5.0 with: token: ${{ secrets.CODECOV_TOKEN }} if: ${{ strategy.job-index }} == 0 diff --git a/.github/workflows/twine-check.yml b/.github/workflows/twine-check.yml index 2c30b8ec0..3b439db6d 100644 --- a/.github/workflows/twine-check.yml +++ b/.github/workflows/twine-check.yml @@ -14,9 +14,9 @@ jobs: name: Check long description runs-on: ubuntu-latest steps: - - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 - name: Set up Python - uses: actions/setup-python@0a5c61591373683505ea898e09a3ea4f39ef2b9c # v5.0.0 + uses: actions/setup-python@82c7e631bb3cdc910f68e0081d67478d79c6982d # v5.1.0 with: python-version: '3.x' - name: Install wheel diff --git a/CHANGELOG.md b/CHANGELOG.md index 343965f60..7f7e8e7db 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,111 @@ # Changelog +## v1.88.2 (2024-11-18) + +### Bug Fixes and Other Changes + + * Pin cloudpickle==2.2.1 + +## v1.88.1 (2024-10-21) + +### Bug Fixes and Other Changes + + * correct typing for task results methods + +## v1.88.0 (2024-09-27) + +### Deprecations and Removals + + * Mark Aspen-M-3 as deprecated, replace with Ankaa-2 in tests + +### Bug Fixes and Other Changes + + * Update pulse integration tests for Ankaa-2 device + +## v1.87.1 (2024-09-23) + +### Bug Fixes and Other Changes + + * Pass through inputs for SerializableProgram simulation + +## v1.87.0 (2024-09-05) + +### Deprecations and Removals + + * Retire IonQ Harmony + +### Bug Fixes and Other Changes + + * Return observable target if absent for RT + +## v1.86.1 (2024-08-29) + +### Bug Fixes and Other Changes + + * Use observable targets for targetless results + +## v1.86.0 (2024-08-26) + +### Features + + * Rigetti Ankaa + * add off_center to erf_square + +## v1.85.0 (2024-08-20) + +### Features + + * Allow early qubit binding of observables + +## v1.84.0 (2024-07-30) + +### Features + + * support erf_square and swap_phases + +## v1.83.0 (2024-06-28) + +### Deprecations and Removals + + * Remove OQC + +### Features + + * Use `run_multiple` for local batches + +### Documentation Changes + + * update PR title instructions + +## v1.82.0 (2024-06-27) + +### Features + + * Track classical target indices for measurements + +### Bug Fixes and Other Changes + + * Add test to check classical indices used in measurement are preserved between Circuit and OpenQASM Translations. + +## v1.81.1 (2024-06-17) + +### Bug Fixes and Other Changes + + * Error when FreeParameters are named QASM types + +## v1.81.0 (2024-06-13) + +### Features + + * Add IQM to get compiled program convenience method + +## v1.80.1 (2024-06-10) + +### Bug Fixes and Other Changes + + * docs: add stack exchange badge to the readme + * Implement `braket.ahs.AnalogHamiltonianSimulation.from_ir()` + ## v1.80.0 (2024-05-22) ### Features diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 0df22f51e..5635cd689 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -95,10 +95,18 @@ You can also pass in various pytest arguments `tox -e integ-tests -- your-argume 1. Run `tox`, to run all the unit tests, linters, and documentation creation, and verify that all checks and tests pass. 1. If your changes include documentation changes, please see the [Documentation Guidelines](#documentation-guidelines). +### Send a Pull Request + +GitHub provides additional documentation on [Creating a Pull Request](https://help.github.com/articles/creating-a-pull-request/). + +Please remember to: +* Use PR titles that follow the guidelines under [PR Title Format](#pr-title-format). +* Send us a pull request, answering any default questions in the pull request interface. +* Pay attention to any automated CI failures reported in the pull request, and stay involved in the conversation. -### Commit Your Change +#### PR Title Format -We use commit messages to update the project version number and generate changelog entries, so it's important for them to follow the right format. Valid commit messages include a prefix, separated from the rest of the message by a colon and a space. Here are a few examples: +We use commit messages to update the project version number and generate changelog entries. The PR title is used as the commit message when merging a PR, so it's important for PR titles to follow the right format. Valid PR titles include a prefix, separated from the rest of the message by a colon and a space. Here are a few examples: ``` feature: support new parameter for `xyz` @@ -122,16 +130,6 @@ Some of the prefixes allow abbreviation ; `break`, `feat`, `depr`, and `doc` are For the rest of the message, use imperative style and keep things concise but informative. See [How to Write a Git Commit Message](https://chris.beams.io/posts/git-commit/) for guidance. -### Send a Pull Request - -GitHub provides additional documentation on [Creating a Pull Request](https://help.github.com/articles/creating-a-pull-request/). - -Please remember to: -* Use commit messages (and PR titles) that follow the guidelines under [Commit Your Change](#commit-your-change). -* Send us a pull request, answering any default questions in the pull request interface. -* Pay attention to any automated CI failures reported in the pull request, and stay involved in the conversation. - - ```mermaid timeline title Code integration journey (CI) diff --git a/README.md b/README.md index 0c935853d..3c3e85d51 100644 --- a/README.md +++ b/README.md @@ -5,6 +5,7 @@ [![Build status](https://github.com/amazon-braket/amazon-braket-sdk-python/actions/workflows/python-package.yml/badge.svg?branch=main)](https://github.com/amazon-braket/amazon-braket-sdk-python/actions/workflows/python-package.yml) [![codecov](https://codecov.io/gh/amazon-braket/amazon-braket-sdk-python/branch/main/graph/badge.svg?token=1lsqkZL3Ll)](https://codecov.io/gh/amazon-braket/amazon-braket-sdk-python) [![Documentation Status](https://img.shields.io/readthedocs/amazon-braket-sdk-python.svg?logo=read-the-docs)](https://amazon-braket-sdk-python.readthedocs.io/en/latest/?badge=latest) +[![Stack Overflow](https://img.shields.io/badge/StackExchange-Ask%20questions-blue?logo=stackexchange)](https://quantumcomputing.stackexchange.com/questions/tagged/amazon-braket) The Amazon Braket Python SDK is an open source library that provides a framework that you can use to interact with quantum computing hardware devices through Amazon Braket. @@ -137,7 +138,7 @@ import boto3 from braket.circuits import Circuit from braket.aws import AwsDevice -device = AwsDevice("arn:aws:braket:::device/qpu/rigetti/Aspen-8") +device = AwsDevice("arn:aws:braket:::device/qpu/rigetti/Ankaa-2") bell = Circuit().h(0).cnot(0, 1) task = device.run(bell) diff --git a/examples/bell_result_types.py b/examples/bell_result_types.py index 2bcb87b79..9271d09fc 100644 --- a/examples/bell_result_types.py +++ b/examples/bell_result_types.py @@ -11,7 +11,7 @@ # ANY KIND, either express or implied. See the License for the specific # language governing permissions and limitations under the License. -from braket.circuits import Circuit, Observable +from braket.circuits import Circuit, observables from braket.devices import LocalSimulator device = LocalSimulator() @@ -24,7 +24,7 @@ .h(0) .cnot(0, 1) .probability(target=[0]) - .expectation(observable=Observable.Z(), target=[1]) + .expectation(observable=observables.Z(1)) .amplitude(state=["00"]) .state_vector() ) @@ -45,9 +45,9 @@ Circuit() .h(0) .cnot(0, 1) - .expectation(observable=Observable.Y() @ Observable.X(), target=[0, 1]) - .variance(observable=Observable.Y() @ Observable.X(), target=[0, 1]) - .sample(observable=Observable.Y() @ Observable.X(), target=[0, 1]) + .expectation(observable=observables.Y(0) @ observables.X(1)) + .variance(observable=observables.Y(0) @ observables.X(1)) + .sample(observable=observables.Y(0) @ observables.X(1)) ) # When shots>0 for a simulator, probability, expectation, variance are calculated from measurements diff --git a/examples/hybrid_job.py b/examples/hybrid_job.py index 7f54c9955..545c0505c 100644 --- a/examples/hybrid_job.py +++ b/examples/hybrid_job.py @@ -12,7 +12,7 @@ # language governing permissions and limitations under the License. from braket.aws import AwsDevice -from braket.circuits import Circuit, FreeParameter, Observable +from braket.circuits import Circuit, FreeParameter, observables from braket.devices import Devices from braket.jobs import get_job_device_arn, hybrid_job from braket.jobs.metrics import log_metric @@ -34,7 +34,7 @@ def run_hybrid_job(num_tasks=1): circ = Circuit() circ.rx(0, FreeParameter("theta")) circ.cnot(0, 1) - circ.expectation(observable=Observable.X(), target=0) + circ.expectation(observable=observables.X(0)) # initial parameter theta = 0.0 diff --git a/examples/hybrid_job_script.py b/examples/hybrid_job_script.py index b544ff9df..f7db6df94 100644 --- a/examples/hybrid_job_script.py +++ b/examples/hybrid_job_script.py @@ -13,7 +13,7 @@ from braket.aws import AwsDevice, AwsQuantumJob -from braket.circuits import Circuit, FreeParameter, Observable +from braket.circuits import Circuit, FreeParameter, observables from braket.devices import Devices from braket.jobs import get_job_device_arn, save_job_result from braket.jobs.metrics import log_metric @@ -27,7 +27,7 @@ def run_hybrid_job(num_tasks: int): circ = Circuit() circ.rx(0, FreeParameter("theta")) circ.cnot(0, 1) - circ.expectation(observable=Observable.X(), target=0) + circ.expectation(observable=observables.X(0)) # initial parameter theta = 0.0 diff --git a/examples/local_noise_simulation.py b/examples/local_noise_simulation.py index 0857bfd0c..8c173dd51 100644 --- a/examples/local_noise_simulation.py +++ b/examples/local_noise_simulation.py @@ -11,7 +11,7 @@ # ANY KIND, either express or implied. See the License for the specific # language governing permissions and limitations under the License. -from braket.circuits import Circuit, Noise +from braket.circuits import Circuit, noises from braket.devices import LocalSimulator device = LocalSimulator("braket_dm") @@ -23,7 +23,7 @@ circuit = Circuit().x(0).x(1) -noise = Noise.BitFlip(probability=0.1) +noise = noises.BitFlip(probability=0.1) circuit.apply_gate_noise(noise) print("Second example: ") print(circuit) diff --git a/setup.py b/setup.py index 6763c4a55..2e1afe1c7 100644 --- a/setup.py +++ b/setup.py @@ -28,15 +28,17 @@ package_dir={"": "src"}, install_requires=[ "amazon-braket-schemas>=1.21.3", - "amazon-braket-default-simulator>=1.21.4", + "amazon-braket-default-simulator>=1.26.0", "oqpy~=0.3.5", "backoff", "boltons", "boto3>=1.28.53", + # SageMaker pinned cloudpickle==2.2.1 + # see https://github.com/aws/sagemaker-python-sdk/issues/4871 "cloudpickle==2.2.1", "nest-asyncio", "networkx", - "numpy<2", + "numpy", "openpulse", "openqasm3", "sympy", @@ -46,7 +48,7 @@ "test": [ "black", "botocore", - "flake8<=5.0.4", + "flake8", "isort", "jsonschema==3.2.0", "pre-commit", @@ -55,11 +57,13 @@ "pytest-cov", "pytest-rerunfailures", "pytest-xdist[psutil]", + "tox", + ], + "docs": [ "sphinx", "sphinx-rtd-theme", "sphinxcontrib-apidoc", - "tox", - ] + ], }, include_package_data=True, url="https://github.com/amazon-braket/amazon-braket-sdk-python", diff --git a/src/braket/_sdk/_version.py b/src/braket/_sdk/_version.py index ce2f1eb66..3a2292d9b 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.80.1.dev0" +__version__ = "1.88.3.dev0" diff --git a/src/braket/ahs/analog_hamiltonian_simulation.py b/src/braket/ahs/analog_hamiltonian_simulation.py index af02471e2..f11675d41 100644 --- a/src/braket/ahs/analog_hamiltonian_simulation.py +++ b/src/braket/ahs/analog_hamiltonian_simulation.py @@ -23,6 +23,7 @@ from braket.ahs.hamiltonian import Hamiltonian from braket.ahs.local_detuning import LocalDetuning from braket.device_schema import DeviceActionType +from braket.timings.time_series import TimeSeries class AnalogHamiltonianSimulation: @@ -49,6 +50,52 @@ def hamiltonian(self) -> Hamiltonian: """Hamiltonian: The hamiltonian to simulate.""" return self._hamiltonian + @staticmethod + def from_ir(source: ir.Program) -> AnalogHamiltonianSimulation: + """Converts the canonical intermediate representation into + the Analog Hamiltonian Simulation. + + Args: + source (ir.Program): The IR representation of the circuit. + + Returns: + AnalogHamiltonianSimulation: The Analog Hamiltonian Simulation. + """ + atom_arrangement = AtomArrangement() + for site, fill in zip(source.setup.ahs_register.sites, source.setup.ahs_register.filling): + atom_arrangement.add( + coordinate=site, site_type=SiteType.FILLED if fill == 1 else SiteType.VACANT + ) + hamiltonian = Hamiltonian() + for term in source.hamiltonian.drivingFields: + amplitude = TimeSeries.from_lists( + times=term.amplitude.time_series.times, + values=term.amplitude.time_series.values, + ) + phase = TimeSeries.from_lists( + times=term.phase.time_series.times, + values=term.phase.time_series.values, + ) + detuning = TimeSeries.from_lists( + times=term.detuning.time_series.times, + values=term.detuning.time_series.values, + ) + hamiltonian += DrivingField( + amplitude=amplitude, + phase=phase, + detuning=detuning, + ) + for term in source.hamiltonian.localDetuning: + hamiltonian += LocalDetuning.from_lists( + times=term.magnitude.time_series.times, + values=term.magnitude.time_series.values, + pattern=term.magnitude.pattern, + ) + return AnalogHamiltonianSimulation( + register=atom_arrangement, + hamiltonian=hamiltonian, + ) + def to_ir(self) -> ir.Program: """Converts the Analog Hamiltonian Simulation into the canonical intermediate representation. diff --git a/src/braket/ahs/field.py b/src/braket/ahs/field.py index 1522b9d65..28319cc99 100644 --- a/src/braket/ahs/field.py +++ b/src/braket/ahs/field.py @@ -66,3 +66,30 @@ def discretize( discretized_pattern = self.pattern.discretize(pattern_resolution) discretized_field = Field(time_series=discretized_time_series, pattern=discretized_pattern) return discretized_field + + @staticmethod + def from_lists(times: list[Decimal], values: list[Decimal], pattern: list[Decimal]) -> Field: + """Builds Field from lists of time points, values and pattern. + + Args: + times (list[Decimal]): The time points of the field + values (list[Decimal]): The values of the field + pattern (list[Decimal]): The pattern of the field + + Raises: + ValueError: If the length of times and values differs. + + Returns: + Field: Field. + """ + if not (len(times) == len(values)): + raise ValueError( + f"The lengths of the lists for times({len(times)}) and values({len(values)})\ + are not equal" + ) + + time_series = TimeSeries.from_lists(times=times, values=values) + + field = Field(time_series=time_series, pattern=Pattern(pattern)) + + return field diff --git a/src/braket/aws/aws_device.py b/src/braket/aws/aws_device.py index 041098f5a..598271424 100644 --- a/src/braket/aws/aws_device.py +++ b/src/braket/aws/aws_device.py @@ -76,6 +76,7 @@ class AwsDevice(Device): "Cz": "CZ", "Cphaseshift": "CPhaseShift", "Xy": "XY", + "Iswap": "ISwap", } def __init__( diff --git a/src/braket/aws/aws_quantum_task_batch.py b/src/braket/aws/aws_quantum_task_batch.py index 300963a6f..30c3a8658 100644 --- a/src/braket/aws/aws_quantum_task_batch.py +++ b/src/braket/aws/aws_quantum_task_batch.py @@ -16,7 +16,7 @@ import time from concurrent.futures.thread import ThreadPoolExecutor from itertools import repeat -from typing import Any, Union +from typing import TYPE_CHECKING, Any, Union from braket.ahs.analog_hamiltonian_simulation import AnalogHamiltonianSimulation from braket.annealing import Problem @@ -30,6 +30,14 @@ from braket.registers.qubit_set import QubitSet from braket.tasks.quantum_task_batch import QuantumTaskBatch +if TYPE_CHECKING: + from braket.tasks.analog_hamiltonian_simulation_quantum_task_result import ( + AnalogHamiltonianSimulationQuantumTaskResult, + ) + from braket.tasks.annealing_quantum_task_result import AnnealingQuantumTaskResult + from braket.tasks.gate_model_quantum_task_result import GateModelQuantumTaskResult + from braket.tasks.photonic_model_quantum_task_result import PhotonicModelQuantumTaskResult + class AwsQuantumTaskBatch(QuantumTaskBatch): """Executes a batch of quantum tasks in parallel. @@ -331,7 +339,12 @@ def results( fail_unsuccessful: bool = False, max_retries: int = MAX_RETRIES, use_cached_value: bool = True, - ) -> list[AwsQuantumTask]: + ) -> list[ + GateModelQuantumTaskResult + | AnnealingQuantumTaskResult + | PhotonicModelQuantumTaskResult + | AnalogHamiltonianSimulationQuantumTaskResult + ]: """Retrieves the result of every quantum task in the batch. Polling for results happens in parallel; this method returns when all quantum tasks @@ -348,7 +361,8 @@ def results( even when results have already been cached. Default: `True`. Returns: - list[AwsQuantumTask]: The results of all of the quantum tasks in the batch. + list[GateModelQuantumTaskResult | AnnealingQuantumTaskResult | PhotonicModelQuantumTaskResult | AnalogHamiltonianSimulationQuantumTaskResult]: The # noqa: E501 + results of all of the quantum tasks in the batch. `FAILED`, `CANCELLED`, or timed out quantum tasks will have a result of None """ if not self._results or not use_cached_value: @@ -369,7 +383,14 @@ def results( return self._results @staticmethod - def _retrieve_results(tasks: list[AwsQuantumTask], max_workers: int) -> list[AwsQuantumTask]: + def _retrieve_results( + tasks: list[AwsQuantumTask], max_workers: int + ) -> list[ + GateModelQuantumTaskResult + | AnnealingQuantumTaskResult + | PhotonicModelQuantumTaskResult + | AnalogHamiltonianSimulationQuantumTaskResult + ]: with ThreadPoolExecutor(max_workers=max_workers) as executor: result_futures = [executor.submit(task.result) for task in tasks] return [future.result() for future in result_futures] diff --git a/src/braket/circuits/braket_program_context.py b/src/braket/circuits/braket_program_context.py index 4371637d3..558692510 100644 --- a/src/braket/circuits/braket_program_context.py +++ b/src/braket/circuits/braket_program_context.py @@ -11,6 +11,7 @@ # ANY KIND, either express or implied. See the License for the specific # language governing permissions and limitations under the License. +from collections.abc import Iterable from typing import Optional, Union import numpy as np @@ -161,16 +162,17 @@ def handle_parameter_value( return FreeParameterExpression(evaluated_value) return value - def add_measure(self, target: tuple[int]) -> None: + def add_measure( + self, target: tuple[int], classical_targets: Optional[Iterable[int]] = None + ) -> None: """Add a measure instruction to the circuit Args: target (tuple[int]): the target qubits to be measured. + classical_targets (Optional[Iterable[int]]): the classical registers + to use in the qubit measurement. """ - for index, qubit in enumerate(target): + for iter, qubit in enumerate(target): + index = classical_targets[iter] if classical_targets else iter instruction = Instruction(Measure(index=index), qubit) self._circuit.add_instruction(instruction) - if self._circuit._measure_targets: - self._circuit._measure_targets.append(qubit) - else: - self._circuit._measure_targets = [qubit] diff --git a/src/braket/circuits/circuit.py b/src/braket/circuits/circuit.py index c19bb0a06..e3181a603 100644 --- a/src/braket/circuits/circuit.py +++ b/src/braket/circuits/circuit.py @@ -319,8 +319,8 @@ def add_result_type( observable = Circuit._extract_observable(result_type_to_add) # We can skip this for now for AdjointGradient (the only subtype of this # type) because AdjointGradient can only be used when `shots=0`, and the - # qubit_observable_mapping is used to generate basis rotation instrunctions - # and make sure the observables are simultaneously commuting for `shots>0` mode. + # qubit_observable_mapping is used to generate basis rotation instructions + # and make sure the observables mutually commute for `shots>0` mode. supports_basis_rotation_instructions = not isinstance( result_type_to_add, ObservableParameterResultType ) @@ -504,6 +504,11 @@ def add_instruction( # Check if there is a measure instruction on the circuit self._check_if_qubit_measured(instruction, target, target_mapping) + # Update measure targets if instruction is a measurement + if isinstance(instruction.operator, Measure): + measure_target = target or instruction.target[0] + self._measure_targets = (self._measure_targets or []) + [measure_target] + if not target_mapping and not target: # Nothing has been supplied, add instruction instructions_to_add = [instruction] @@ -710,10 +715,6 @@ def _add_measure(self, target_qubits: QubitSetInput) -> None: target=target, ) ) - if self._measure_targets: - self._measure_targets.append(target) - else: - self._measure_targets = [target] def measure(self, target_qubits: QubitSetInput) -> Circuit: """ diff --git a/src/braket/circuits/observable.py b/src/braket/circuits/observable.py index d3f3fc862..e0572e3d9 100644 --- a/src/braket/circuits/observable.py +++ b/src/braket/circuits/observable.py @@ -16,7 +16,6 @@ import numbers from collections.abc import Sequence from copy import deepcopy -from typing import Union import numpy as np @@ -27,7 +26,7 @@ OpenQASMSerializationProperties, SerializationProperties, ) -from braket.registers.qubit_set import QubitSet +from braket.registers import QubitInput, QubitSet, QubitSetInput class Observable(QuantumOperator): @@ -37,23 +36,34 @@ class Observable(QuantumOperator): `ResultType.Expectation` to specify the measurement basis. """ - def __init__(self, qubit_count: int, ascii_symbols: Sequence[str]): + def __init__( + self, qubit_count: int, ascii_symbols: Sequence[str], targets: QubitSetInput | None = None + ): super().__init__(qubit_count=qubit_count, ascii_symbols=ascii_symbols) + targets = QubitSet(targets) + if targets: + if (num_targets := len(targets)) != qubit_count: + raise ValueError( + f"Length of target {num_targets} does not match qubit count {qubit_count}" + ) + self._targets = targets self._coef = 1 def _unscaled(self) -> Observable: - return Observable(qubit_count=self.qubit_count, ascii_symbols=self.ascii_symbols) + return Observable( + qubit_count=self.qubit_count, ascii_symbols=self.ascii_symbols, targets=self._targets + ) def to_ir( self, - target: QubitSet | None = None, + target: QubitSetInput | None = None, ir_type: IRType = IRType.JAQCD, serialization_properties: SerializationProperties | None = None, - ) -> Union[str, list[Union[str, list[list[list[float]]]]]]: + ) -> str | list[str | list[list[list[float]]]]: """Returns the IR representation for the observable Args: - target (QubitSet | None): target qubit(s). Defaults to None. + target (QubitSetInput | None): target qubit(s). Defaults to None. ir_type(IRType) : The IRType to use for converting the result type object to its IR representation. Defaults to IRType.JAQCD. serialization_properties (SerializationProperties | None): The serialization properties @@ -61,7 +71,7 @@ def to_ir( properties supplied must correspond to the supplied `ir_type`. Defaults to None. Returns: - Union[str, list[Union[str, list[list[list[float]]]]]]: The IR representation for + str | list[str | list[list[list[float]]]]: The IR representation for the observable. Raises: @@ -84,27 +94,35 @@ def to_ir( else: raise ValueError(f"Supplied ir_type {ir_type} is not supported.") - def _to_jaqcd(self) -> list[Union[str, list[list[list[float]]]]]: + def _to_jaqcd(self) -> list[str | list[list[list[float]]]]: """Returns the JAQCD representation of the observable.""" raise NotImplementedError("to_jaqcd has not been implemented yet.") def _to_openqasm( self, serialization_properties: OpenQASMSerializationProperties, - target: QubitSet | None = None, + targets: QubitSetInput | None = None, ) -> str: """Returns the openqasm string representation of the result type. Args: serialization_properties (OpenQASMSerializationProperties): The serialization properties to use while serializing the object to the IR representation. - target (QubitSet | None): target qubit(s). Defaults to None. + targets (QubitSetInput | None): target qubit(s). Defaults to None. Returns: str: Representing the openqasm representation of the result type. """ raise NotImplementedError("to_openqasm has not been implemented yet.") + @property + def targets(self) -> QubitSet | None: + """QubitSet | None: The target qubits of this observable + + If `None`, this is provided by the enclosing result type. + """ + return self._targets + @property def coefficient(self) -> int: """The coefficient of the observable. @@ -185,7 +203,11 @@ def __sub__(self, other: Observable): return self + (-1 * other) def __repr__(self) -> str: - return f"{self.name}('qubit_count': {self.qubit_count})" + return ( + f"{self.name}('qubit_count': {self._qubit_count})" + if not self._targets + else f"{self.name}('qubit_count': {self._qubit_count}, 'target': {self._targets})" + ) def __eq__(self, other: Observable) -> bool: if isinstance(other, Observable): @@ -198,8 +220,12 @@ class StandardObservable(Observable): eigenvalues of (+1, -1). """ - def __init__(self, ascii_symbols: Sequence[str]): - super().__init__(qubit_count=1, ascii_symbols=ascii_symbols) + def __init__(self, ascii_symbols: Sequence[str], target: QubitInput | None = None): + super().__init__( + qubit_count=1, + ascii_symbols=ascii_symbols, + targets=[target] if target is not None else None, + ) self._eigenvalues = (1.0, -1.0) # immutable def _unscaled(self) -> StandardObservable: diff --git a/src/braket/circuits/observables.py b/src/braket/circuits/observables.py index ae4f36a09..8249d156f 100644 --- a/src/braket/circuits/observables.py +++ b/src/braket/circuits/observables.py @@ -30,20 +30,28 @@ verify_quantum_operator_matrix_dimensions, ) from braket.circuits.serialization import IRType, OpenQASMSerializationProperties -from braket.registers.qubit_set import QubitSet +from braket.registers import QubitInput, QubitSet, QubitSetInput class H(StandardObservable): """Hadamard operation as an observable.""" - def __init__(self): - """Examples: - >>> Observable.H() + def __init__(self, target: QubitInput | None = None): + """Initializes Hadamard observable. + + Args: + target (QubitInput | None): The target qubit to measure the observable on. + If not provided, this needs to be provided from the enclosing result type. + Default: `None`. + + Examples: + >>> observables.H(0) + >>> observables.H() """ - super().__init__(ascii_symbols=["H"]) + super().__init__(ascii_symbols=["H"], target=target) def _unscaled(self) -> StandardObservable: - return H() + return H(self._targets) def _to_jaqcd(self) -> list[str]: if self.coefficient != 1: @@ -54,11 +62,11 @@ def _to_openqasm( self, serialization_properties: OpenQASMSerializationProperties, target: QubitSet = None ) -> str: coef_prefix = f"{self.coefficient} * " if self.coefficient != 1 else "" - if target: - qubit_target = serialization_properties.format_target(int(target[0])) - return f"{coef_prefix}h({qubit_target})" - else: - return f"{coef_prefix}h all" + targets = target or self._targets + qubit_target = int(targets[0]) if targets else None + if qubit_target is not None: + return f"{coef_prefix}h({serialization_properties.format_target(qubit_target)})" + return f"{coef_prefix}h all" def to_matrix(self) -> np.ndarray: return self.coefficient * ( @@ -76,14 +84,22 @@ def basis_rotation_gates(self) -> tuple[Gate, ...]: class I(Observable): # noqa: E742 """Identity operation as an observable.""" - def __init__(self): - """Examples: - >>> Observable.I() + def __init__(self, target: QubitInput | None = None): + """Initializes Identity observable. + + Args: + target (QubitInput | None): The target qubit to measure the observable on. + If not provided, this needs to be provided from the enclosing result type. + Default: `None`. + + Examples: + >>> observables.I(0) + >>> observables.I() """ - super().__init__(qubit_count=1, ascii_symbols=["I"]) + super().__init__(qubit_count=1, ascii_symbols=["I"], targets=target) def _unscaled(self) -> Observable: - return I() + return I(self._targets) def _to_jaqcd(self) -> list[str]: if self.coefficient != 1: @@ -94,11 +110,11 @@ def _to_openqasm( self, serialization_properties: OpenQASMSerializationProperties, target: QubitSet = None ) -> str: coef_prefix = f"{self.coefficient} * " if self.coefficient != 1 else "" - if target: - qubit_target = serialization_properties.format_target(int(target[0])) - return f"{coef_prefix}i({qubit_target})" - else: - return f"{coef_prefix}i all" + targets = target or self._targets + qubit_target = int(targets[0]) if targets else None + if qubit_target is not None: + return f"{coef_prefix}i({serialization_properties.format_target(qubit_target)})" + return f"{coef_prefix}i all" def to_matrix(self) -> np.ndarray: return self.coefficient * np.eye(2, dtype=complex) @@ -126,14 +142,22 @@ def eigenvalue(self, index: int) -> float: class X(StandardObservable): """Pauli-X operation as an observable.""" - def __init__(self): - """Examples: - >>> Observable.X() + def __init__(self, target: QubitInput | None = None): + """Initializes Pauli-X observable. + + Args: + target (QubitInput | None): The target qubit to measure the observable on. + If not provided, this needs to be provided from the enclosing result type. + Default: `None`. + + Examples: + >>> observables.X(0) + >>> observables.X() """ - super().__init__(ascii_symbols=["X"]) + super().__init__(ascii_symbols=["X"], target=target) def _unscaled(self) -> StandardObservable: - return X() + return X(self._targets) def _to_jaqcd(self) -> list[str]: if self.coefficient != 1: @@ -144,11 +168,11 @@ def _to_openqasm( self, serialization_properties: OpenQASMSerializationProperties, target: QubitSet = None ) -> str: coef_prefix = f"{self.coefficient} * " if self.coefficient != 1 else "" - if target: - qubit_target = serialization_properties.format_target(int(target[0])) - return f"{coef_prefix}x({qubit_target})" - else: - return f"{coef_prefix}x all" + targets = target or self._targets + qubit_target = int(targets[0]) if targets else None + if qubit_target is not None: + return f"{coef_prefix}x({serialization_properties.format_target(qubit_target)})" + return f"{coef_prefix}x all" def to_matrix(self) -> np.ndarray: return self.coefficient * np.array([[0.0, 1.0], [1.0, 0.0]], dtype=complex) @@ -164,14 +188,22 @@ def basis_rotation_gates(self) -> tuple[Gate, ...]: class Y(StandardObservable): """Pauli-Y operation as an observable.""" - def __init__(self): - """Examples: - >>> Observable.Y() + def __init__(self, target: QubitInput | None = None): + """Initializes Pauli-Y observable. + + Args: + target (QubitInput | None): The target qubit to measure the observable on. + If not provided, this needs to be provided from the enclosing result type. + Default: `None`. + + Examples: + >>> observables.Y(0) + >>> observables.Y() """ - super().__init__(ascii_symbols=["Y"]) + super().__init__(ascii_symbols=["Y"], target=target) def _unscaled(self) -> StandardObservable: - return Y() + return Y(self._targets) def _to_jaqcd(self) -> list[str]: if self.coefficient != 1: @@ -182,11 +214,11 @@ def _to_openqasm( self, serialization_properties: OpenQASMSerializationProperties, target: QubitSet = None ) -> str: coef_prefix = f"{self.coefficient} * " if self.coefficient != 1 else "" - if target: - qubit_target = serialization_properties.format_target(int(target[0])) - return f"{coef_prefix}y({qubit_target})" - else: - return f"{coef_prefix}y all" + targets = target or self._targets + qubit_target = int(targets[0]) if targets else None + if qubit_target is not None: + return f"{coef_prefix}y({serialization_properties.format_target(qubit_target)})" + return f"{coef_prefix}y all" def to_matrix(self) -> np.ndarray: return self.coefficient * np.array([[0.0, -1.0j], [1.0j, 0.0]], dtype=complex) @@ -202,14 +234,22 @@ def basis_rotation_gates(self) -> tuple[Gate, ...]: class Z(StandardObservable): """Pauli-Z operation as an observable.""" - def __init__(self): - """Examples: - >>> Observable.Z() + def __init__(self, target: QubitInput | None = None): + """Initializes Pauli-Z observable. + + Args: + target (QubitInput | None): The target qubit to measure the observable on. + If not provided, this needs to be provided from the enclosing result type. + Default: `None`. + + Examples: + >>> observables.Z(0) + >>> observables.Z() """ - super().__init__(ascii_symbols=["Z"]) + super().__init__(ascii_symbols=["Z"], target=target) def _unscaled(self) -> StandardObservable: - return Z() + return Z(self._targets) def _to_jaqcd(self) -> list[str]: if self.coefficient != 1: @@ -220,11 +260,11 @@ def _to_openqasm( self, serialization_properties: OpenQASMSerializationProperties, target: QubitSet = None ) -> str: coef_prefix = f"{self.coefficient} * " if self.coefficient != 1 else "" - if target: - qubit_target = serialization_properties.format_target(int(target[0])) - return f"{coef_prefix}z({qubit_target})" - else: - return f"{coef_prefix}z all" + targets = target or self._targets + qubit_target = int(targets[0]) if targets else None + if qubit_target is not None: + return f"{coef_prefix}z({serialization_properties.format_target(qubit_target)})" + return f"{coef_prefix}z all" def to_matrix(self) -> np.ndarray: return self.coefficient * np.array([[1.0, 0.0], [0.0, -1.0]], dtype=complex) @@ -247,13 +287,13 @@ def __init__(self, observables: list[Observable]): observables (list[Observable]): List of observables for tensor product Examples: - >>> t1 = Observable.Y() @ Observable.X() + >>> t1 = Observable.Y(0) @ Observable.X(1) >>> t1.to_matrix() array([[0.+0.j, 0.+0.j, 0.-0.j, 0.-1.j], [0.+0.j, 0.+0.j, 0.-1.j, 0.-0.j], [0.+0.j, 0.+1.j, 0.+0.j, 0.+0.j], [0.+1.j, 0.+0.j, 0.+0.j, 0.+0.j]]) - >>> t2 = Observable.Z() @ t1 + >>> t2 = Observable.Z(3) @ t1 >>> t2.factors (Z('qubit_count': 1), Y('qubit_count': 1), X('qubit_count': 1)) @@ -281,7 +321,22 @@ def __init__(self, observables: list[Observable]): f"{coefficient if coefficient != 1 else ''}" f"{'@'.join([obs.ascii_symbols[0] for obs in unscaled_factors])}" ) - super().__init__(qubit_count=qubit_count, ascii_symbols=[display_name] * qubit_count) + all_targets = [factor.targets for factor in unscaled_factors] + if not any(all_targets): + merged_targets = QubitSet() + elif all(all_targets): + flat_targets = [qubit for target in all_targets for qubit in target] + merged_targets = QubitSet(flat_targets) + if len(merged_targets) != len(flat_targets): + raise ValueError("Cannot have repeated target qubits") + else: + raise ValueError("Cannot mix factors with and without targets") + + super().__init__( + qubit_count=qubit_count, + ascii_symbols=[display_name] * qubit_count, + targets=merged_targets, + ) self._coef = coefficient self._factors = unscaled_factors self._factor_dimensions = tuple( @@ -316,7 +371,7 @@ def _to_openqasm( ) -> str: coef_prefix = f"{self.coefficient} * " if self.coefficient != 1 else "" factors = [] - use_qubits = iter(target) + use_qubits = iter(target or self._targets) for obs in self._factors: obs_target = QubitSet() num_qubits = int(np.log2(obs.to_matrix().shape[0])) @@ -438,7 +493,7 @@ def __init__(self, observables: list[Observable], display_name: str = "Hamiltoni observable for circuit diagrams. Defaults to `Hamiltonian`. Examples: - >>> t1 = -3 * Observable.Y() + 2 * Observable.X() + >>> t1 = -3 * Observable.Y(0) + 2 * Observable.X(0) Sum(X('qubit_count': 1), Y('qubit_count': 1)) >>> t1.summands (X('qubit_count': 1), Y('qubit_count': 1)) @@ -452,7 +507,15 @@ def __init__(self, observables: list[Observable], display_name: str = "Hamiltoni self._summands = tuple(flattened_observables) qubit_count = max(flattened_observables, key=lambda obs: obs.qubit_count).qubit_count + all_targets = [observable.targets for observable in flattened_observables] + if not any(all_targets): + targets = QubitSet() + elif all(all_targets): + targets = all_targets + else: + raise ValueError("Cannot mix terms with and without targets") super().__init__(qubit_count=qubit_count, ascii_symbols=[display_name] * qubit_count) + self._targets = targets def __mul__(self, other: numbers.Number) -> Observable: """Scalar multiplication""" @@ -469,8 +532,9 @@ def _to_jaqcd(self) -> list[str]: def _to_openqasm( self, serialization_properties: OpenQASMSerializationProperties, - target: list[QubitSet] = None, + target: list[QubitSetInput] = None, ) -> str: + target = target or self._targets if len(self.summands) != len(target): raise ValueError( f"Invalid target of length {len(target)} for Sum with {len(self.summands)} terms" @@ -529,21 +593,30 @@ class Hermitian(Observable): # Cache of eigenpairs _eigenpairs: ClassVar = {} - def __init__(self, matrix: np.ndarray, display_name: str = "Hermitian"): + def __init__( + self, + matrix: np.ndarray, + display_name: str = "Hermitian", + targets: QubitSetInput | None = None, + ): """Inits a `Hermitian`. Args: matrix (np.ndarray): Hermitian matrix that defines the observable. display_name (str): Name to use for an instance of this Hermitian matrix observable for circuit diagrams. Defaults to `Hermitian`. + targets (QubitSetInput | None): The target qubits to measure the observable on. + If not provided, this needs to be provided from the enclosing result type. + Default: `None`. Raises: ValueError: If `matrix` is not a two-dimensional square matrix, - or has a dimension length that is not a positive power of 2, - or is not Hermitian. + is not Hermitian, or has a dimension length that is either not a positive power of 2 + or, if targets is supplied, doesn't match the size of targets. Examples: - >>> Observable.Hermitian(matrix=np.array([[0, 1],[1, 0]])) + >>> observables.Hermitian(matrix=np.array([[0, 1],[1, 0]]), targets=[0]) + >>> observables.Hermitian(matrix=np.array([[0, 1],[1, 0]])) """ verify_quantum_operator_matrix_dimensions(matrix) self._matrix = np.array(matrix, dtype=complex) @@ -557,10 +630,14 @@ def __init__(self, matrix: np.ndarray, display_name: str = "Hermitian"): Gate.Unitary(matrix=eigendecomposition["eigenvectors"].conj().T), ) - super().__init__(qubit_count=qubit_count, ascii_symbols=[display_name] * qubit_count) + super().__init__( + qubit_count=qubit_count, ascii_symbols=[display_name] * qubit_count, targets=targets + ) def _unscaled(self) -> Observable: - return Hermitian(matrix=self._matrix, display_name=self.ascii_symbols[0]) + return Hermitian( + matrix=self._matrix, display_name=self.ascii_symbols[0], targets=self._targets + ) def _to_jaqcd(self) -> list[list[list[list[float]]]]: if self.coefficient != 1: @@ -573,6 +650,7 @@ def _to_openqasm( self, serialization_properties: OpenQASMSerializationProperties, target: QubitSet = None ) -> str: coef_prefix = f"{self.coefficient} * " if self.coefficient != 1 else "" + target = target or self._targets if target: qubit_target = ", ".join( [serialization_properties.format_target(int(t)) for t in target] diff --git a/src/braket/circuits/result_type.py b/src/braket/circuits/result_type.py index 3e9f0dfad..8343429e0 100644 --- a/src/braket/circuits/result_type.py +++ b/src/braket/circuits/result_type.py @@ -209,32 +209,32 @@ def __init__( super().__init__(ascii_symbols) self._observable = observable self._target = QubitSet(target) - if not self._target: - if self._observable.qubit_count != 1: - raise ValueError( - f"Observable {self._observable} must only operate on 1 qubit for target=None" - ) - elif isinstance(observable, Sum): # nested target - if len(target) != len(observable.summands): - raise ValueError( - "Sum observable's target shape must be a nested list where each term's " - "target length is equal to the observable term's qubits count." - ) - self._target = [QubitSet(term_target) for term_target in target] - for term_target, obs in zip(self._target, observable.summands): - if obs.qubit_count != len(term_target): + if self._target: + if isinstance(observable, Sum): # nested target + if len(target) != len(observable.summands): raise ValueError( "Sum observable's target shape must be a nested list where each term's " "target length is equal to the observable term's qubits count." ) - elif self._observable.qubit_count != len(self._target): - raise ValueError( - f"Observable's qubit count {self._observable.qubit_count} and " - f"the size of the target qubit set {self._target} must be equal" - ) - elif self._observable.qubit_count != len(self.ascii_symbols): + self._target = [QubitSet(term_target) for term_target in target] + for term_target, obs in zip(self._target, observable.summands): + if obs.qubit_count != len(term_target): + raise ValueError( + "Sum observable's target shape must be a nested list where each term's " + "target length is equal to the observable term's qubits count." + ) + elif self._observable.qubit_count != len(self._target): + raise ValueError( + f"Observable's qubit count {self._observable.qubit_count} and " + f"the size of the target qubit set {self._target} must be equal" + ) + elif self._observable.qubit_count != len(self.ascii_symbols): + raise ValueError( + "Observable's qubit count and the number of ASCII symbols must be equal" + ) + elif (not self._observable.targets) and self._observable.qubit_count != 1: raise ValueError( - "Observable's qubit count and the number of ASCII symbols must be equal" + f"Observable {self._observable} must only operate on 1 qubit for target=None" ) @property @@ -243,7 +243,7 @@ def observable(self) -> Observable: @property def target(self) -> QubitSet: - return self._target + return self._target or self._observable.targets @target.setter def target(self, target: QubitSetInput) -> None: diff --git a/src/braket/circuits/result_types.py b/src/braket/circuits/result_types.py index 325fa8f46..6c1232b06 100644 --- a/src/braket/circuits/result_types.py +++ b/src/braket/circuits/result_types.py @@ -14,14 +14,12 @@ from __future__ import annotations import re -from functools import reduce from typing import Union import braket.ir.jaqcd as ir from braket.circuits import circuit from braket.circuits.free_parameter import FreeParameter from braket.circuits.observable import Observable -from braket.circuits.observables import Sum from braket.circuits.result_type import ( ObservableParameterResultType, ObservableResultType, @@ -96,7 +94,7 @@ def __init__(self, target: QubitSetInput | None = None): full density matrix is returned. Examples: - >>> ResultType.DensityMatrix(target=[0, 1]) + >>> result_types.DensityMatrix(target=[0, 1]) """ self._target = QubitSet(target) ascii_symbols = ["DensityMatrix"] * len(self._target) if self._target else ["DensityMatrix"] @@ -198,22 +196,19 @@ def __init__( Examples: - >>> ResultType.AdjointGradient(observable=Observable.Z(), + >>> result_types.AdjointGradient(observable=observables.Z(0), + parameters=["alpha", "beta"]) + >>> result_types.AdjointGradient(observable=observables.Z(), target=0, parameters=["alpha", "beta"]) - >>> tensor_product = Observable.Y() @ Observable.Z() - >>> hamiltonian = Observable.Y() @ Observable.Z() + Observable.H() - >>> ResultType.AdjointGradient( + >>> tensor_product = observables.Y(0) @ observables.Z(1) + >>> hamiltonian = observables.Y(0) @ observables.Z(1) + observables.H(0) + >>> result_types.AdjointGradient( >>> observable=tensor_product, - >>> target=[[0, 1], [2]], >>> parameters=["alpha", "beta"], >>> ) """ - if isinstance(observable, Sum): - target_qubits = reduce(QubitSet.union, map(QubitSet, target), QubitSet()) - else: - target_qubits = QubitSet(target) - + target_qubits = QubitSet(target if target is not None else observable.targets) super().__init__( ascii_symbols=[f"AdjointGradient({observable.ascii_symbols[0]})"] * len(target_qubits), observable=observable, @@ -223,7 +218,7 @@ def __init__( def _to_openqasm(self, serialization_properties: OpenQASMSerializationProperties) -> str: observable_ir = self.observable.to_ir( - target=self.target, + target=self._target, ir_type=IRType.OPENQASM, serialization_properties=serialization_properties, ) @@ -261,7 +256,7 @@ def adjoint_gradient( Examples: >>> alpha, beta = FreeParameter('alpha'), FreeParameter('beta') >>> circ = Circuit().h(0).h(1).rx(0, alpha).yy(0, 1, beta).adjoint_gradient( - >>> observable=Observable.Z(), target=[0], parameters=[alpha, beta] + >>> observable=observables.Z(0), parameters=[alpha, beta] >>> ) """ return ResultType.AdjointGradient( @@ -288,7 +283,7 @@ def __init__(self, state: list[str]): state is not a list of strings of '0' and '1' Examples: - >>> ResultType.Amplitude(state=['01', '10']) + >>> result_types.Amplitude(state=['01', '10']) """ if ( not state @@ -367,7 +362,7 @@ def __init__(self, target: QubitSetInput | None = None): circuit. Examples: - >>> ResultType.Probability(target=[0, 1]) + >>> result_types.Probability(target=[0, 1]) """ self._target = QubitSet(target) ascii_symbols = ["Probability"] * len(self._target) if self._target else ["Probability"] @@ -453,16 +448,18 @@ def __init__(self, observable: Observable, target: QubitSetInput | None = None): Args: observable (Observable): the observable for the result type - target (QubitSetInput | None): Target qubits that the - result type is requested for. Default is `None`, which means the observable must - operate only on 1 qubit and it is applied to all qubits in parallel. - + target (QubitSetInput | None): Target qubits that the result type is requested for. + If not provided, the observable's target will be used instead. If neither exist, + then it is applied to all qubits in parallel; in this case the observable must + operate only on 1 qubit. + Default: `None`. Examples: - >>> ResultType.Expectation(observable=Observable.Z(), target=0) + >>> result_types.Expectation(observable=observables.Z(0)) + >>> result_types.Expectation(observable=observables.Z(), target=0) - >>> tensor_product = Observable.Y() @ Observable.Z() - >>> ResultType.Expectation(observable=tensor_product, target=[0, 1]) + >>> tensor_product = observables.Y(0) @ observables.Z(1) + >>> result_types.Expectation(observable=tensor_product) """ super().__init__( ascii_symbols=[f"Expectation({obs_ascii})" for obs_ascii in observable.ascii_symbols], @@ -480,7 +477,7 @@ def _to_jaqcd(self) -> ir.Expectation: def _to_openqasm(self, serialization_properties: OpenQASMSerializationProperties) -> str: observable_ir = self.observable.to_ir( - target=self.target, + target=self._target, ir_type=IRType.OPENQASM, serialization_properties=serialization_properties, ) @@ -501,7 +498,7 @@ def expectation(observable: Observable, target: QubitSetInput | None = None) -> ResultType: expectation as a requested result type Examples: - >>> circ = Circuit().expectation(observable=Observable.Z(), target=0) + >>> circ = Circuit().expectation(observable=observables.Z(0)) """ return ResultType.Expectation(observable=observable, target=target) @@ -526,15 +523,18 @@ def __init__(self, observable: Observable, target: QubitSetInput | None = None): Args: observable (Observable): the observable for the result type - target (QubitSetInput | None): Target qubits that the - result type is requested for. Default is `None`, which means the observable must - operate only on 1 qubit and it is applied to all qubits in parallel. + target (QubitSetInput | None): Target qubits that the result type is requested for. + If not provided, the observable's target will be used instead. If neither exist, + then it is applied to all qubits in parallel; in this case the observable must + operate only on 1 qubit. + Default: `None`. Examples: - >>> ResultType.Sample(observable=Observable.Z(), target=0) + >>> result_types.Sample(observable=observables.Z(0)) + >>> result_types.Sample(observable=observables.Z(), target=0) - >>> tensor_product = Observable.Y() @ Observable.Z() - >>> ResultType.Sample(observable=tensor_product, target=[0, 1]) + >>> tensor_product = observables.Y(0) @ observables.Z(1) + >>> result_types.Sample(observable=tensor_product) """ super().__init__( ascii_symbols=[f"Sample({obs_ascii})" for obs_ascii in observable.ascii_symbols], @@ -552,7 +552,7 @@ def _to_jaqcd(self) -> ir.Sample: def _to_openqasm(self, serialization_properties: OpenQASMSerializationProperties) -> str: observable_ir = self.observable.to_ir( - target=self.target, + target=self._target, ir_type=IRType.OPENQASM, serialization_properties=serialization_properties, ) @@ -573,7 +573,7 @@ def sample(observable: Observable, target: QubitSetInput | None = None) -> Resul ResultType: sample as a requested result type Examples: - >>> circ = Circuit().sample(observable=Observable.Z(), target=0) + >>> circ = Circuit().sample(observable=observables.Z(0)) """ return ResultType.Sample(observable=observable, target=target) @@ -599,19 +599,22 @@ def __init__(self, observable: Observable, target: QubitSetInput | None = None): Args: observable (Observable): the observable for the result type - target (QubitSetInput | None): Target qubits that the - result type is requested for. Default is `None`, which means the observable must - operate only on 1 qubit and it is applied to all qubits in parallel. + target (QubitSetInput | None): Target qubits that the result type is requested for. + If not provided, the observable's target will be used instead. If neither exist, + then it is applied to all qubits in parallel; in this case the observable must + operate only on 1 qubit. + Default: `None`. Raises: ValueError: If the observable's qubit count does not equal the number of target qubits, or if `target=None` and the observable's qubit count is not 1. Examples: - >>> ResultType.Variance(observable=Observable.Z(), target=0) + >>> result_types.Variance(observable=observables.Z(0)) + >>> result_types.Variance(observable=observables.Z(), target=0) - >>> tensor_product = Observable.Y() @ Observable.Z() - >>> ResultType.Variance(observable=tensor_product, target=[0, 1]) + >>> tensor_product = observables.Y(0) @ observables.Z(1) + >>> result_types.Variance(observable=tensor_product) """ super().__init__( ascii_symbols=[f"Variance({obs_ascii})" for obs_ascii in observable.ascii_symbols], @@ -629,7 +632,7 @@ def _to_jaqcd(self) -> ir.Variance: def _to_openqasm(self, serialization_properties: OpenQASMSerializationProperties) -> str: observable_ir = self.observable.to_ir( - target=self.target, + target=self._target, ir_type=IRType.OPENQASM, serialization_properties=serialization_properties, ) @@ -650,7 +653,7 @@ def variance(observable: Observable, target: QubitSetInput | None = None) -> Res ResultType: variance as a requested result type Examples: - >>> circ = Circuit().variance(observable=Observable.Z(), target=0) + >>> circ = Circuit().variance(observable=observables.Z(0)) """ return ResultType.Variance(observable=observable, target=target) diff --git a/src/braket/devices/device.py b/src/braket/devices/device.py index dbc7b6b35..af3467a40 100644 --- a/src/braket/devices/device.py +++ b/src/braket/devices/device.py @@ -131,7 +131,7 @@ def _validate_device_noise_model_support(self, noise_model: NoiseModel) -> None: def _apply_noise_model_to_circuit( self, task_specification: Union[Circuit, Problem, Program, AnalogHamiltonianSimulation] - ) -> None: + ) -> Union[Circuit, Problem, Program, AnalogHamiltonianSimulation]: if isinstance(task_specification, Circuit): for instruction in task_specification.instructions: if isinstance(instruction.operator, Noise): diff --git a/src/braket/devices/devices.py b/src/braket/devices/devices.py index fa2c6d025..44d6edb13 100644 --- a/src/braket/devices/devices.py +++ b/src/braket/devices/devices.py @@ -31,13 +31,13 @@ class _IQM(str, Enum): Garnet = "arn:aws:braket:eu-north-1::device/qpu/iqm/Garnet" class _IonQ(str, Enum): - Harmony = "arn:aws:braket:us-east-1::device/qpu/ionq/Harmony" + _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" + _Lucy = "arn:aws:braket:eu-west-2::device/qpu/oqc/Lucy" class _QuEra(str, Enum): Aquila = "arn:aws:braket:us-east-1::device/qpu/quera/Aquila" @@ -49,7 +49,8 @@ class _Rigetti(str, Enum): _Aspen11 = "arn:aws:braket:::device/qpu/rigetti/Aspen-11" _AspenM1 = "arn:aws:braket:us-west-1::device/qpu/rigetti/Aspen-M-1" _AspenM2 = "arn:aws:braket:us-west-1::device/qpu/rigetti/Aspen-M-2" - AspenM3 = "arn:aws:braket:us-west-1::device/qpu/rigetti/Aspen-M-3" + _AspenM3 = "arn:aws:braket:us-west-1::device/qpu/rigetti/Aspen-M-3" + Ankaa2 = "arn:aws:braket:us-west-1::device/qpu/rigetti/Ankaa-2" class _Xanadu(str, Enum): _Borealis = "arn:aws:braket:us-east-1::device/qpu/xanadu/Borealis" @@ -58,7 +59,7 @@ class _Xanadu(str, Enum): # DWave = _DWave IonQ = _IonQ IQM = _IQM - OQC = _OQC + # OQC = _OQC QuEra = _QuEra Rigetti = _Rigetti # Xanadu = _Xanadu diff --git a/src/braket/devices/local_simulator.py b/src/braket/devices/local_simulator.py index 15ec904de..f9e70a1ea 100644 --- a/src/braket/devices/local_simulator.py +++ b/src/braket/devices/local_simulator.py @@ -16,7 +16,6 @@ import sys from functools import singledispatchmethod from itertools import repeat -from multiprocessing import Pool from os import cpu_count from typing import Any, Optional, Union @@ -31,6 +30,11 @@ from braket.ir.ahs import Program as AHSProgram from braket.ir.openqasm import Program as OpenQASMProgram from braket.simulator import BraketSimulator +from braket.task_result import ( + AnalogHamiltonianSimulationTaskResult, + AnnealingTaskResult, + GateModelTaskResult, +) from braket.tasks import AnnealingQuantumTaskResult, GateModelQuantumTaskResult from braket.tasks.analog_hamiltonian_simulation_quantum_task_result import ( AnalogHamiltonianSimulationQuantumTaskResult, @@ -118,8 +122,9 @@ def run( """ if self._noise_model: task_specification = self._apply_noise_model_to_circuit(task_specification) - result = self._run_internal(task_specification, shots, inputs=inputs, *args, **kwargs) - return LocalQuantumTask(result) + payload = self._construct_payload(task_specification, inputs, shots) + result = self._delegate.run(payload, *args, shots=shots, **kwargs) + return LocalQuantumTask(self._to_result_object(result)) def run_batch( # noqa: C901 self, @@ -151,7 +156,7 @@ def run_batch( # noqa: C901 shots (Optional[int]): The number of times to run the quantum task. Default: 0. max_parallel (Optional[int]): The maximum number of quantum tasks to run in parallel. Default - is the number of CPU. + is the number of logical CPUs. inputs (Optional[Union[dict[str, float], list[dict[str, float]]]]): Inputs to be passed along with the IR. If the IR supports inputs, the inputs will be updated with this value. Default: {}. @@ -195,6 +200,7 @@ def run_batch( # noqa: C901 else: tasks_and_inputs = list(tasks_and_inputs) + payloads = [] for task_specification, input_map in tasks_and_inputs: if isinstance(task_specification, Circuit): param_names = {param.name for param in task_specification.parameters} @@ -203,12 +209,12 @@ def run_batch( # noqa: C901 f"Cannot execute circuit with unbound parameters: " f"{unbounded_parameters}" ) + payloads.append(self._construct_payload(task_specification, input_map, shots)) - with Pool(min(max_parallel, len(tasks_and_inputs))) as pool: - param_list = [(task, shots, inp, *args, *kwargs) for task, inp in tasks_and_inputs] - results = pool.starmap(self._run_internal_wrap, param_list) - - return LocalQuantumTaskBatch(results) + results = self._delegate.run_multiple( + payloads, *args, shots=shots, max_parallel=max_parallel, **kwargs + ) + return LocalQuantumTaskBatch([self._to_result_object(result) for result in results]) @property def properties(self) -> DeviceCapabilities: @@ -230,21 +236,8 @@ def registered_backends() -> set[str]: """ return set(_simulator_devices.keys()) - def _run_internal_wrap( - self, - task_specification: Union[ - Circuit, Problem, OpenQASMProgram, AnalogHamiltonianSimulation, SerializableProgram - ], - shots: Optional[int] = None, - inputs: Optional[dict[str, float]] = None, - *args, - **kwargs, - ) -> Union[GateModelQuantumTaskResult, AnnealingQuantumTaskResult]: # pragma: no cover - """Wraps _run_interal for pickle dump""" - return self._run_internal(task_specification, shots, inputs=inputs, *args, **kwargs) - @singledispatchmethod - def _get_simulator(self, simulator: Union[str, BraketSimulator]) -> LocalSimulator: + def _get_simulator(self, simulator: Any) -> BraketSimulator: raise TypeError("Simulator must either be a string or a BraketSimulator instance") @_get_simulator.register @@ -261,66 +254,29 @@ def _(self, backend_impl: BraketSimulator): return backend_impl @singledispatchmethod - def _run_internal( + def _construct_payload( self, - task_specification: Union[ - Circuit, - Problem, - OpenQASMProgram, - AnalogHamiltonianSimulation, - AHSProgram, - SerializableProgram, - ], - shots: Optional[int] = None, - *args, - **kwargs, - ) -> Union[GateModelQuantumTaskResult, AnnealingQuantumTaskResult]: + task_specification: Any, + inputs: Optional[dict[str, float]], + shots: Optional[int], + ) -> Any: raise NotImplementedError(f"Unsupported task type {type(task_specification)}") - @_run_internal.register - def _( - self, - circuit: Circuit, - shots: Optional[int] = None, - inputs: Optional[dict[str, float]] = None, - *args, - **kwargs, - ): + @_construct_payload.register + def _(self, circuit: Circuit, inputs: Optional[dict[str, float]], shots: Optional[int]): simulator = self._delegate if DeviceActionType.OPENQASM in simulator.properties.action: validate_circuit_and_shots(circuit, shots) program = circuit.to_ir(ir_type=IRType.OPENQASM) program.inputs.update(inputs or {}) - results = simulator.run(program, shots, *args, **kwargs) - return GateModelQuantumTaskResult.from_object(results) + return program elif DeviceActionType.JAQCD in simulator.properties.action: validate_circuit_and_shots(circuit, shots) - program = circuit.to_ir(ir_type=IRType.JAQCD) - qubits = circuit.qubit_count - results = simulator.run(program, qubits, shots, *args, **kwargs) - return GateModelQuantumTaskResult.from_object(results) + return circuit.to_ir(ir_type=IRType.JAQCD) raise NotImplementedError(f"{type(simulator)} does not support qubit gate-based programs") - @_run_internal.register - def _(self, problem: Problem, shots: Optional[int] = None, *args, **kwargs): - simulator = self._delegate - if DeviceActionType.ANNEALING not in simulator.properties.action: - raise NotImplementedError( - f"{type(simulator)} does not support quantum annealing problems" - ) - ir = problem.to_ir() - results = simulator.run(ir, shots, *args, *kwargs) - return AnnealingQuantumTaskResult.from_object(results) - - @_run_internal.register - def _( - self, - program: OpenQASMProgram, - shots: Optional[int] = None, - inputs: Optional[dict[str, float]] = None, - *args, - **kwargs, - ): + @_construct_payload.register + def _(self, program: OpenQASMProgram, inputs: Optional[dict[str, float]], _shots): simulator = self._delegate if DeviceActionType.OPENQASM not in simulator.properties.action: raise NotImplementedError(f"{type(simulator)} does not support OpenQASM programs") @@ -331,54 +287,56 @@ def _( source=program.source, inputs=inputs_copy, ) + return program - results = simulator.run(program, shots, *args, **kwargs) - - if isinstance(results, GateModelQuantumTaskResult): - return results + @_construct_payload.register + def _(self, program: SerializableProgram, inputs: Optional[dict[str, float]], _shots): + inputs_copy = inputs.copy() if inputs is not None else {} + return OpenQASMProgram(source=program.to_ir(ir_type=IRType.OPENQASM), inputs=inputs_copy) - return GateModelQuantumTaskResult.from_object(results) - - @_run_internal.register - def _( - self, - program: SerializableProgram, - shots: Optional[int] = None, - inputs: Optional[dict[str, float]] = None, - *args, - **kwargs, - ): - program = OpenQASMProgram(source=program.to_ir(ir_type=IRType.OPENQASM)) - return self._run_internal(program, shots, inputs, *args, **kwargs) - - @_run_internal.register - def _( - self, - program: AnalogHamiltonianSimulation, - shots: Optional[int] = None, - *args, - **kwargs, - ): + @_construct_payload.register + def _(self, program: AnalogHamiltonianSimulation, _inputs, _shots): simulator = self._delegate if DeviceActionType.AHS not in simulator.properties.action: raise NotImplementedError( f"{type(simulator)} does not support analog Hamiltonian simulation programs" ) - results = simulator.run(program.to_ir(), shots, *args, **kwargs) - return AnalogHamiltonianSimulationQuantumTaskResult.from_object(results) + return program.to_ir() - @_run_internal.register - def _( - self, - program: AHSProgram, - shots: Optional[int] = None, - *args, - **kwargs, - ): + @_construct_payload.register + def _(self, program: AHSProgram, _inputs, _shots): simulator = self._delegate if DeviceActionType.AHS not in simulator.properties.action: raise NotImplementedError( f"{type(simulator)} does not support analog Hamiltonian simulation programs" ) - results = simulator.run(program, shots, *args, **kwargs) - return AnalogHamiltonianSimulationQuantumTaskResult.from_object(results) + return program + + @_construct_payload.register + def _(self, problem: Problem, _inputs, _shots): + simulator = self._delegate + if DeviceActionType.ANNEALING not in simulator.properties.action: + raise NotImplementedError( + f"{type(simulator)} does not support quantum annealing problems" + ) + return problem.to_ir() + + @singledispatchmethod + def _to_result_object(self, result: Any) -> Any: + raise NotImplementedError(f"Unsupported task result type {type(result)}") + + @_to_result_object.register + def _(self, result: GateModelQuantumTaskResult): + return result + + @_to_result_object.register + def _(self, result: GateModelTaskResult): + return GateModelQuantumTaskResult.from_object(result) + + @_to_result_object.register + def _(self, result: AnalogHamiltonianSimulationTaskResult): + return AnalogHamiltonianSimulationQuantumTaskResult.from_object(result) + + @_to_result_object.register + def _(self, result: AnnealingTaskResult): + return AnnealingQuantumTaskResult.from_object(result) diff --git a/src/braket/parametric/free_parameter.py b/src/braket/parametric/free_parameter.py index 1f3a69e72..8f437bbca 100644 --- a/src/braket/parametric/free_parameter.py +++ b/src/braket/parametric/free_parameter.py @@ -20,6 +20,69 @@ from braket.parametric.free_parameter_expression import FreeParameterExpression +PREDEFINED_VARIABLE_NAMES = {"b", "q"} + +# The reserved words are picked from below +# https://github.com/openqasm/openqasm/blob/main/source/grammar/qasm3Lexer.g4 +# https://github.com/openqasm/openpulse-python/blob/main/source/grammar/openpulseLexer.g4 +QASM_RESERVED_WORDS = { + "OPENQASM", + "include", + "defcalgrammar", + "def", + "cal", + "defcal", + "gate", + "extern", + "box", + "let", + "break", + "continue", + "if", + "else", + "end", + "return", + "for", + "while", + "in", + "pragma", + "input", + "output", + "const", + "readonly", + "mutable", + "qreg", + "qubit", + "creg", + "bool", + "bit", + "int", + "uint", + "float", + "angle", + "complex", + "array", + "void", + "duration", + "stretch", + "gphase", + "inv", + "pow", + "ctrl", + "negctrl", + "dim", + "durationof", + "delay", + "reset", + "measure", + "barrier", + "true", + "false", + "waveform", + "port", + "frame", +} + class FreeParameter(FreeParameterExpression): """Class 'FreeParameter' @@ -94,6 +157,16 @@ def _set_name(self, name: str) -> None: raise TypeError("FreeParameter names must be strings") if not name[0].isalpha() and name[0] != "_": raise ValueError("FreeParameter names must start with a letter or an underscore") + if name in PREDEFINED_VARIABLE_NAMES: + raise ValueError( + f"FreeParameter names must not be one of the Braket reserved variable names: " + f"{PREDEFINED_VARIABLE_NAMES}." + ) + if name in QASM_RESERVED_WORDS: + raise ValueError( + f"FreeParameter names must not be one of the OpenQASM or OpenPulse keywords: " + f"{QASM_RESERVED_WORDS}." + ) self._name = Symbol(name) def to_dict(self) -> dict: diff --git a/src/braket/pulse/__init__.py b/src/braket/pulse/__init__.py index 01ef66892..48f7022af 100644 --- a/src/braket/pulse/__init__.py +++ b/src/braket/pulse/__init__.py @@ -18,5 +18,6 @@ ArbitraryWaveform, ConstantWaveform, DragGaussianWaveform, + ErfSquareWaveform, GaussianWaveform, ) diff --git a/src/braket/pulse/ast/approximation_parser.py b/src/braket/pulse/ast/approximation_parser.py index d2dcf65e8..16c9c6726 100644 --- a/src/braket/pulse/ast/approximation_parser.py +++ b/src/braket/pulse/ast/approximation_parser.py @@ -26,6 +26,7 @@ from braket.pulse.waveforms import ( ConstantWaveform, DragGaussianWaveform, + ErfSquareWaveform, GaussianWaveform, Waveform, ) @@ -468,6 +469,20 @@ def set_scale(self, node: ast.FunctionCall, context: _ParseState) -> None: value = self.visit(node.arguments[1], context) context.frame_data[frame].scale = value + def swap_phases(self, node: ast.FunctionCall, context: _ParseState) -> None: + """A 'swap_phases' Function call. + + Args: + node (ast.FunctionCall): The function call node. + context (_ParseState): The parse state. + """ + frame1 = self.visit(node.arguments[0], context) + frame2 = self.visit(node.arguments[1], context) + phase1 = context.frame_data[frame1].phase + phase2 = context.frame_data[frame2].phase + context.frame_data[frame1].phase = phase2 + context.frame_data[frame2].phase = phase1 + def capture_v0(self, node: ast.FunctionCall, context: _ParseState) -> None: """A 'capture_v0' Function call. @@ -549,6 +564,19 @@ def drag_gaussian(self, node: ast.FunctionCall, context: _ParseState) -> Wavefor args = [self.visit(arg, context) for arg in node.arguments] return DragGaussianWaveform(*args) + def erf_square(self, node: ast.FunctionCall, context: _ParseState) -> Waveform: + """A 'erf_square' Waveform Function call. + + Args: + node (ast.FunctionCall): The function call node. + context (_ParseState): The parse state. + + Returns: + Waveform: The waveform object representing the function call. + """ + args = [self.visit(arg, context) for arg in node.arguments] + return ErfSquareWaveform(*args) + def _init_frame_data(frames: dict[str, Frame]) -> dict[str, _FrameState]: frame_states = { diff --git a/src/braket/pulse/pulse_sequence.py b/src/braket/pulse/pulse_sequence.py index 9d43127a0..6c5d60444 100644 --- a/src/braket/pulse/pulse_sequence.py +++ b/src/braket/pulse/pulse_sequence.py @@ -144,6 +144,26 @@ def shift_phase( self._frames[frame.id] = frame return self + def swap_phases( + self, + frame_1: Frame, + frame_2: Frame, + ) -> PulseSequence: + """Adds an instruction to swap the phases between two frames. + + Args: + frame_1 (Frame): First frame for which to swap the phase. + frame_2 (Frame): Second frame for which to swap the phase. + + Returns: + PulseSequence: self, with the instruction added. + """ + _validate_uniqueness(self._frames, [frame_1, frame_2]) + self._program.function_call("swap_phases", [frame_1, frame_2]) + self._frames[frame_1.id] = frame_1 + self._frames[frame_2.id] = frame_2 + return self + def set_scale( self, frame: Frame, scale: Union[float, FreeParameterExpression] ) -> PulseSequence: diff --git a/src/braket/pulse/waveforms.py b/src/braket/pulse/waveforms.py index 915d187a8..9da7bee4c 100644 --- a/src/braket/pulse/waveforms.py +++ b/src/braket/pulse/waveforms.py @@ -19,6 +19,7 @@ from typing import Optional, Union import numpy as np +import scipy as sp from oqpy import WaveformVar, bool_, complex128, declare_waveform_generator, duration, float64 from oqpy.base import OQPyExpression @@ -501,6 +502,176 @@ def _from_calibration_schema(waveform_json: dict) -> GaussianWaveform: return GaussianWaveform(**waveform_parameters) +class ErfSquareWaveform(Waveform, Parameterizable): + """A square waveform with smoothed edges.""" + + def __init__( + self, + length: Union[float, FreeParameterExpression], + width: Union[float, FreeParameterExpression], + sigma: Union[float, FreeParameterExpression], + off_center: Union[float, FreeParameterExpression] = 0, + amplitude: Union[float, FreeParameterExpression] = 1, + zero_at_edges: bool = False, + id: Optional[str] = None, + ): + r"""Initializes a `ErfSquareWaveform`. + + .. math:: (\text{step}((t-t_1)/sigma) + \text{step}(-(t-t_2)/sigma) - 1) + + where :math:`\text{step}(t)` is the rounded step function defined as + :math:`(erf(t)+1)/2` and :math:`t_1` and :math:`t_2` are the timestamps at the half + height. The waveform is scaled such that its maximum is equal to `amplitude`. + + Args: + length (Union[float, FreeParameterExpression]): Duration (in seconds) from the start + to the end of the waveform. + width (Union[float, FreeParameterExpression]): Duration (in seconds) between the + half height of the two edges. + sigma (Union[float, FreeParameterExpression]): A characteristic time of how quickly + the edges rise and fall. + off_center (Union[float, FreeParameterExpression]): Shift the smoothed square waveform + earlier or later in time. When positive, the smoothed square is shifted later + (to the right), otherwise earlier (to the left). Defaults to 0. + amplitude (Union[float, FreeParameterExpression]): The amplitude of the waveform + envelope. Defaults to 1. + zero_at_edges (bool): Whether the waveform is scaled such that it has zero value at the + edges. Defaults to False. + id (Optional[str]): The identifier used for declaring this waveform. A random string of + ascii characters is assigned by default. + """ + self.length = length + self.width = width + self.sigma = sigma + self.off_center = off_center + self.amplitude = amplitude + self.zero_at_edges = zero_at_edges + self.id = id or _make_identifier_name() + + def __repr__(self) -> str: + return ( + f"ErfSquareWaveform('id': {self.id}, 'length': {self.length}, " + f"'width': {self.width}, 'sigma': {self.sigma}, 'off_center': {self.off_center}, " + f"'amplitude': {self.amplitude}, 'zero_at_edges': {self.zero_at_edges})" + ) + + @property + def parameters(self) -> list[Union[FreeParameterExpression, FreeParameter, float]]: + """Returns the parameters associated with the object, either unbound free parameter + expressions or bound values. + """ + return [self.length, self.width, self.sigma, self.off_center, self.amplitude] + + def bind_values(self, **kwargs: Union[FreeParameter, str]) -> ErfSquareWaveform: + """Takes in parameters and returns an object with specified parameters + replaced with their values. + + Args: + **kwargs (Union[FreeParameter, str]): Arbitrary keyword arguments. + + Returns: + ErfSquareWaveform: A copy of this waveform with the requested parameters bound. + """ + constructor_kwargs = { + "length": subs_if_free_parameter(self.length, **kwargs), + "width": subs_if_free_parameter(self.width, **kwargs), + "sigma": subs_if_free_parameter(self.sigma, **kwargs), + "off_center": subs_if_free_parameter(self.off_center, **kwargs), + "amplitude": subs_if_free_parameter(self.amplitude, **kwargs), + "zero_at_edges": self.zero_at_edges, + "id": self.id, + } + return ErfSquareWaveform(**constructor_kwargs) + + def __eq__(self, other: ErfSquareWaveform): + return isinstance(other, ErfSquareWaveform) and ( + self.length, + self.width, + self.sigma, + self.off_center, + self.amplitude, + self.zero_at_edges, + self.id, + ) == ( + other.length, + other.width, + other.sigma, + other.off_center, + other.amplitude, + other.zero_at_edges, + other.id, + ) + + def _to_oqpy_expression(self) -> OQPyExpression: + """Returns an OQPyExpression defining this waveform. + + Returns: + OQPyExpression: The OQPyExpression. + """ + erf_square_generator = declare_waveform_generator( + "erf_square", + [ + ("length", duration), + ("width", duration), + ("sigma", duration), + ("off_center", duration), + ("amplitude", float64), + ("zero_at_edges", bool_), + ], + ) + return WaveformVar( + init_expression=erf_square_generator( + self.length, + self.width, + self.sigma, + self.off_center, + self.amplitude, + self.zero_at_edges, + ), + name=self.id, + ) + + def sample(self, dt: float) -> np.ndarray: + """Generates a sample of amplitudes for this Waveform based on the given time resolution. + + Args: + dt (float): The time resolution. + + Returns: + np.ndarray: The sample amplitudes for this waveform. + """ + sample_range = np.arange(0, self.length, dt) + t1 = (self.length - self.width) / 2 + self.off_center + t2 = (self.length + self.width) / 2 + self.off_center + samples = ( + sp.special.erf((sample_range - t1) / self.sigma) + + sp.special.erf(-(sample_range - t2) / self.sigma) + ) / 2 + + mid_waveform_height = sp.special.erf((self.width / 2) / self.sigma) + waveform_bottom = (sp.special.erf(-t1 / self.sigma) + sp.special.erf(t2 / self.sigma)) / 2 + + if self.zero_at_edges: + return ( + (samples - waveform_bottom) + / (mid_waveform_height - waveform_bottom) + * self.amplitude + ) + else: + return samples * self.amplitude / mid_waveform_height + + @staticmethod + def _from_calibration_schema(waveform_json: dict) -> ErfSquareWaveform: + waveform_parameters = {"id": waveform_json["waveformId"]} + for val in waveform_json["arguments"]: + waveform_parameters[val["name"]] = ( + float(val["value"]) + if val["type"] == "float" + else FreeParameterExpression(val["value"]) + ) + return ErfSquareWaveform(**waveform_parameters) + + def _make_identifier_name() -> str: return "".join([random.choice(string.ascii_letters) for _ in range(10)]) # noqa S311 @@ -511,6 +682,7 @@ def _parse_waveform_from_calibration_schema(waveform: dict) -> Waveform: "drag_gaussian": DragGaussianWaveform._from_calibration_schema, "gaussian": GaussianWaveform._from_calibration_schema, "constant": ConstantWaveform._from_calibration_schema, + "erf_square": ErfSquareWaveform._from_calibration_schema, } if "amplitudes" in waveform: waveform["name"] = "arbitrary" diff --git a/src/braket/tasks/gate_model_quantum_task_result.py b/src/braket/tasks/gate_model_quantum_task_result.py index 81f90ae7b..e8267032f 100644 --- a/src/braket/tasks/gate_model_quantum_task_result.py +++ b/src/braket/tasks/gate_model_quantum_task_result.py @@ -145,6 +145,8 @@ def get_compiled_circuit(self) -> Optional[str]: return metadata.rigettiMetadata.compiledProgram elif metadata.oqcMetadata: return metadata.oqcMetadata.compiledProgram + elif metadata.iqmMetadata: + return metadata.iqmMetadata.compiledProgram else: return None diff --git a/test/integ_tests/test_cost_tracking.py b/test/integ_tests/test_cost_tracking.py index 0c06f297a..c2c590331 100644 --- a/test/integ_tests/test_cost_tracking.py +++ b/test/integ_tests/test_cost_tracking.py @@ -28,8 +28,8 @@ "qpu", [ "arn:aws:braket:us-east-1::device/qpu/ionq/Harmony", - "arn:aws:braket:eu-west-2::device/qpu/oqc/Lucy", - "arn:aws:braket:us-west-1::device/qpu/rigetti/Aspen-M-3", + "arn:aws:braket:eu-north-1::device/qpu/iqm/Garnet", + "arn:aws:braket:us-west-1::device/qpu/rigetti/Ankaa-2", ], ) def test_qpu_tracking(qpu): diff --git a/test/integ_tests/test_device_creation.py b/test/integ_tests/test_device_creation.py index 540c09f61..58bdb35a1 100644 --- a/test/integ_tests/test_device_creation.py +++ b/test/integ_tests/test_device_creation.py @@ -17,15 +17,15 @@ from braket.aws import AwsDevice from braket.devices import Devices -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" +RIGETTI_ARN = "arn:aws:braket:us-west-1::device/qpu/rigetti/Ankaa-2" +IONQ_ARN = "arn:aws:braket:us-east-1::device/qpu/ionq/Aria-1" +IQM_ARN = "arn:aws:braket:eu-north-1::device/qpu/iqm/Garnet" SIMULATOR_ARN = "arn:aws:braket:::device/quantum-simulator/amazon/sv1" -OQC_ARN = "arn:aws:braket:eu-west-2::device/qpu/oqc/Lucy" -PULSE_ARN = "arn:aws:braket:us-west-1::device/qpu/rigetti/Aspen-M-3" +PULSE_ARN = "arn:aws:braket:us-west-1::device/qpu/rigetti/Ankaa-2" @pytest.mark.parametrize( - "arn", [(RIGETTI_ARN), (IONQ_ARN), (OQC_ARN), (SIMULATOR_ARN), (PULSE_ARN)] + "arn", [(RIGETTI_ARN), (IONQ_ARN), (IQM_ARN), (SIMULATOR_ARN), (PULSE_ARN)] ) def test_device_creation(arn, created_braket_devices): device = created_braket_devices[arn] @@ -48,10 +48,10 @@ def test_device_across_regions(aws_session, created_braket_devices): # assert QPUs across different regions can be created using the same aws_session created_braket_devices[RIGETTI_ARN] created_braket_devices[IONQ_ARN] - created_braket_devices[OQC_ARN] + created_braket_devices[IQM_ARN] -@pytest.mark.parametrize("arn", [(RIGETTI_ARN), (IONQ_ARN), (OQC_ARN), (SIMULATOR_ARN)]) +@pytest.mark.parametrize("arn", [(RIGETTI_ARN), (IONQ_ARN), (IQM_ARN), (SIMULATOR_ARN)]) def test_get_devices_arn(arn): results = AwsDevice.get_devices(arns=[arn]) assert results[0].arn == arn @@ -77,7 +77,7 @@ def test_get_devices_others(): def test_get_devices_all(braket_devices): result_arns = [result.arn for result in braket_devices] - for arn in [RIGETTI_ARN, IONQ_ARN, SIMULATOR_ARN, OQC_ARN]: + for arn in [RIGETTI_ARN, IONQ_ARN, IQM_ARN, SIMULATOR_ARN]: assert arn in result_arns diff --git a/test/integ_tests/test_measure.py b/test/integ_tests/test_measure.py index b7fef275b..8fd72e6bf 100644 --- a/test/integ_tests/test_measure.py +++ b/test/integ_tests/test_measure.py @@ -23,9 +23,9 @@ DEVICE = LocalSimulator() SHOTS = 8000 -IONQ_ARN = "arn:aws:braket:us-east-1::device/qpu/ionq/Harmony" +IONQ_ARN = "arn:aws:braket:us-east-1::device/qpu/ionq/Aria-1" SIMULATOR_ARN = "arn:aws:braket:::device/quantum-simulator/amazon/sv1" -OQC_ARN = "arn:aws:braket:eu-west-2::device/qpu/oqc/Lucy" +IQM_ARN = "arn:aws:braket:eu-north-1::device/qpu/iqm/Garnet" @pytest.mark.parametrize("arn", [(IONQ_ARN), (SIMULATOR_ARN)]) @@ -53,7 +53,7 @@ def test_measure_on_local_sim(sim): assert result.measured_qubits == [0, 1] -@pytest.mark.parametrize("arn", [(OQC_ARN)]) +@pytest.mark.parametrize("arn", [(IQM_ARN)]) def test_measure_on_supported_devices(arn): device = AwsDevice(arn) if not device.is_available: diff --git a/test/integ_tests/test_pulse.py b/test/integ_tests/test_pulse.py index 4eb3ffa93..e5f42bcd3 100644 --- a/test/integ_tests/test_pulse.py +++ b/test/integ_tests/test_pulse.py @@ -11,7 +11,7 @@ @pytest.fixture def device(): - return AwsDevice("arn:aws:braket:us-west-1::device/qpu/rigetti/Aspen-M-3") + return AwsDevice("arn:aws:braket:us-west-1::device/qpu/rigetti/Ankaa-2") @pytest.fixture @@ -174,7 +174,7 @@ def h_gate(q0): return Circuit().rz(q0, np.pi).rx(q0, np.pi / 2).rz(q0, np.pi / 2).rx(q0, -np.pi / 2) -def cz_pulse( +def make_pulse( q0: str, q1: str, shift_phases_q0: float, @@ -182,30 +182,22 @@ def cz_pulse( waveform: ArbitraryWaveform, device: AwsDevice, ): - q0_rf_frame = device.frames[f"q{q0}_rf_frame"] - q1_rf_frame = device.frames[f"q{q1}_rf_frame"] - q0_q1_cz_frame = device.frames[f"q{q0}_q{q1}_cz_frame"] - frames = [q0_rf_frame, q1_rf_frame, q0_q1_cz_frame] + q0_drive_frame = device.frames[f"Transmon_{q0}_charge_tx"] + q1_drive_frame = device.frames[f"Transmon_{q1}_charge_tx"] + frames = [q0_drive_frame, q1_drive_frame] - dt = device.properties.pulse.ports[q0_q1_cz_frame.port.id].dt + dt = device.properties.pulse.ports[q0_drive_frame.port.id].dt wfm_duration = len(waveform.amplitudes) * dt pulse_sequence = ( PulseSequence() .barrier(frames) - .play(q0_q1_cz_frame, waveform) - .delay(q0_rf_frame, wfm_duration) - .shift_phase(q0_rf_frame, shift_phases_q0) - .delay(q1_rf_frame, wfm_duration) - .shift_phase(q1_rf_frame, shift_phases_q1) + .delay(q0_drive_frame, wfm_duration) + .shift_phase(q0_drive_frame, shift_phases_q0) + .delay(q1_drive_frame, wfm_duration) + .shift_phase(q1_drive_frame, shift_phases_q1) .barrier(frames) ) - for phase, q in [(shift_phases_q0 * 0.5, q0), (-shift_phases_q1 * 0.5, q1)]: - for neighbor in device.properties.paradigm.connectivity.connectivityGraph[str(q)]: - xy_frame_name = f"q{min(q, int(neighbor))}_q{max(q, int(neighbor))}_xy_frame" - if xy_frame_name in device.frames: - xy_frame = device.frames[xy_frame_name] - pulse_sequence.shift_phase(xy_frame, phase) return pulse_sequence @@ -216,17 +208,17 @@ def test_pulse_bell(arbitrary_waveform, device): a, b, ) = ( - 10, - 113, + 26, + 33, ) # qubits used p0, p1 = 1.1733407221086924, 6.269846678712192 theta_0, theta_1 = FreeParameter("theta_0"), FreeParameter("theta_1") - a_b_cz_waveform = arbitrary_waveform - cz = cz_pulse(a, b, theta_0, theta_1, a_b_cz_waveform, device) + a_b_waveform = arbitrary_waveform + pulse = make_pulse(a, b, theta_0, theta_1, a_b_waveform, device) - bell_pair_with_gates = Circuit().h(a).h(b).cz(a, b).h(b) + bell_pair_with_gates = Circuit().h(a).h(b).iswap(a, b).h(b) bell_pair_with_pulses_unbound = ( - h_gate(a) + h_gate(b) + Circuit().pulse_gate([a, b], cz) + h_gate(b) + h_gate(a) + h_gate(b) + Circuit().pulse_gate([a, b], pulse) + h_gate(b) ) bell_pair_with_pulses = bell_pair_with_pulses_unbound(theta_0=p0, theta_1=p1) @@ -266,27 +258,27 @@ def test_pulse_sequence(arbitrary_waveform, device): a, b, ) = ( - 10, - 113, + 26, + 33, ) # qubits used p0, p1 = 1.1733407221086924, 6.269846678712192 theta_0, theta_1 = FreeParameter("theta_0"), FreeParameter("theta_1") - a_b_cz_waveform = arbitrary_waveform + a_b_waveform = arbitrary_waveform - cz_with_pulses_unbound = cz_pulse(a, b, theta_0, theta_1, a_b_cz_waveform, device) + pulse_unbound = make_pulse(a, b, theta_0, theta_1, a_b_waveform, device) - q0_readout_frame = device.frames[f"q{a}_ro_rx_frame"] - q1_readout_frame = device.frames[f"q{b}_ro_rx_frame"] - cz_with_pulses = ( - cz_with_pulses_unbound(theta_0=p0, theta_1=p1) + q0_readout_frame = device.frames[f"Transmon_{a}_readout_rx"] + q1_readout_frame = device.frames[f"Transmon_{b}_readout_rx"] + pulses = ( + pulse_unbound(theta_0=p0, theta_1=p1) .capture_v0(q0_readout_frame) .capture_v0(q1_readout_frame) ) - cz_with_gates = Circuit().cz(a, b) + circuit_with_gates = Circuit().iswap(a, b) num_shots = 1000 - gate_task = device.run(cz_with_gates, shots=num_shots, disable_qubit_rewiring=True) - pulse_task = device.run(cz_with_pulses, shots=num_shots) + gate_task = device.run(circuit_with_gates, shots=num_shots, disable_qubit_rewiring=True) + pulse_task = device.run(pulses, shots=num_shots) if not device.is_available: try: @@ -313,12 +305,13 @@ def test_pulse_sequence(arbitrary_waveform, device): assert chi_squared < 10 # adjust this threshold if test is flaky +@pytest.mark.skip(reason="needs to be updated to work correctly on Ankaa-2") def test_gate_calibration_run(device, pulse_sequence): if device.status == "OFFLINE": pytest.skip("Device offline") user_gate_calibrations = GateCalibrations({(Gate.Rx(math.pi / 2), QubitSet(0)): pulse_sequence}) num_shots = 50 - bell_circuit = Circuit().rx(0, math.pi / 2).rx(1, math.pi / 2).cz(0, 1).rx(1, -math.pi / 2) + bell_circuit = Circuit().rx(0, math.pi / 2).rx(1, math.pi / 2).iswap(0, 1).rx(1, -math.pi / 2) user_calibration_task = device.run( bell_circuit, gate_definitions=user_gate_calibrations.pulse_sequences, diff --git a/test/integ_tests/test_reservation_arn.py b/test/integ_tests/test_reservation_arn.py index 64135f76e..57833a840 100644 --- a/test/integ_tests/test_reservation_arn.py +++ b/test/integ_tests/test_reservation_arn.py @@ -33,7 +33,7 @@ def reservation_arn(aws_session): def test_create_task_via_invalid_reservation_arn_on_qpu(reservation_arn): circuit = Circuit().h(0) - device = AwsDevice(Devices.IonQ.Harmony) + device = AwsDevice(Devices.IonQ.Aria1) with pytest.raises(ClientError, match="Reservation arn is invalid"): device.run(circuit, shots=10, reservation_arn=reservation_arn) diff --git a/test/unit_tests/braket/ahs/test_analog_hamiltonian_simulation.py b/test/unit_tests/braket/ahs/test_analog_hamiltonian_simulation.py index 83178c120..3af8554a1 100644 --- a/test/unit_tests/braket/ahs/test_analog_hamiltonian_simulation.py +++ b/test/unit_tests/braket/ahs/test_analog_hamiltonian_simulation.py @@ -70,6 +70,140 @@ def local_detuning(): ) +@pytest.fixture +def ir(): + return Program.parse_raw_schema( + """ +{ + "braketSchemaHeader": { + "name": "braket.ir.ahs.program", + "version": "1" + }, + "setup": { + "ahs_register": { + "sites": [ + [ + "0.0", + "0.0" + ], + [ + "0.0", + "0.000003" + ], + [ + "0.0", + "0.000006" + ], + [ + "0.000003", + "0.0" + ], + [ + "0.000003", + "0.000003" + ], + [ + "0.000003", + "0.000003" + ], + [ + "0.000003", + "0.000006" + ] + ], + "filling": [ + 1, + 1, + 1, + 1, + 1, + 0, + 0 + ] + } + }, + "hamiltonian": { + "drivingFields": [ + { + "amplitude": { + "time_series": { + "values": [ + "0.0", + "25132700.0", + "25132700.0", + "0.0" + ], + "times": [ + "0.0", + "3E-7", + "0.0000027", + "0.000003" + ] + }, + "pattern": "uniform" + }, + "phase": { + "time_series": { + "values": [ + "0", + "0" + ], + "times": [ + "0.0", + "0.000003" + ] + }, + "pattern": "uniform" + }, + "detuning": { + "time_series": { + "values": [ + "-125664000.0", + "-125664000.0", + "125664000.0", + "125664000.0" + ], + "times": [ + "0.0", + "3E-7", + "0.0000027", + "0.000003" + ] + }, + "pattern": "uniform" + } + } + ], + "localDetuning": [ + { + "magnitude": { + "time_series": { + "values": [ + "-125664000.0", + "125664000.0" + ], + "times": [ + "0.0", + "0.000003" + ] + }, + "pattern": [ + "0.5", + "1.0", + "0.5", + "0.5", + "0.5", + "0.5" + ] + } + } + ] + } +} +""" + ) + + def test_create(): mock0 = Mock() mock1 = Mock() @@ -95,6 +229,38 @@ def test_to_ir_empty(): assert problem == Program.parse_raw_schema(problem.json()) +def test_from_ir(ir): + problem = AnalogHamiltonianSimulation.from_ir(ir).to_ir() + assert problem == ir + assert problem == Program.parse_raw_schema(problem.json()) + + +def test_from_ir_empty(): + empty_ir = Program.parse_raw_schema( + """ +{ + "braketSchemaHeader": { + "name": "braket.ir.ahs.program", + "version": "1" + }, + "setup": { + "ahs_register": { + "sites": [], + "filling": [] + } + }, + "hamiltonian": { + "drivingFields": [], + "localDetuning": [] + } +} +""" + ) + problem = AnalogHamiltonianSimulation.from_ir(empty_ir).to_ir() + assert problem == empty_ir + assert problem == Program.parse_raw_schema(problem.json()) + + @pytest.mark.xfail(raises=TypeError) def test_to_ir_invalid_hamiltonian(register): hamiltonian = Mock() diff --git a/test/unit_tests/braket/ahs/test_field.py b/test/unit_tests/braket/ahs/test_field.py index 2ff6714ce..21006fa6d 100644 --- a/test/unit_tests/braket/ahs/test_field.py +++ b/test/unit_tests/braket/ahs/test_field.py @@ -98,3 +98,23 @@ def test_uniform_field( ) or expected.pattern.series == actual.pattern.series assert expected.time_series.times() == actual.time_series.times() assert expected.time_series.values() == actual.time_series.values() + + +def test_from_lists(): + times = [0, 0.1, 0.2, 0.3] + values = [0.5, 0.8, 0.9, 1.0] + pattern = [0.3, 0.7, 0.6, -0.5, 0, 1.6] + + sh_field = Field.from_lists(times, values, pattern) + assert sh_field.time_series.times() == times + assert sh_field.time_series.values() == values + assert sh_field.pattern.series == pattern + + +@pytest.mark.xfail(raises=ValueError) +def test_from_lists_not_eq_length(): + times = [0, 0.1, 0.2] + values = [0.5, 0.8, 0.9, 1.0] + pattern = [0.3, 0.7, 0.6, -0.5, 0, 1.6] + + Field.from_lists(times, values, pattern) diff --git a/test/unit_tests/braket/aws/common_test_utils.py b/test/unit_tests/braket/aws/common_test_utils.py index aaca559f5..0e1fd4d48 100644 --- a/test/unit_tests/braket/aws/common_test_utils.py +++ b/test/unit_tests/braket/aws/common_test_utils.py @@ -17,7 +17,7 @@ from braket.aws import AwsQuantumTaskBatch DWAVE_ARN = "arn:aws:braket:::device/qpu/d-wave/Advantage_system1" -RIGETTI_ARN = "arn:aws:braket:::device/qpu/rigetti/Aspen-10" +RIGETTI_ARN = "arn:aws:braket:::device/qpu/rigetti/Ankaa-2" IONQ_ARN = "arn:aws:braket:us-east-1::device/qpu/ionq/Harmony" OQC_ARN = "arn:aws:braket:eu-west-2::device/qpu/oqc/Lucy" SV1_ARN = "arn:aws:braket:::device/quantum-simulator/amazon/sv1" diff --git a/test/unit_tests/braket/aws/test_aws_device.py b/test/unit_tests/braket/aws/test_aws_device.py index a778dfb5f..3fed52aa2 100644 --- a/test/unit_tests/braket/aws/test_aws_device.py +++ b/test/unit_tests/braket/aws/test_aws_device.py @@ -216,7 +216,7 @@ def test_mock_rigetti_schema_1(): MOCK_GATE_MODEL_QPU_1 = { - "deviceName": "Aspen-10", + "deviceName": "Ankaa-2", "deviceType": "QPU", "providerName": "Rigetti", "deviceStatus": "OFFLINE", @@ -1451,7 +1451,7 @@ def test_run_device_poll_interval_kwargs( capabilities = MOCK_GATE_MODEL_QPU_CAPABILITIES_1 capabilities.service.getTaskPollIntervalMillis = poll_interval_seconds properties = { - "deviceName": "Aspen-10", + "deviceName": "Ankaa-2", "deviceType": "QPU", "providerName": "provider1", "deviceStatus": "OFFLINE", 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 67ca98228..028ad3cc4 100644 --- a/test/unit_tests/braket/aws/test_aws_quantum_job.py +++ b/test/unit_tests/braket/aws/test_aws_quantum_job.py @@ -70,7 +70,7 @@ def _get_job_response(**kwargs): }, "createdAt": datetime.datetime(2021, 6, 28, 21, 4, 51), "deviceConfig": { - "device": "arn:aws:braket:::device/qpu/rigetti/Aspen-10", + "device": "arn:aws:braket:::device/qpu/rigetti/Ankaa-2", }, "hyperParameters": { "foo": "bar", 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 aec875568..b10891830 100644 --- a/test/unit_tests/braket/circuits/test_ascii_circuit_diagram.py +++ b/test/unit_tests/braket/circuits/test_ascii_circuit_diagram.py @@ -741,19 +741,19 @@ def test_noise_multi_probabilities(): def test_noise_multi_probabilities_with_parameter(): a = FreeParameter("a") - b = FreeParameter("b") c = FreeParameter("c") - circ = Circuit().h(0).x(1).pauli_channel(1, a, b, c) + d = FreeParameter("d") + circ = Circuit().h(0).x(1).pauli_channel(1, a, c, d) expected = ( "T : | 0 |", " ", "q0 : -H-----------", " ", - "q1 : -X-PC(a,b,c)-", + "q1 : -X-PC(a,c,d)-", "", "T : | 0 |", "", - "Unassigned parameters: [a, b, c].", + "Unassigned parameters: [a, c, d].", ) _assert_correct_diagram(circ, expected) diff --git a/test/unit_tests/braket/circuits/test_circuit.py b/test/unit_tests/braket/circuits/test_circuit.py index 71eecd1f1..897828b27 100644 --- a/test/unit_tests/braket/circuits/test_circuit.py +++ b/test/unit_tests/braket/circuits/test_circuit.py @@ -3610,3 +3610,32 @@ def test_circuit_with_global_phase(): "b[0] = measure $0;", ] ) + + +def test_from_ir_round_trip_transformation_with_targeted_measurements(): + circuit = ( + Circuit() + .h(0) + .cnot(0, 1) + .add_instruction(Instruction(Measure(index=2), 1)) + .add_instruction(Instruction(Measure(index=1), 2)) + .add_instruction(Instruction(Measure(index=0), 0)) + ) + ir = OpenQasmProgram( + source="\n".join( + [ + "OPENQASM 3.0;", + "bit[3] b;", + "qubit[3] q;", + "h q[0];", + "cnot q[0], q[1];", + "b[2] = measure q[1];", + "b[1] = measure q[2];", + "b[0] = measure q[0];", + ] + ), + inputs={}, + ) + + assert Circuit.from_ir(ir) == Circuit.from_ir(circuit.to_ir("OPENQASM")) + assert circuit.to_ir("OPENQASM") == Circuit.from_ir(ir).to_ir("OPENQASM") diff --git a/test/unit_tests/braket/circuits/test_gates.py b/test/unit_tests/braket/circuits/test_gates.py index 0b9ce52d7..285b530c1 100644 --- a/test/unit_tests/braket/circuits/test_gates.py +++ b/test/unit_tests/braket/circuits/test_gates.py @@ -1069,8 +1069,8 @@ def test_bind_values_pulse_gate(): frame = Frame("user_frame", Port("device_port_x", 1e-9), 1e9) gate = Gate.PulseGate( PulseSequence() - .set_frequency(frame, FreeParameter("a") + FreeParameter("b")) - .delay(frame, FreeParameter("c")), + .set_frequency(frame, FreeParameter("a") + FreeParameter("c")) + .delay(frame, FreeParameter("d")), qubit_count, ) @@ -1084,8 +1084,8 @@ def to_ir(pulse_gate): assert a_bound_ir == "\n".join( [ "cal {", - " set_frequency(user_frame, 3.0 + b);", - " delay[c * 1s] user_frame;", + " set_frequency(user_frame, 3.0 + c);", + " delay[d * 1s] user_frame;", "}", ] ) diff --git a/test/unit_tests/braket/circuits/test_noises.py b/test/unit_tests/braket/circuits/test_noises.py index 5fffba43f..3b7f08dd8 100644 --- a/test/unit_tests/braket/circuits/test_noises.py +++ b/test/unit_tests/braket/circuits/test_noises.py @@ -483,10 +483,10 @@ def test_parameter_binding(parameterized_noise, params, expected_noise): def test_parameterized_noise(): - noise = Noise.PauliChannel(FreeParameter("a"), 0.2, FreeParameter("b")) + noise = Noise.PauliChannel(FreeParameter("a"), 0.2, FreeParameter("d")) assert noise.probX == FreeParameter("a") assert noise.probY == 0.2 - assert noise.probZ == FreeParameter("b") + assert noise.probZ == FreeParameter("d") # Additional Unitary noise tests diff --git a/test/unit_tests/braket/circuits/test_observables.py b/test/unit_tests/braket/circuits/test_observables.py index b6430d4d8..213a29705 100644 --- a/test/unit_tests/braket/circuits/test_observables.py +++ b/test/unit_tests/braket/circuits/test_observables.py @@ -61,100 +61,116 @@ def test_to_ir(testobject, gateobject, expected_ir, basis_rotation_gates, eigenv @pytest.mark.parametrize( - "observable, serialization_properties, target, expected_ir", + "observable, observable_with_targets, serialization_properties, target, expected_ir", [ ( Observable.I(), + Observable.I(3), OpenQASMSerializationProperties(qubit_reference_type=QubitReferenceType.VIRTUAL), [3], "i(q[3])", ), ( Observable.I(), + Observable.I(3), OpenQASMSerializationProperties(qubit_reference_type=QubitReferenceType.PHYSICAL), [3], "i($3)", ), ( Observable.I(), + None, OpenQASMSerializationProperties(qubit_reference_type=QubitReferenceType.VIRTUAL), None, "i all", ), ( Observable.X(), + Observable.X(3), OpenQASMSerializationProperties(qubit_reference_type=QubitReferenceType.VIRTUAL), [3], "x(q[3])", ), ( Observable.X(), + Observable.X(3), OpenQASMSerializationProperties(qubit_reference_type=QubitReferenceType.PHYSICAL), [3], "x($3)", ), ( Observable.X(), + None, OpenQASMSerializationProperties(qubit_reference_type=QubitReferenceType.VIRTUAL), None, "x all", ), ( Observable.Y(), + Observable.Y(3), OpenQASMSerializationProperties(qubit_reference_type=QubitReferenceType.VIRTUAL), [3], "y(q[3])", ), ( Observable.Y(), + Observable.Y(3), OpenQASMSerializationProperties(qubit_reference_type=QubitReferenceType.PHYSICAL), [3], "y($3)", ), ( Observable.Y(), + None, OpenQASMSerializationProperties(qubit_reference_type=QubitReferenceType.VIRTUAL), None, "y all", ), ( Observable.Z(), + Observable.Z(3), OpenQASMSerializationProperties(qubit_reference_type=QubitReferenceType.VIRTUAL), [3], "z(q[3])", ), ( Observable.Z(), + Observable.Z(3), OpenQASMSerializationProperties(qubit_reference_type=QubitReferenceType.PHYSICAL), [3], "z($3)", ), ( Observable.Z(), + None, OpenQASMSerializationProperties(qubit_reference_type=QubitReferenceType.VIRTUAL), None, "z all", ), ( Observable.H(), + Observable.H(3), OpenQASMSerializationProperties(qubit_reference_type=QubitReferenceType.VIRTUAL), [3], "h(q[3])", ), ( Observable.H(), + Observable.H(3), OpenQASMSerializationProperties(qubit_reference_type=QubitReferenceType.PHYSICAL), [3], "h($3)", ), ( Observable.H(), + None, OpenQASMSerializationProperties(qubit_reference_type=QubitReferenceType.VIRTUAL), None, "h all", ), ( Observable.Hermitian(np.eye(4)), + Observable.Hermitian(np.eye(4), targets=[1, 2]), OpenQASMSerializationProperties(qubit_reference_type=QubitReferenceType.VIRTUAL), [1, 2], "hermitian([[1+0im, 0im, 0im, 0im], [0im, 1+0im, 0im, 0im], " @@ -162,6 +178,7 @@ def test_to_ir(testobject, gateobject, expected_ir, basis_rotation_gates, eigenv ), ( Observable.Hermitian(np.eye(4)), + Observable.Hermitian(np.eye(4), targets=[1, 2]), OpenQASMSerializationProperties(qubit_reference_type=QubitReferenceType.PHYSICAL), [1, 2], "hermitian([[1+0im, 0im, 0im, 0im], [0im, 1+0im, 0im, 0im], " @@ -169,36 +186,42 @@ def test_to_ir(testobject, gateobject, expected_ir, basis_rotation_gates, eigenv ), ( Observable.Hermitian(np.eye(2)), + None, OpenQASMSerializationProperties(qubit_reference_type=QubitReferenceType.VIRTUAL), None, "hermitian([[1+0im, 0im], [0im, 1+0im]]) all", ), ( Observable.H() @ Observable.Z(), + Observable.H(3) @ Observable.Z(0), OpenQASMSerializationProperties(qubit_reference_type=QubitReferenceType.VIRTUAL), [3, 0], "h(q[3]) @ z(q[0])", ), ( Observable.H() @ Observable.Z(), + Observable.H(3) @ Observable.Z(0), OpenQASMSerializationProperties(qubit_reference_type=QubitReferenceType.PHYSICAL), [3, 0], "h($3) @ z($0)", ), ( Observable.H() @ Observable.Z() @ Observable.I(), + Observable.H(3) @ Observable.Z(0) @ Observable.I(1), OpenQASMSerializationProperties(qubit_reference_type=QubitReferenceType.VIRTUAL), [3, 0, 1], "h(q[3]) @ z(q[0]) @ i(q[1])", ), ( Observable.H() @ Observable.Z() @ Observable.I(), + Observable.H(3) @ Observable.Z(0) @ Observable.I(1), OpenQASMSerializationProperties(qubit_reference_type=QubitReferenceType.PHYSICAL), [3, 0, 1], "h($3) @ z($0) @ i($1)", ), ( Observable.Hermitian(np.eye(4)) @ Observable.I(), + Observable.Hermitian(np.eye(4), targets=[3, 0]) @ Observable.I(1), OpenQASMSerializationProperties(qubit_reference_type=QubitReferenceType.VIRTUAL), [3, 0, 1], "hermitian([[1+0im, 0im, 0im, 0im], [0im, 1+0im, 0im, 0im], " @@ -207,32 +230,23 @@ def test_to_ir(testobject, gateobject, expected_ir, basis_rotation_gates, eigenv ), ( Observable.I() @ Observable.Hermitian(np.eye(4)), + Observable.I(3) @ Observable.Hermitian(np.eye(4), targets=[0, 1]), OpenQASMSerializationProperties(qubit_reference_type=QubitReferenceType.PHYSICAL), [3, 0, 1], "i($3) @ " "hermitian([[1+0im, 0im, 0im, 0im], [0im, 1+0im, 0im, 0im], " "[0im, 0im, 1+0im, 0im], [0im, 0im, 0im, 1+0im]]) $0, $1", ), - ( - (2 * Observable.Z()) @ (3 * Observable.H()), - OpenQASMSerializationProperties(qubit_reference_type=QubitReferenceType.PHYSICAL), - [3, 3], - "6 * z($3) @ h($3)", - ), - ( - (2 * Observable.Z()) @ (3 * Observable.H()) @ (2 * Observable.Y()), - OpenQASMSerializationProperties(qubit_reference_type=QubitReferenceType.PHYSICAL), - [3, 3, 1], - "12 * z($3) @ h($3) @ y($1)", - ), ( 3 * (2 * Observable.Z()), + 3 * (2 * Observable.Z(3)), OpenQASMSerializationProperties(qubit_reference_type=QubitReferenceType.PHYSICAL), [3], "6 * z($3)", ), ( (2 * Observable.I()) @ (2 * Observable.Hermitian(np.eye(4))), + (2 * Observable.I(3)) @ (2 * Observable.Hermitian(np.eye(4), targets=[0, 1])), OpenQASMSerializationProperties(qubit_reference_type=QubitReferenceType.PHYSICAL), [3, 0, 1], "4 * i($3) @ " @@ -241,55 +255,84 @@ def test_to_ir(testobject, gateobject, expected_ir, basis_rotation_gates, eigenv ), ( Observable.Z() + 2 * Observable.H(), + Observable.Z(3) + 2 * Observable.H(4), OpenQASMSerializationProperties(qubit_reference_type=QubitReferenceType.PHYSICAL), [[3], [4]], "z($3) + 2 * h($4)", ), ( 3 * (Observable.H() + 2 * Observable.X()), + 3 * (Observable.H(3) + 2 * Observable.X(0)), OpenQASMSerializationProperties(qubit_reference_type=QubitReferenceType.PHYSICAL), [[3], [0]], "3 * h($3) + 6 * x($0)", ), ( 3 * (Observable.H() + 2 * Observable.H()), + 3 * (Observable.H(3) + 2 * Observable.H(3)), OpenQASMSerializationProperties(qubit_reference_type=QubitReferenceType.PHYSICAL), [[3], [3]], "3 * h($3) + 6 * h($3)", ), ( 3 * (Observable.H() + 2 * Observable.H()), + 3 * (Observable.H(3) + 2 * Observable.H(5)), OpenQASMSerializationProperties(qubit_reference_type=QubitReferenceType.PHYSICAL), [[3], [5]], "3 * h($3) + 6 * h($5)", ), ( (2 * Observable.Y()) @ (3 * Observable.I()) + 0.75 * Observable.Y() @ Observable.Z(), + (2 * Observable.Y(0)) @ (3 * Observable.I(1)) + + 0.75 * Observable.Y(0) @ Observable.Z(1), OpenQASMSerializationProperties(qubit_reference_type=QubitReferenceType.PHYSICAL), [[0, 1], [0, 1]], "6 * y($0) @ i($1) + 0.75 * y($0) @ z($1)", ), ( (-2 * Observable.Y()) @ (3 * Observable.I()) + -0.75 * Observable.Y() @ Observable.Z(), + (-2 * Observable.Y(0)) @ (3 * Observable.I(1)) + + -0.75 * Observable.Y(0) @ Observable.Z(1), OpenQASMSerializationProperties(qubit_reference_type=QubitReferenceType.PHYSICAL), [[0, 1], [0, 1]], "-6 * y($0) @ i($1) - 0.75 * y($0) @ z($1)", ), ( 4 * (2 * Observable.Z() + 2 * (3 * Observable.X() @ (2 * Observable.Y()))), + 4 * (2 * Observable.Z(0) + 2 * (3 * Observable.X(1) @ (2 * Observable.Y(2)))), OpenQASMSerializationProperties(qubit_reference_type=QubitReferenceType.PHYSICAL), [[0], [1, 2]], "8 * z($0) + 48 * x($1) @ y($2)", ), + ( + 4 * (2 * Observable.Z(0) + 2 * (3 * Observable.X(1) @ (2 * Observable.Y(2)))), + None, + OpenQASMSerializationProperties(qubit_reference_type=QubitReferenceType.PHYSICAL), + [[5], [4, 3]], + "8 * z($5) + 48 * x($4) @ y($3)", + ), ], ) -def test_observables_to_ir_openqasm(observable, serialization_properties, target, expected_ir): +def test_observables_to_ir_openqasm( + observable, + observable_with_targets, + serialization_properties, + target, + expected_ir, +): assert ( observable.to_ir( target, ir_type=IRType.OPENQASM, serialization_properties=serialization_properties ) == expected_ir ) + if observable_with_targets: + assert ( + observable_with_targets.to_ir( + None, ir_type=IRType.OPENQASM, serialization_properties=serialization_properties + ) + == expected_ir + ) @pytest.mark.parametrize( @@ -458,6 +501,11 @@ def test_hermitian_eigenvalues(matrix, eigenvalues): compare_eigenvalues(Observable.Hermitian(matrix=matrix), eigenvalues) +def test_hermitian_matrix_target_mismatch(): + with pytest.raises(ValueError): + Observable.Hermitian(np.eye(4), targets=[0, 1, 2]) + + def test_flattened_tensor_product(): observable_one = Observable.Z() @ Observable.Y() observable_two = Observable.X() @ Observable.H() @@ -612,6 +660,16 @@ def test_tensor_product_basis_rotation_gates(observable, basis_rotation_gates): assert observable.basis_rotation_gates == basis_rotation_gates +def test_tensor_product_repeated_qubits(): + with pytest.raises(ValueError): + (2 * Observable.Z(3)) @ (3 * Observable.H(3)) + + +def test_tensor_product_with_and_without_targets(): + with pytest.raises(ValueError): + (2 * Observable.Z(3)) @ (3 * Observable.H()) + + def test_observable_from_ir_tensor_product(): expected_observable = Observable.TensorProduct([Observable.Z(), Observable.I(), Observable.X()]) actual_observable = observable_from_ir(["z", "i", "x"]) @@ -714,3 +772,8 @@ def test_unscaled_tensor_product(): observable = 3 * ((2 * Observable.X()) @ (5 * Observable.Y())) assert observable == 30 * (Observable.X() @ Observable.Y()) assert observable._unscaled() == Observable.X() @ Observable.Y() + + +def test_sum_with_and_without_targets(): + with pytest.raises(ValueError): + Observable.X() + 3 * Observable.Y(4) diff --git a/test/unit_tests/braket/circuits/test_result_type.py b/test/unit_tests/braket/circuits/test_result_type.py index bc7cc0909..bb543b7c0 100644 --- a/test/unit_tests/braket/circuits/test_result_type.py +++ b/test/unit_tests/braket/circuits/test_result_type.py @@ -18,6 +18,7 @@ from braket.circuits.free_parameter import FreeParameter from braket.circuits.result_type import ObservableParameterResultType from braket.circuits.serialization import IRType +from braket.registers import QubitSet @pytest.fixture @@ -168,6 +169,15 @@ def test_obs_rt_repr(): ) +def test_obs_rt_target(): + assert ObservableResultType( + ascii_symbols=["Obs"], observable=Observable.X(), target=1 + ).target == QubitSet(1) + assert ObservableResultType( + ascii_symbols=["Obs"], observable=Observable.X(1) + ).target == QubitSet(1) + + @pytest.mark.parametrize( "ir_type, serialization_properties, expected_exception, expected_message", [ diff --git a/test/unit_tests/braket/circuits/test_result_types.py b/test/unit_tests/braket/circuits/test_result_types.py index 5f76eeaf9..422fed7a4 100644 --- a/test/unit_tests/braket/circuits/test_result_types.py +++ b/test/unit_tests/braket/circuits/test_result_types.py @@ -272,6 +272,25 @@ def test_ir_result_level(testclass, subroutine_name, irclass, input, ir_input): "#pragma braket result adjoint_gradient expectation(hermitian([[1+0im, 0im], " "[0im, 1+0im]]) q[0]) all", ), + ( + ResultType.AdjointGradient( + Observable.H(0) @ Observable.I(1) + 2 * Observable.Z(2), + parameters=[FreeParameter("alpha"), "beta", FreeParameter("gamma")], + ), + OpenQASMSerializationProperties(qubit_reference_type=QubitReferenceType.VIRTUAL), + "#pragma braket result adjoint_gradient expectation(h(q[0]) @ i(q[1]) + 2 * z(q[2])) " + "alpha, beta, gamma", + ), + ( + ResultType.AdjointGradient( + Observable.H(0) @ Observable.I(1) + 2 * Observable.Z(2), + target=[[3, 4], [5]], + parameters=[FreeParameter("alpha"), "beta", FreeParameter("gamma")], + ), + OpenQASMSerializationProperties(qubit_reference_type=QubitReferenceType.VIRTUAL), + "#pragma braket result adjoint_gradient expectation(h(q[3]) @ i(q[4]) + 2 * z(q[5])) " + "alpha, beta, gamma", + ), ], ) def test_result_to_ir_openqasm(result_type, serialization_properties, expected_ir): diff --git a/test/unit_tests/braket/circuits/test_unicode_circuit_diagram.py b/test/unit_tests/braket/circuits/test_unicode_circuit_diagram.py index 268c53a96..efc87ee24 100644 --- a/test/unit_tests/braket/circuits/test_unicode_circuit_diagram.py +++ b/test/unit_tests/braket/circuits/test_unicode_circuit_diagram.py @@ -853,20 +853,20 @@ def test_noise_multi_probabilities(): def test_noise_multi_probabilities_with_parameter(): a = FreeParameter("a") - b = FreeParameter("b") c = FreeParameter("c") - circ = Circuit().h(0).x(1).pauli_channel(1, a, b, c) + d = FreeParameter("d") + circ = Circuit().h(0).x(1).pauli_channel(1, a, c, d) expected = ( "T : │ 0 │", " ┌───┐ ", "q0 : ─┤ H ├───────────────", " └───┘ ", " ┌───┐ ┌───────────┐ ", - "q1 : ─┤ X ├─┤ PC(a,b,c) ├─", + "q1 : ─┤ X ├─┤ PC(a,c,d) ├─", " └───┘ └───────────┘ ", "T : │ 0 │", "", - "Unassigned parameters: [a, b, c].", + "Unassigned parameters: [a, c, d].", ) _assert_correct_diagram(circ, expected) diff --git a/test/unit_tests/braket/devices/test_local_simulator.py b/test/unit_tests/braket/devices/test_local_simulator.py index 216d161c7..6c2976812 100644 --- a/test/unit_tests/braket/devices/test_local_simulator.py +++ b/test/unit_tests/braket/devices/test_local_simulator.py @@ -156,7 +156,12 @@ def properties(self) -> DeviceCapabilities: class DummyJaqcdSimulator(BraketSimulator): def run( - self, program: ir.jaqcd.Program, qubits: int, shots: Optional[int], *args, **kwargs + self, + program: ir.jaqcd.Program, + qubits: Optional[int] = None, + shots: Optional[int] = None, + *args, + **kwargs, ) -> dict[str, Any]: if not isinstance(program, ir.jaqcd.Program): raise TypeError("Not a Jaqcd program") @@ -520,7 +525,7 @@ def test_run_gate_model_inputs(): ), inputs={"theta": 2}, ), - 10, + shots=10, ) assert task.result() == GateModelQuantumTaskResult.from_object(GATE_MODEL_RESULT) @@ -543,7 +548,7 @@ def test_run_program_model_inputs(): task = sim.run(program, inputs=update_inputs, shots=10) assert program.inputs == inputs program.inputs.update(update_inputs) - dummy.run.assert_called_with(program, 10) + dummy.run.assert_called_with(program, shots=10) assert task.result() == GateModelQuantumTaskResult.from_object(GATE_MODEL_RESULT) @@ -552,7 +557,7 @@ def test_run_jaqcd_only(): sim = LocalSimulator(dummy) task = sim.run(Circuit().h(0).cnot(0, 1), 10) dummy.assert_shots(10) - dummy.assert_qubits(2) + dummy.assert_qubits(None) assert task.result() == GateModelQuantumTaskResult.from_object(GATE_MODEL_RESULT) @@ -583,10 +588,8 @@ def test_run_serializable_program_model(): source=""" qubit[2] q; bit[2] c; - h q[0]; cnot q[0], q[1]; - c = measure q; """ ) @@ -594,6 +597,25 @@ def test_run_serializable_program_model(): assert task.result() == GateModelQuantumTaskResult.from_object(GATE_MODEL_RESULT) +def test_run_serializable_program_model_with_inputs(): + dummy = DummySerializableProgramSimulator() + sim = LocalSimulator(dummy) + task = sim.run( + DummySerializableProgram( + source=""" +input float a; +qubit[2] q; +bit[2] c; +h q[0]; +cnot q[0], q[1]; +c = measure q; +""" + ), + inputs={"a": 0.1}, + ) + assert task.result() == GateModelQuantumTaskResult.from_object(GATE_MODEL_RESULT) + + @pytest.mark.xfail(raises=ValueError) def test_run_gate_model_value_error(): dummy = DummyCircuitSimulator() @@ -713,7 +735,7 @@ def test_run_with_noise_model(mock_run, noise_model): mock_run.assert_called_with( Program(source=expected_circuit, inputs={}), - 4, + shots=4, ) @@ -753,7 +775,7 @@ def test_run_noisy_circuit_with_noise_model(mock_run, noise_model): mock_run.assert_called_with( Program(source=expected_circuit, inputs={}), - 4, + shots=4, ) assert w[-1].message.__str__() == expected_warning @@ -781,6 +803,6 @@ def test_run_openqasm_with_noise_model(mock_run, noise_model): mock_run.assert_called_with( Program(source=expected_circuit, inputs=None), - 4, + shots=4, ) assert w[-1].message.__str__() == expected_warning 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 d12a29b00..39cb94c5d 100644 --- a/test/unit_tests/braket/jobs/test_quantum_job_creation.py +++ b/test/unit_tests/braket/jobs/test_quantum_job_creation.py @@ -198,7 +198,7 @@ def _get_job_response(**kwargs): }, "createdAt": datetime.datetime(2021, 6, 28, 21, 4, 51), "deviceConfig": { - "device": "arn:aws:braket:::device/qpu/rigetti/Aspen-10", + "device": "arn:aws:braket:::device/qpu/rigetti/Ankaa-2", }, "hyperParameters": { "foo": "bar", diff --git a/test/unit_tests/braket/parametric/test_free_parameter.py b/test/unit_tests/braket/parametric/test_free_parameter.py index b94c60a97..0b42bfe5a 100644 --- a/test/unit_tests/braket/parametric/test_free_parameter.py +++ b/test/unit_tests/braket/parametric/test_free_parameter.py @@ -67,3 +67,21 @@ def test_sub_successful(free_parameter): def test_sub_wrong_param(free_parameter): assert free_parameter.subs({"alpha": 1}) == FreeParameter("theta") + + +@pytest.mark.parametrize("param_name", ["for", "qubit"]) +def test_error_raised_when_reserved_keyword_used(param_name): + with pytest.raises( + ValueError, + match="FreeParameter names must not be one of the OpenQASM or OpenPulse keywords: ", + ): + FreeParameter(param_name) + + +@pytest.mark.parametrize("param_name", ["b", "q"]) +def test_error_raised_when_predefined_variable_used(param_name): + with pytest.raises( + ValueError, + match="FreeParameter names must not be one of the Braket reserved variable names: ", + ): + FreeParameter(param_name) diff --git a/test/unit_tests/braket/parametric/test_free_parameter_expression.py b/test/unit_tests/braket/parametric/test_free_parameter_expression.py index 1bf6818bf..51402482f 100644 --- a/test/unit_tests/braket/parametric/test_free_parameter_expression.py +++ b/test/unit_tests/braket/parametric/test_free_parameter_expression.py @@ -165,7 +165,7 @@ def test_sub_return_expression(): @pytest.mark.parametrize( "param, kwargs, expected_value, expected_type", [ - (FreeParameter("a") + 2 * FreeParameter("b"), {"a": 0.1, "b": 0.3}, 0.7, float), + (FreeParameter("a") + 2 * FreeParameter("d"), {"a": 0.1, "d": 0.3}, 0.7, float), (FreeParameter("x"), {"y": 1}, FreeParameter("x"), FreeParameter), (FreeParameter("y"), {"y": -0.1}, -0.1, float), (2 * FreeParameter("i"), {"i": 1}, 2.0, float), diff --git a/test/unit_tests/braket/pulse/ast/test_approximation_parser.py b/test/unit_tests/braket/pulse/ast/test_approximation_parser.py index 56f02aa12..4c0e2105c 100644 --- a/test/unit_tests/braket/pulse/ast/test_approximation_parser.py +++ b/test/unit_tests/braket/pulse/ast/test_approximation_parser.py @@ -19,7 +19,13 @@ from openpulse import ast from oqpy import IntVar -from braket.pulse import ArbitraryWaveform, ConstantWaveform, DragGaussianWaveform, GaussianWaveform +from braket.pulse import ( + ArbitraryWaveform, + ConstantWaveform, + DragGaussianWaveform, + ErfSquareWaveform, + GaussianWaveform, +) from braket.pulse.ast.approximation_parser import _ApproximationParser from braket.pulse.frame import Frame from braket.pulse.port import Port @@ -352,6 +358,46 @@ def test_set_shift_phase_beyond_2_pi(port): verify_results(parser, expected_amplitudes, expected_frequencies, expected_phases) +def test_swap_phases(port): + phase1 = 0.12 + phase2 = 0.34 + frequency = 1e8 + frame1 = Frame( + frame_id="frame1", port=port, frequency=frequency, phase=phase1, is_predefined=False + ) + frame2 = Frame( + frame_id="frame2", port=port, frequency=frequency, phase=phase2, is_predefined=False + ) + pulse_seq = PulseSequence().delay([], 10e-9).swap_phases(frame1, frame2).delay([], 10e-9) + expected_amplitudes = {"frame1": TimeSeries(), "frame2": TimeSeries()} + expected_frequencies = {"frame1": TimeSeries(), "frame2": TimeSeries()} + expected_phases = {"frame1": TimeSeries(), "frame2": TimeSeries()} + + # properties of frame1 before swap + expected_amplitudes["frame1"].put(0, 0).put(9e-9, 0) + expected_frequencies["frame1"].put(0, frequency).put(9e-9, frequency) + expected_phases["frame1"].put(0, phase1).put(9e-9, phase1) + + # properties of frame1 after swap + expected_amplitudes["frame1"].put(10e-9, 0).put(19e-9, 0) + expected_frequencies["frame1"].put(10e-9, frequency).put(19e-9, frequency) + expected_phases["frame1"].put(10e-9, phase2).put(19e-9, phase2) + + # properties of frame2 before swap + expected_amplitudes["frame2"].put(0, 0).put(9e-9, 0) + expected_frequencies["frame2"].put(0, frequency).put(9e-9, frequency) + expected_phases["frame2"].put(0, phase2).put(9e-9, phase2) + + # properties of frame2 after swap + expected_amplitudes["frame2"].put(10e-9, 0).put(19e-9, 0) + expected_frequencies["frame2"].put(10e-9, frequency).put(19e-9, frequency) + expected_phases["frame2"].put(10e-9, phase1).put(19e-9, phase1) + + parser = _ApproximationParser(program=pulse_seq._program, frames=to_dict([frame1, frame2])) + + verify_results(parser, expected_amplitudes, expected_frequencies, expected_phases) + + def test_set_shift_frequency(port): frame = Frame(frame_id="frame1", port=port, frequency=1e8, phase=0, is_predefined=False) pulse_seq = ( @@ -647,6 +693,117 @@ def test_play_drag_gaussian_waveforms(port): verify_results(parser, expected_amplitudes, expected_frequencies, expected_phases) +def test_play_erf_square_waveforms(port): + frame1 = Frame(frame_id="frame1", port=port, frequency=1e8, phase=0, is_predefined=False) + erf_square_wf_ZaE_False = ErfSquareWaveform( + length=1e-8, width=8e-9, sigma=1e-9, off_center=0.0, amplitude=0.8, zero_at_edges=False + ) + pulse_seq = PulseSequence().play(frame1, erf_square_wf_ZaE_False) + + times = np.arange(0, 1e-8, port.dt) + values = np.array( + [ + complex(0.06291968379016318), + complex(0.4000000061669033), + complex(0.7370803285436436), + complex(0.7981289183125405), + complex(0.7999911761342559), + complex(0.8), + complex(0.7999911761342559), + complex(0.7981289183125405), + complex(0.7370803285436436), + complex(0.4000000061669033), + ], + dtype=np.complex128, + ) + + expected_amplitudes = {"frame1": TimeSeries()} + expected_frequencies = {"frame1": TimeSeries()} + expected_phases = {"frame1": TimeSeries()} + + for t, v in zip(times, values): + expected_amplitudes["frame1"].put(t, v) + expected_frequencies["frame1"].put(t, 1e8) + expected_phases["frame1"].put(t, 0) + + parser = _ApproximationParser(program=pulse_seq._program, frames=to_dict(frame1)) + verify_results(parser, expected_amplitudes, expected_frequencies, expected_phases) + + +def test_play_erf_square_waveforms_zero_at_edges(port): + frame1 = Frame(frame_id="frame1", port=port, frequency=1e8, phase=0, is_predefined=False) + erf_square_wf_ZaE_True = ErfSquareWaveform( + length=1e-8, width=8e-9, sigma=1e-9, off_center=0.0, amplitude=0.8, zero_at_edges=True + ) + pulse_seq = PulseSequence().play(frame1, erf_square_wf_ZaE_True) + + times = np.arange(0, 1e-8, port.dt) + values = np.array( + [ + complex(4.819981832973268e-17), + complex(0.36585464564844294), + complex(0.731709291296886), + complex(0.7979691964131336), + complex(0.7999904228990518), + complex(0.8), + complex(0.7999904228990518), + complex(0.7979691964131336), + complex(0.731709291296886), + complex(0.36585464564844294), + ], + dtype=np.complex128, + ) + + expected_amplitudes = {"frame1": TimeSeries()} + expected_frequencies = {"frame1": TimeSeries()} + expected_phases = {"frame1": TimeSeries()} + + for t, v in zip(times, values): + expected_amplitudes["frame1"].put(t, v) + expected_frequencies["frame1"].put(t, 1e8) + expected_phases["frame1"].put(t, 0) + + parser = _ApproximationParser(program=pulse_seq._program, frames=to_dict(frame1)) + verify_results(parser, expected_amplitudes, expected_frequencies, expected_phases) + + +def test_play_erf_square_waveforms_off_center(port): + frame1 = Frame(frame_id="frame1", port=port, frequency=1e8, phase=0, is_predefined=False) + erf_square_wf_ZaE_False = ErfSquareWaveform( + length=1e-8, width=8e-9, sigma=1e-9, off_center=-2e-9, amplitude=0.8, zero_at_edges=False + ) + pulse_seq = PulseSequence().play(frame1, erf_square_wf_ZaE_False) + + times = np.arange(0, 1e-8, port.dt) + values = np.array( + [ + complex(0.7370803285436436), + complex(0.7981289183125405), + complex(0.7999911761342559), + complex(0.8), + complex(0.7999911761342559), + complex(0.7981289183125405), + complex(0.7370803285436436), + complex(0.4000000061669036), + complex(0.0629196837901632), + complex(0.0018710940212660543), + ], + dtype=np.complex128, + ) + + expected_amplitudes = {"frame1": TimeSeries()} + expected_frequencies = {"frame1": TimeSeries()} + expected_phases = {"frame1": TimeSeries()} + + for t, v in zip(times, values): + expected_amplitudes["frame1"].put(t, v) + expected_frequencies["frame1"].put(t, 1e8) + expected_phases["frame1"].put(t, 0) + + parser = _ApproximationParser(program=pulse_seq._program, frames=to_dict(frame1)) + verify_results(parser, expected_amplitudes, expected_frequencies, expected_phases) + + def test_barrier_same_dt(port): frame1 = Frame(frame_id="frame1", port=port, frequency=1e8, phase=0, is_predefined=False) frame2 = Frame(frame_id="frame2", port=port, frequency=1e8, phase=0, is_predefined=False) diff --git a/test/unit_tests/braket/pulse/test_pulse_sequence.py b/test/unit_tests/braket/pulse/test_pulse_sequence.py index da8e0a8a3..fa6eca395 100644 --- a/test/unit_tests/braket/pulse/test_pulse_sequence.py +++ b/test/unit_tests/braket/pulse/test_pulse_sequence.py @@ -18,6 +18,7 @@ ArbitraryWaveform, ConstantWaveform, DragGaussianWaveform, + ErfSquareWaveform, Frame, GaussianWaveform, Port, @@ -81,7 +82,7 @@ def test_pulse_sequence_with_user_defined_frame(user_defined_frame): def test_pulse_sequence_make_bound_pulse_sequence(predefined_frame_1, predefined_frame_2): - param = FreeParameter("a") + 2 * FreeParameter("b") + param = FreeParameter("a") + 2 * FreeParameter("c") pulse_sequence = ( PulseSequence() .set_frequency(predefined_frame_1, param) @@ -119,6 +120,16 @@ def test_pulse_sequence_make_bound_pulse_sequence(predefined_frame_1, predefined predefined_frame_2, ArbitraryWaveform([complex(1, 0.4), 0, 0.3, complex(0.1, 0.2)], id="arb_wf"), ) + .play( + predefined_frame_1, + ErfSquareWaveform( + length=FreeParameter("length_es"), + width=FreeParameter("width_es"), + sigma=2e-9, + off_center=8e-9, + id="erf_square_wf", + ), + ) .capture_v0(predefined_frame_2) ) expected_str_unbound = "\n".join( @@ -135,20 +146,23 @@ def test_pulse_sequence_make_bound_pulse_sequence(predefined_frame_1, predefined " sigma_dg * 1s, 0.2, 1, false);", " waveform constant_wf = constant(length_c * 1s, 2.0 + 0.3im);", " waveform arb_wf = {1.0 + 0.4im, 0, 0.3, 0.1 + 0.2im};", - " set_frequency(predefined_frame_1, a + 2.0 * b);", - " shift_frequency(predefined_frame_1, a + 2.0 * b);", - " set_phase(predefined_frame_1, a + 2.0 * b);", - " shift_phase(predefined_frame_1, -1.0 * a + -2.0 * b);", - " set_scale(predefined_frame_1, a + 2.0 * b);", + " waveform erf_square_wf = erf_square(length_es * 1s, width_es * 1s, 2.0ns," + " 8.0ns, 1, false);", + " set_frequency(predefined_frame_1, a + 2.0 * c);", + " shift_frequency(predefined_frame_1, a + 2.0 * c);", + " set_phase(predefined_frame_1, a + 2.0 * c);", + " shift_phase(predefined_frame_1, -1.0 * a + -2.0 * c);", + " set_scale(predefined_frame_1, a + 2.0 * c);", " psb[0] = capture_v0(predefined_frame_1);", - " delay[(a + 2.0 * b) * 1s] predefined_frame_1, predefined_frame_2;", - " delay[(a + 2.0 * b) * 1s] predefined_frame_1;", + " delay[(a + 2.0 * c) * 1s] predefined_frame_1, predefined_frame_2;", + " delay[(a + 2.0 * c) * 1s] predefined_frame_1;", " delay[1.0ms] predefined_frame_1;", " barrier predefined_frame_1, predefined_frame_2;", " play(predefined_frame_1, gauss_wf);", " play(predefined_frame_2, drag_gauss_wf);", " play(predefined_frame_1, constant_wf);", " play(predefined_frame_2, arb_wf);", + " play(predefined_frame_1, erf_square_wf);", " psb[1] = capture_v0(predefined_frame_2);", "}", ] @@ -156,17 +170,35 @@ def test_pulse_sequence_make_bound_pulse_sequence(predefined_frame_1, predefined assert pulse_sequence.to_ir() == expected_str_unbound assert pulse_sequence.parameters == { FreeParameter("a"), - FreeParameter("b"), + FreeParameter("c"), FreeParameter("length_g"), FreeParameter("length_dg"), FreeParameter("sigma_g"), FreeParameter("sigma_dg"), FreeParameter("length_c"), + FreeParameter("width_es"), + FreeParameter("length_es"), } b_bound = pulse_sequence.make_bound_pulse_sequence( - {"b": 2, "length_g": 1e-3, "length_dg": 3e-3, "sigma_dg": 0.4, "length_c": 4e-3} + { + "c": 2, + "length_g": 1e-3, + "length_dg": 3e-3, + "sigma_dg": 0.4, + "length_c": 4e-3, + "length_es": 20e-9, + "width_es": 12e-9, + } + ) + b_bound_call = pulse_sequence( + c=2, + length_g=1e-3, + length_dg=3e-3, + sigma_dg=0.4, + length_c=4e-3, + length_es=20e-9, + width_es=12e-9, ) - b_bound_call = pulse_sequence(b=2, length_g=1e-3, length_dg=3e-3, sigma_dg=0.4, length_c=4e-3) expected_str_b_bound = "\n".join( [ "OPENQASM 3.0;", @@ -177,6 +209,7 @@ def test_pulse_sequence_make_bound_pulse_sequence(predefined_frame_1, predefined " waveform drag_gauss_wf = drag_gaussian(3.0ms, 400.0ms, 0.2, 1, false);", " waveform constant_wf = constant(4.0ms, 2.0 + 0.3im);", " waveform arb_wf = {1.0 + 0.4im, 0, 0.3, 0.1 + 0.2im};", + " waveform erf_square_wf = erf_square(20.0ns, 12.0ns, 2.0ns, 8.0ns, 1, false);", " set_frequency(predefined_frame_1, a + 4.0);", " shift_frequency(predefined_frame_1, a + 4.0);", " set_phase(predefined_frame_1, a + 4.0);", @@ -191,6 +224,7 @@ def test_pulse_sequence_make_bound_pulse_sequence(predefined_frame_1, predefined " play(predefined_frame_2, drag_gauss_wf);", " play(predefined_frame_1, constant_wf);", " play(predefined_frame_2, arb_wf);", + " play(predefined_frame_1, erf_square_wf);", " psb[1] = capture_v0(predefined_frame_2);", "}", ] @@ -209,6 +243,7 @@ def test_pulse_sequence_make_bound_pulse_sequence(predefined_frame_1, predefined " waveform drag_gauss_wf = drag_gaussian(3.0ms, 400.0ms, 0.2, 1, false);", " waveform constant_wf = constant(4.0ms, 2.0 + 0.3im);", " waveform arb_wf = {1.0 + 0.4im, 0, 0.3, 0.1 + 0.2im};", + " waveform erf_square_wf = erf_square(20.0ns, 12.0ns, 2.0ns, 8.0ns, 1, false);", " set_frequency(predefined_frame_1, 5.0);", " shift_frequency(predefined_frame_1, 5.0);", " set_phase(predefined_frame_1, 5.0);", @@ -223,6 +258,7 @@ def test_pulse_sequence_make_bound_pulse_sequence(predefined_frame_1, predefined " play(predefined_frame_2, drag_gauss_wf);", " play(predefined_frame_1, constant_wf);", " play(predefined_frame_2, arb_wf);", + " play(predefined_frame_1, erf_square_wf);", " psb[1] = capture_v0(predefined_frame_2);", "}", ] @@ -302,6 +338,16 @@ def test_pulse_sequence_to_ir(predefined_frame_1, predefined_frame_2): predefined_frame_2, ArbitraryWaveform([complex(1, 0.4), 0, 0.3, complex(0.1, 0.2)], id="arb_wf"), ) + .play( + predefined_frame_1, + ErfSquareWaveform( + length=32e-9, + width=20e-9, + sigma=2e-9, + off_center=8e-9, + id="erf_square_wf", + ), + ) .capture_v0(predefined_frame_2) ) expected_str = "\n".join( @@ -313,6 +359,7 @@ def test_pulse_sequence_to_ir(predefined_frame_1, predefined_frame_2): " waveform drag_gauss_wf = drag_gaussian(3.0ms, 400.0ms, 0.2, 1, false);", " waveform constant_wf = constant(4.0ms, 2.0 + 0.3im);", " waveform arb_wf = {1.0 + 0.4im, 0, 0.3, 0.1 + 0.2im};", + " waveform erf_square_wf = erf_square(32.0ns, 20.0ns, 2.0ns, 8.0ns, 1, false);", " set_frequency(predefined_frame_1, 3000000000.0);", " shift_frequency(predefined_frame_1, 1000000000.0);", " set_phase(predefined_frame_1, -0.5);", @@ -328,6 +375,7 @@ def test_pulse_sequence_to_ir(predefined_frame_1, predefined_frame_2): " play(predefined_frame_2, drag_gauss_wf);", " play(predefined_frame_1, constant_wf);", " play(predefined_frame_2, arb_wf);", + " play(predefined_frame_1, erf_square_wf);", " psb[1] = capture_v0(predefined_frame_2);", "}", ] diff --git a/test/unit_tests/braket/pulse/test_waveforms.py b/test/unit_tests/braket/pulse/test_waveforms.py index 34f989c0b..5654762fa 100644 --- a/test/unit_tests/braket/pulse/test_waveforms.py +++ b/test/unit_tests/braket/pulse/test_waveforms.py @@ -20,7 +20,13 @@ from oqpy import Program from braket.circuits.free_parameter import FreeParameter -from braket.pulse import ArbitraryWaveform, ConstantWaveform, DragGaussianWaveform, GaussianWaveform +from braket.pulse import ( + ArbitraryWaveform, + ConstantWaveform, + DragGaussianWaveform, + ErfSquareWaveform, + GaussianWaveform, +) from braket.pulse.ast.qasm_parser import ast_to_qasm from braket.pulse.waveforms import _parse_waveform_from_calibration_schema @@ -294,6 +300,102 @@ def test_gaussian_wf_free_params(): _assert_wf_qasm(wf_3, "waveform gauss_wf = gaussian(600.0ms, 300.0ms, 0.1, false);") +def test_erf_square_waveform(): + length = 4e-9 + width = 0.3 + sigma = 0.2 + off_center = 1e-9 + amplitude = 0.4 + zero_at_edges = False + id = "erf_square_wf" + wf = ErfSquareWaveform(length, width, sigma, off_center, amplitude, zero_at_edges, id) + assert wf.id == id + assert wf.zero_at_edges == zero_at_edges + assert wf.amplitude == amplitude + assert wf.width == width + assert wf.sigma == sigma + assert wf.length == length + assert wf.off_center == off_center + + +def test_erf_square_waveform_repr(): + length = 4e-9 + width = 0.3 + sigma = 0.2 + off_center = 1e-9 + amplitude = 0.4 + zero_at_edges = False + id = "erf_square_wf" + wf = ErfSquareWaveform(length, width, sigma, off_center, amplitude, zero_at_edges, id) + repr(wf) + + +def test_erf_square_waveform_default_params(): + length = 4e-9 + width = 0.3 + sigma = 0.2 + off_center = 1e-9 + wf = ErfSquareWaveform(length, width, sigma, off_center) + assert re.match(r"[A-Za-z]{10}", wf.id) + assert wf.zero_at_edges is False + assert wf.amplitude == 1 + assert wf.width == width + assert wf.sigma == sigma + assert wf.length == length + assert wf.off_center == off_center + + +def test_erf_square_wf_eq(): + wf = ErfSquareWaveform(4e-9, 0.3, 0.2, 1e-9, 0.7, True, "wf_es") + wf_2 = ErfSquareWaveform( + wf.length, wf.width, wf.sigma, wf.off_center, wf.amplitude, wf.zero_at_edges, wf.id + ) + assert wf_2 == wf + for att in ["length", "width", "sigma", "off_center", "amplitude", "zero_at_edges", "id"]: + wfc = deepcopy(wf_2) + setattr(wfc, att, "wrong_value") + assert wf != wfc + + +def test_erf_square_wf_free_params(): + wf = ErfSquareWaveform( + FreeParameter("length_v"), + FreeParameter("width_x"), + FreeParameter("sigma_y"), + FreeParameter("off_center_x"), + FreeParameter("amp_z"), + id="erf_square_wf", + ) + assert wf.parameters == [ + FreeParameter("length_v"), + FreeParameter("width_x"), + FreeParameter("sigma_y"), + FreeParameter("off_center_x"), + FreeParameter("amp_z"), + ] + + wf_2 = wf.bind_values(length_v=0.6, width_x=0.4) + assert wf_2.parameters == [ + 0.6, + 0.4, + FreeParameter("sigma_y"), + FreeParameter("off_center_x"), + FreeParameter("amp_z"), + ] + _assert_wf_qasm( + wf_2, + "waveform erf_square_wf = erf_square(600.0ms, 400.0ms, sigma_y * 1s, off_center_x * 1s," + " amp_z, false);", + ) + + wf_3 = wf.bind_values(length_v=0.6, width_x=0.3, sigma_y=0.1, off_center_x=0.05) + assert wf_3.parameters == [0.6, 0.3, 0.1, 0.05, FreeParameter("amp_z")] + _assert_wf_qasm( + wf_3, + "waveform erf_square_wf = erf_square(600.0ms, 300.0ms, 100.0ms, 50.0ms, amp_z, false);", + ) + + def _assert_wf_qasm(waveform, expected_qasm): p = Program(None) p.declare(waveform._to_oqpy_expression()) @@ -357,6 +459,27 @@ def _assert_wf_qasm(waveform, expected_qasm): }, ConstantWaveform(id="wf_constant", length=2.1, iq=0.23), ), + ( + { + "waveformId": "wf_erf_square_0", + "name": "erf_square", + "arguments": [ + {"name": "length", "value": 6.000000000000001e-8, "type": "float"}, + {"name": "width", "value": 3.000000000000000e-8, "type": "float"}, + {"name": "sigma", "value": 5.000000000060144e-9, "type": "float"}, + {"name": "off_center", "value": 4.000000000000000e-9, "type": "float"}, + {"name": "amplitude", "value": 0.4549282253548838, "type": "float"}, + ], + }, + ErfSquareWaveform( + id="wf_erf_square_0", + length=6.000000000000001e-8, + width=3.000000000000000e-8, + sigma=5.000000000060144e-9, + off_center=4.000000000000000e-9, + amplitude=0.4549282253548838, + ), + ), ], ) def test_parse_waveform_from_calibration_schema(waveform_json, waveform): diff --git a/test/unit_tests/braket/tasks/test_gate_model_quantum_task_result.py b/test/unit_tests/braket/tasks/test_gate_model_quantum_task_result.py index 5447bd8b2..99ab7e345 100644 --- a/test/unit_tests/braket/tasks/test_gate_model_quantum_task_result.py +++ b/test/unit_tests/braket/tasks/test_gate_model_quantum_task_result.py @@ -26,6 +26,7 @@ ResultTypeValue, TaskMetadata, ) +from braket.task_result.iqm_metadata_v1 import IqmMetadata from braket.task_result.oqc_metadata_v1 import OqcMetadata from braket.task_result.rigetti_metadata_v1 import RigettiMetadata from braket.tasks import GateModelQuantumTaskResult @@ -103,6 +104,22 @@ def additional_metadata_rigetti(quil_program): return AdditionalMetadata(action=program, rigettiMetadata=rigetti_metadata) +@pytest.fixture +def additional_metadata_iqm(): + source = """ + OPENQASM 3.0; + bit[2] b; + h $0; + cnot $0, $7; + b[0] = measure $0; + b[1] = measure $7; + """ + program = openqasm.Program(source=source) + iqm_metadata = IqmMetadata(compiledProgram=source) + + return AdditionalMetadata(action=program, iqmMetadata=iqm_metadata) + + @pytest.fixture def qasm2_program(): return """ @@ -160,6 +177,13 @@ def result_oqc(result_obj_1, additional_metadata_oqc): return result +@pytest.fixture +def result_iqm(result_obj_1, additional_metadata_iqm): + result = GateModelQuantumTaskResult.from_object(result_obj_1) + result.additional_metadata = additional_metadata_iqm + return result + + @pytest.fixture def result_str_1(result_obj_1): return result_obj_1.json() @@ -323,6 +347,11 @@ def test_get_compiled_circuit_oqc(result_oqc, qasm2_program): assert result_oqc.get_compiled_circuit() == qasm2_program +def test_get_compiled_circuit_iqm(result_iqm): + """Test get_compiled_circuit method.""" + assert result_iqm.get_compiled_circuit() == result_iqm.additional_metadata.action.source + + def test_get_compiled_circuit_no_qhp_metadata(result_obj_1): """Test get_compiled_circuit method.""" result = GateModelQuantumTaskResult.from_object(result_obj_1) diff --git a/tox.ini b/tox.ini index 95c862106..f8da75234 100644 --- a/tox.ini +++ b/tox.ini @@ -107,11 +107,9 @@ commands = basepython = python3 deps = {[test-deps]deps} - sphinx - sphinx-rtd-theme - sphinxcontrib-apidoc commands = sphinx-build -E -T -b html doc build/documentation/html -j auto +extras = docs [testenv:serve-docs] basepython = python3