From e35eb840290f02f1b8a6769e34c27b62c52911c3 Mon Sep 17 00:00:00 2001 From: Phillip Weinberg Date: Mon, 27 May 2024 13:11:56 -0400 Subject: [PATCH 1/5] adding custom service backend for submitting quera tasks. --- src/bloqade/ir/routine/quera.py | 99 ++++++++++++++++++++++++++++++++- 1 file changed, 97 insertions(+), 2 deletions(-) diff --git a/src/bloqade/ir/routine/quera.py b/src/bloqade/ir/routine/quera.py index c818caef5..b0318a044 100644 --- a/src/bloqade/ir/routine/quera.py +++ b/src/bloqade/ir/routine/quera.py @@ -1,4 +1,4 @@ -from collections import OrderedDict +from collections import OrderedDict, namedtuple from pydantic.v1.dataclasses import dataclass import json @@ -10,8 +10,9 @@ from bloqade.task.batch import RemoteBatch from bloqade.task.quera import QuEraTask -from beartype.typing import Tuple, Union, Optional +from beartype.typing import Tuple, Union, Optional, NamedTuple, List from beartype import beartype +from requests import Response, request @dataclass(frozen=True, config=__pydantic_dataclass_config__) @@ -41,6 +42,100 @@ def mock( backend = MockBackend(state_file=state_file, submission_error=submission_error) return QuEraHardwareRoutine(self.source, self.circuit, self.params, backend) + @property + def custom(self) -> "CustomSubmissionRoutine": + return CustomSubmissionRoutine(self.source, self.circuit, self.params) + + +@dataclass(frozen=True, config=__pydantic_dataclass_config__) +class CustomSubmissionRoutine(RoutineBase): + def _compile( + self, + shots: int, + args: Tuple[LiteralType, ...] = (), + ): + from bloqade.compiler.passes.hardware import ( + analyze_channels, + canonicalize_circuit, + assign_circuit, + validate_waveforms, + generate_ahs_code, + generate_quera_ir, + ) + from bloqade.factory import get_capabilities + + circuit, params = self.circuit, self.params + capabilities = get_capabilities() + + for batch_params in params.batch_assignments(*args): + assignments = {**batch_params, **params.static_params} + final_circuit, metadata = assign_circuit(circuit, assignments) + + level_couplings = analyze_channels(final_circuit) + final_circuit = canonicalize_circuit(final_circuit, level_couplings) + + validate_waveforms(level_couplings, final_circuit) + ahs_components = generate_ahs_code( + capabilities, level_couplings, final_circuit + ) + + task_ir = generate_quera_ir(ahs_components, shots).discretize(capabilities) + + MetaData = namedtuple("MetaData", metadata.keys()) + + yield MetaData(**metadata), task_ir + + def submit( + self, + shots: int, + url: str, + json_body_template: str, + method: str = "POST", + args: Tuple[LiteralType] = (), + ) -> List[Tuple[NamedTuple, Response]]: + """Compile to QuEraTaskSpecification and submit to a custom service. + + Args: + shots (int): number of shots + url (str): url of the custom service + json_body_template (str): json body template, must contain '{task_ir}' + to be replaced by QuEraTaskSpecification + method (str): http method to be used. Defaults to "POST". + args (Tuple[LiteralType]): additional arguments to be passed into the + compiler coming from `args` option of the build. Defaults to (). + + Returns: + List[Tuple[NamedTuple, Response]]: List of parameters for each batch in + the task and the response from the post request. + + Examples: + + Here is a simple example of how to use this method. + + ```python + >>> body_template = "{"token": "my_token", "task": {task_ir}}" + >>> responses = ( + program.quera.custom.submit( + 100, + "http://my_custom_service.com", + body_template + ) + ) + ``` + """ + + if r"{task_ir}" not in json_body_template: + raise ValueError(r"body_template must contain '{task_ir}'") + + out = [] + for metadata, task_ir in self._compile(shots, args): + response = request( + method, url, json=json_body_template.format(task_ir=task_ir.json()) + ) + out.append((metadata, response)) + + return out + @dataclass(frozen=True, config=__pydantic_dataclass_config__) class QuEraHardwareRoutine(RoutineBase): From 4b7ae1345deb77b1c546a21d6ec83df15ed5d506 Mon Sep 17 00:00:00 2001 From: Phillip Weinberg Date: Mon, 27 May 2024 13:19:27 -0400 Subject: [PATCH 2/5] allowing more options into the request. --- src/bloqade/ir/routine/quera.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/bloqade/ir/routine/quera.py b/src/bloqade/ir/routine/quera.py index b0318a044..31f75cd79 100644 --- a/src/bloqade/ir/routine/quera.py +++ b/src/bloqade/ir/routine/quera.py @@ -10,7 +10,7 @@ from bloqade.task.batch import RemoteBatch from bloqade.task.quera import QuEraTask -from beartype.typing import Tuple, Union, Optional, NamedTuple, List +from beartype.typing import Tuple, Union, Optional, NamedTuple, List, Dict, Any from beartype import beartype from requests import Response, request @@ -92,6 +92,7 @@ def submit( json_body_template: str, method: str = "POST", args: Tuple[LiteralType] = (), + request_options: Dict[str, Any] = {}, ) -> List[Tuple[NamedTuple, Response]]: """Compile to QuEraTaskSpecification and submit to a custom service. @@ -103,6 +104,8 @@ def submit( method (str): http method to be used. Defaults to "POST". args (Tuple[LiteralType]): additional arguments to be passed into the compiler coming from `args` option of the build. Defaults to (). + **request_options: additional options to be passed into the request method, + Note the `json` option will be overwritten by the `json_body_template`. Returns: List[Tuple[NamedTuple, Response]]: List of parameters for each batch in @@ -129,9 +132,10 @@ def submit( out = [] for metadata, task_ir in self._compile(shots, args): - response = request( - method, url, json=json_body_template.format(task_ir=task_ir.json()) + request_options.update( + json=json_body_template.format(task_ir=task_ir.json()) ) + response = request(method, url, **request_options) out.append((metadata, response)) return out From 154bd2bb25fe8b74107978a0f9eb7a58ea6c54ad Mon Sep 17 00:00:00 2001 From: Phillip Weinberg Date: Mon, 27 May 2024 21:15:41 -0400 Subject: [PATCH 3/5] fix docs. --- src/bloqade/ir/routine/quera.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/bloqade/ir/routine/quera.py b/src/bloqade/ir/routine/quera.py index 31f75cd79..57fc80c6a 100644 --- a/src/bloqade/ir/routine/quera.py +++ b/src/bloqade/ir/routine/quera.py @@ -112,8 +112,7 @@ def submit( the task and the response from the post request. Examples: - - Here is a simple example of how to use this method. + Here is a simple example of how to use this method. ```python >>> body_template = "{"token": "my_token", "task": {task_ir}}" From 6ea71a96171c911a0641da7f790b672da6fe6dd0 Mon Sep 17 00:00:00 2001 From: Phillip Weinberg Date: Wed, 29 May 2024 13:26:09 -0400 Subject: [PATCH 4/5] enabling routing from builder and adding some better error messages and docs. adding happy path test. --- src/bloqade/builder/backend/quera.py | 11 +++++++ src/bloqade/ir/routine/quera.py | 37 +++++++++++++++++------ tests/test_costum_submission.py | 45 ++++++++++++++++++++++++++++ 3 files changed, 84 insertions(+), 9 deletions(-) create mode 100644 tests/test_costum_submission.py diff --git a/src/bloqade/builder/backend/quera.py b/src/bloqade/builder/backend/quera.py index 208789797..9ed1f7cb8 100644 --- a/src/bloqade/builder/backend/quera.py +++ b/src/bloqade/builder/backend/quera.py @@ -126,3 +126,14 @@ def mock(self, state_file: str = ".mock_state.txt", submission_error: bool = Fal return self.parse().quera.mock( state_file=state_file, submission_error=submission_error ) + + def custom(self): + """ + Specify custom backend + + Return: + CustomSubmissionRoutine + + """ + + return self.parse().quera.custom() diff --git a/src/bloqade/ir/routine/quera.py b/src/bloqade/ir/routine/quera.py index 57fc80c6a..1f3f35f7c 100644 --- a/src/bloqade/ir/routine/quera.py +++ b/src/bloqade/ir/routine/quera.py @@ -1,4 +1,5 @@ from collections import OrderedDict, namedtuple +import time from pydantic.v1.dataclasses import dataclass import json @@ -42,7 +43,6 @@ def mock( backend = MockBackend(state_file=state_file, submission_error=submission_error) return QuEraHardwareRoutine(self.source, self.circuit, self.params, backend) - @property def custom(self) -> "CustomSubmissionRoutine": return CustomSubmissionRoutine(self.source, self.circuit, self.params) @@ -62,7 +62,7 @@ def _compile( generate_ahs_code, generate_quera_ir, ) - from bloqade.factory import get_capabilities + from bloqade.submission.capabilities import get_capabilities circuit, params = self.circuit, self.params capabilities = get_capabilities() @@ -80,7 +80,6 @@ def _compile( ) task_ir = generate_quera_ir(ahs_components, shots).discretize(capabilities) - MetaData = namedtuple("MetaData", metadata.keys()) yield MetaData(**metadata), task_ir @@ -93,6 +92,7 @@ def submit( method: str = "POST", args: Tuple[LiteralType] = (), request_options: Dict[str, Any] = {}, + sleep_time: float = 0.1, ) -> List[Tuple[NamedTuple, Response]]: """Compile to QuEraTaskSpecification and submit to a custom service. @@ -100,22 +100,28 @@ def submit( shots (int): number of shots url (str): url of the custom service json_body_template (str): json body template, must contain '{task_ir}' + which is a placeholder for a string representation of the task ir. + The task ir string will be inserted into the template with + `json_body_template.format(task_ir=task_ir_string)`. to be replaced by QuEraTaskSpecification method (str): http method to be used. Defaults to "POST". args (Tuple[LiteralType]): additional arguments to be passed into the compiler coming from `args` option of the build. Defaults to (). - **request_options: additional options to be passed into the request method, - Note the `json` option will be overwritten by the `json_body_template`. + request_options: additional options to be passed into the request method, + Note the `data` option will be overwritten by the + `json_body_template.format(task_ir=task_ir_string)`. + sleep_time (float): time to sleep between each request. Defaults to 0.1. Returns: List[Tuple[NamedTuple, Response]]: List of parameters for each batch in the task and the response from the post request. Examples: - Here is a simple example of how to use this method. + Here is a simple example of how to use this method. Note the body_template + has double curly braces on the outside to escape the string formatting. ```python - >>> body_template = "{"token": "my_token", "task": {task_ir}}" + >>> body_template = "{{"token": "my_token", "task": {task_ir}}}" >>> responses = ( program.quera.custom.submit( 100, @@ -129,13 +135,26 @@ def submit( if r"{task_ir}" not in json_body_template: raise ValueError(r"body_template must contain '{task_ir}'") + partial_eval = json_body_template.format(task_ir='"task_ir"') + try: + _ = json.loads(partial_eval) + except json.JSONDecodeError as e: + raise ValueError( + "body_template must be a valid json template. " + 'When evaluating template with task_ir="task_ir", ' + f"the template evaluated to: {partial_eval!r}.\n" + f"JSONDecodeError: {e}" + ) + out = [] for metadata, task_ir in self._compile(shots, args): - request_options.update( - json=json_body_template.format(task_ir=task_ir.json()) + json_request_body = json_body_template.format( + task_ir=task_ir.json(exclude_none=True, exclude_unset=True) ) + request_options.update(data=json_request_body) response = request(method, url, **request_options) out.append((metadata, response)) + time.sleep(sleep_time) return out diff --git a/tests/test_costum_submission.py b/tests/test_costum_submission.py new file mode 100644 index 000000000..968b17f5a --- /dev/null +++ b/tests/test_costum_submission.py @@ -0,0 +1,45 @@ +from bloqade import start +from requests import Response +from typing import Dict, List, Union +import simplejson as json + +import bloqade.ir.routine.quera # noqa: F401 +from unittest.mock import patch + + +def create_response( + status_code: int, content: Union[Dict, List[Dict]], content_type="application/json" +) -> Response: + response = Response() + response.status_code = status_code + response.headers["Content-Type"] = content_type + response._content = bytes(json.dumps(content, use_decimal=True), "utf-8") + return response + + +@patch("bloqade.ir.routine.quera.request") +def test_custom_submission(request_mock): + body_template = "{{'token': 'token', 'body':{task_ir}}}" + + task_ids = ["1", "2", "3"] + request_mock.side_effect = [ + create_response(200, {"task_id": task_id}) for task_id in task_ids + ] + + # build bloqade program + program = ( + start.add_position((0, 0)) + .rydberg.rabi.amplitude.uniform.piecewise_linear( + [0.1, "time", 0.1], [0, 15, 15, 0] + ) + .batch_assign(time=[0.0, 0.1, 0.5]) + ) + + # submit and get responses and meta data associated with each task in the batch + responses = program.quera.custom().submit( + 100, "https://my_service.test", body_template + ) + + for task_id, (metadata, response) in zip(task_ids, responses): + response_json = response.json() + assert response_json["task_id"] == task_id From bf75b3a8713e277b264cfac36b49a60f55bc5363 Mon Sep 17 00:00:00 2001 From: Phillip Weinberg Date: Wed, 29 May 2024 15:24:35 -0400 Subject: [PATCH 5/5] fixing tests. --- tests/test_costum_submission.py | 47 ++++++++++++++++++++++++++++++++- 1 file changed, 46 insertions(+), 1 deletion(-) diff --git a/tests/test_costum_submission.py b/tests/test_costum_submission.py index 968b17f5a..e01e0d60c 100644 --- a/tests/test_costum_submission.py +++ b/tests/test_costum_submission.py @@ -5,6 +5,7 @@ import bloqade.ir.routine.quera # noqa: F401 from unittest.mock import patch +import pytest def create_response( @@ -19,7 +20,7 @@ def create_response( @patch("bloqade.ir.routine.quera.request") def test_custom_submission(request_mock): - body_template = "{{'token': 'token', 'body':{task_ir}}}" + body_template = '{{"token": "token", "body":{task_ir}}}' task_ids = ["1", "2", "3"] request_mock.side_effect = [ @@ -43,3 +44,47 @@ def test_custom_submission(request_mock): for task_id, (metadata, response) in zip(task_ids, responses): response_json = response.json() assert response_json["task_id"] == task_id + + +@patch("bloqade.ir.routine.quera.request") +def test_custom_submission_error_missing_task_ir_key(request_mock): + body_template = '{{"token": "token", "body":}}' + + task_ids = ["1", "2", "3"] + request_mock.side_effect = [ + create_response(200, {"task_id": task_id}) for task_id in task_ids + ] + + # build bloqade program + program = ( + start.add_position((0, 0)) + .rydberg.rabi.amplitude.uniform.piecewise_linear( + [0.1, "time", 0.1], [0, 15, 15, 0] + ) + .batch_assign(time=[0.0, 0.1, 0.5]) + ) + with pytest.raises(ValueError): + # submit and get responses and meta data associated with each task in the batch + program.quera.custom().submit(100, "https://my_service.test", body_template) + + +@patch("bloqade.ir.routine.quera.request") +def test_custom_submission_error_malformed_template(request_mock): + body_template = '{{"token": token", "body":}}' + + task_ids = ["1", "2", "3"] + request_mock.side_effect = [ + create_response(200, {"task_id": task_id}) for task_id in task_ids + ] + + # build bloqade program + program = ( + start.add_position((0, 0)) + .rydberg.rabi.amplitude.uniform.piecewise_linear( + [0.1, "time", 0.1], [0, 15, 15, 0] + ) + .batch_assign(time=[0.0, 0.1, 0.5]) + ) + with pytest.raises(ValueError): + # submit and get responses and meta data associated with each task in the batch + program.quera.custom().submit(100, "https://my_service.test", body_template)