From e5e846b88c1dca13b5cba9d0969c7dc9811bee39 Mon Sep 17 00:00:00 2001 From: Benjamin MacLellan Date: Wed, 26 Feb 2025 20:04:49 -0500 Subject: [PATCH 01/11] Use uv in docker build, add new dependencies --- docker/.dockerignore | 92 ++++++++++++++++++++++++++++++++++++++ docker/Dockerfile | 51 +++++++-------------- docker/docker-compose.yaml | 37 ++++++++++++--- docker/requirements.txt | 3 -- pyproject.toml | 10 ++++- src/oqd_cloud/client.py | 2 +- src/oqd_cloud/provider.py | 2 +- 7 files changed, 150 insertions(+), 47 deletions(-) create mode 100644 docker/.dockerignore diff --git a/docker/.dockerignore b/docker/.dockerignore new file mode 100644 index 0000000..a12670d --- /dev/null +++ b/docker/.dockerignore @@ -0,0 +1,92 @@ +# Git +.git +.gitignore +.gitattributes + + +# CI +.codeclimate.yml +.travis.yml +.taskcluster.yml + +# Docker +docker-compose.yml +Dockerfile +.docker +.dockerignore + +# Byte-compiled / optimized / DLL files +**/__pycache__/ +**/*.py[cod] + +# C extensions +*.so + +# Distribution / packaging +.Python +env/ +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +*.egg-info/ +.installed.cfg +*.egg + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.coverage +.cache +nosetests.xml +coverage.xml + +# Translations +*.mo +*.pot + +# Django stuff: +*.log + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ + +# Virtual environment +.env +.venv/ +venv/ + +# PyCharm +.idea + +# Python mode for VIM +.ropeproject +**/.ropeproject + +# Vim swap files +**/*.swp + +# VS Code +.vscode/ + + +.venv \ No newline at end of file diff --git a/docker/Dockerfile b/docker/Dockerfile index c659f77..b1ffa9e 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -1,62 +1,41 @@ -FROM python:3.10.13-slim-bookworm as build - +FROM python:3.12-slim-bookworm as build +COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/ RUN apt-get update && \ apt-get upgrade -y && \ apt-get install -y --no-install-recommends build-essential gcc && \ - apt-get install -y git # for installing directly from git - -ARG PIP_DISABLE_PIP_VERSION_CHECK=1 -ARG PIP_NO_CACHE_DIR=1 + apt-get install -y git # for installing directly from git WORKDIR /python - -RUN python -m venv /python/venv - -ENV PATH="/python/venv/bin:$PATH" - +RUN uv venv /python/.venv +ENV PATH="/python/.venv/bin:$PATH" COPY docker/requirements.txt . -RUN pip install -r requirements.txt -RUN pip install oqd-compiler-infrastructure -RUN pip install oqd-core -RUN pip install oqd-analog-emulator +RUN uv pip install -r requirements.txt +RUN uv pip install "oqd-analog-emulator@git+https://github.com/openquantumdesign/oqd-analog-emulator" +RUN uv pip install "oqd-trical@git+https://github.com/openquantumdesign/oqd-trical" ######################################################################################## -FROM python:3.10.13-slim-bookworm as app +FROM python:3.12-slim-bookworm as app +COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/ RUN apt update && \ apt install -y --no-install-recommends supervisor && \ apt-get install -y gcc g++ # needed from Cython +COPY --from=build /python/.venv /python/.venv -ARG PIP_DISABLE_PIP_VERSION_CHECK=1 -ARG PIP_NO_CACHE_DIR=1 - -COPY --from=build /python/venv /python/venv - -ENV PATH="/python/venv/bin:$PATH" +ENV PATH="/python/.venv/bin:$PATH" ENV PYTHONPATH="/app/src" COPY . ./app WORKDIR /app -RUN pip install . -#RUN pip install .[all] +RUN uv pip install .[server] -#COPY ./supervisord.conf /etc/supervisor/conf.d/supervisord.conf +# COPY ./supervisord.conf /etc/supervisor/conf.d/supervisord.conf COPY ./docker/supervisord.conf /etc/supervisor/conf.d/supervisord.conf CMD ["/usr/bin/supervisord", "-c", "/etc/supervisor/conf.d/supervisord.conf"] -######################################################################################## - -# RUN \ -# apt update \ -# && apt install wget -y \ -# && wget https://julialang-s3.julialang.org/bin/linux/x64/1.9/julia-1.9.3-linux-x86_64.tar.gz -P /opt \ -# && tar zxvf /opt/julia-1.9.3-linux-x86_64.tar.gz -C /opt - -# ENV PATH "$PATH:/opt/julia-1.9.3/bin" - -# RUN julia -e 'using Pkg; Pkg.add(["QuantumOptics", "Configurations", "StatsBase", "DataStructures", "JSON3", "IonSim"])' +######################################################################################## \ No newline at end of file diff --git a/docker/docker-compose.yaml b/docker/docker-compose.yaml index e52f159..4e2d091 100644 --- a/docker/docker-compose.yaml +++ b/docker/docker-compose.yaml @@ -5,10 +5,39 @@ version: "3.9" networks: internal: name: oqd-cloud-server-internal + # minionetwork: + # driver: bridge + + +volumes: + redis_volume: + driver: local + postgres_volume: + driver: local + minio_volume: + driver: local + ################################################################################ services: + minio: + image: docker.io/bitnami/minio:2022 + container_name: oqd-cloud-server-minio + ports: + - '9000:9000' + - '9001:9001' + networks: + # - minionetwork + internal: + volumes: + - 'minio_volume:/data' + environment: + - MINIO_ROOT_USER=${MINIO_ROOT_USER} + - MINIO_ROOT_PASSWORD=${REDIS_PASSWORD} + - MINIO_DEFAULT_BUCKETS=${REDIS_BUCKET_NAME} + + redis: image: redis container_name: oqd-cloud-server-redis @@ -70,6 +99,9 @@ services: JWT_ALGORITHM: ${JWT_ALGORITHM} JWT_ACCESS_TOKEN_EXPIRE_MINUTES: ${JWT_ACCESS_TOKEN_EXPIRE_MINUTES} RQ_WORKERS: 4 + MINIO_ROOT_USER: ${MINIO_ROOT_USER} + MINIO_ROOT_PASSWORD: ${MINIO_ROOT_PASSWORD} + MINIO_DEFAULT_BUCKETS: ${MINIO_DEFAULT_BUCKETS} ports: - "8000:8000" networks: @@ -80,8 +112,3 @@ services: postgres: condition: service_healthy -volumes: - redis_volume: - driver: local - postgres_volume: - driver: local diff --git a/docker/requirements.txt b/docker/requirements.txt index 0468d34..38eb276 100644 --- a/docker/requirements.txt +++ b/docker/requirements.txt @@ -1,7 +1,4 @@ -qutip~=5.0.1 -numpy~=1.0 pydantic>=2.4 - sqlalchemy fastapi redis diff --git a/pyproject.toml b/pyproject.toml index 7518e11..859bb61 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -49,10 +49,18 @@ server = [ "python-jose", "passlib", "python-multipart", + "pydantic>=2.4", + "sqlalchemy", + "fastapi", + "redis", + "rq", + "python-dotenv", + "minio", # "oqd-compiler-infrastructure@git+https://github.com/openquantumdesign/oqd-compiler-infrastructure", "oqd-core@git+https://github.com/openquantumdesign/oqd-core", - "oqd-compiler-infrastructure@git+https://github.com/openquantumdesign/oqd-analog-emulator", + "oqd-analog-emulator@git+https://github.com/openquantumdesign/oqd-analog-emulator", + "oqd-trical@git+https://github.com/openquantumdesign/oqd-trical", ] diff --git a/src/oqd_cloud/client.py b/src/oqd_cloud/client.py index 7a2467e..117fb66 100644 --- a/src/oqd_cloud/client.py +++ b/src/oqd_cloud/client.py @@ -52,7 +52,7 @@ class Client: user="user", password="password" ) - job = client.submit_job(task=task, backend="analog-qutip") + job = client.submit_job(task=task, backend="oqd-analog-emulator") ``` """ diff --git a/src/oqd_cloud/provider.py b/src/oqd_cloud/provider.py index 8bc583e..dfbefdd 100644 --- a/src/oqd_cloud/provider.py +++ b/src/oqd_cloud/provider.py @@ -29,7 +29,7 @@ def available_backends(self): return self._available_backends else: return [ - "analog-qutip", + "oqd-analog-emulator", ] @property From 655c289889afb9c2e3687c12b77fac4c95d07294 Mon Sep 17 00:00:00 2001 From: Benjamin MacLellan Date: Wed, 26 Feb 2025 20:31:51 -0500 Subject: [PATCH 02/11] Remove requirements.txt, add to pyproject.toml instead, simplfiy Docker --- pyproject.toml | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 859bb61..48b6ecb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -29,7 +29,7 @@ classifiers = [ dependencies = [ "requests", "pydantic>=2.4", - "oqd-core@git+https://github.com/openquantumdesign/oqd-core", + "oqd-core@git+https://github.com/OpenQuantumDesign/oqd-core", ] [project.optional-dependencies] @@ -57,10 +57,8 @@ server = [ "python-dotenv", "minio", # - "oqd-compiler-infrastructure@git+https://github.com/openquantumdesign/oqd-compiler-infrastructure", - "oqd-core@git+https://github.com/openquantumdesign/oqd-core", - "oqd-analog-emulator@git+https://github.com/openquantumdesign/oqd-analog-emulator", - "oqd-trical@git+https://github.com/openquantumdesign/oqd-trical", + "oqd-analog-emulator@git+https://github.com/OpenQuantumDesign/oqd-analog-emulator", + "oqd-trical@git+https://github.com/OpenQuantumDesign/oqd-trical", ] From 05ea1f7aa0d6f21bec8e01f29b41ed59e7489db0 Mon Sep 17 00:00:00 2001 From: Benjamin MacLellan Date: Wed, 26 Feb 2025 22:31:34 -0500 Subject: [PATCH 03/11] Add route for getting available backends --- .gitignore | 3 +- docker/Dockerfile | 33 ++++----------- docker/docker-compose.yaml | 54 ++++++++++++++++++++---- docker/requirements.txt | 6 --- src/oqd_cloud/client.py | 15 +++++-- src/oqd_cloud/provider.py | 20 ++++++--- src/oqd_cloud/server/model.py | 6 ++- src/oqd_cloud/server/route/job.py | 38 ++++++++++------- tests/test_client.py | 70 ++++++++++++++++--------------- 9 files changed, 145 insertions(+), 100 deletions(-) delete mode 100644 docker/requirements.txt diff --git a/.gitignore b/.gitignore index fafbe0d..874aba0 100644 --- a/.gitignore +++ b/.gitignore @@ -165,4 +165,5 @@ cython_debug/ *.code-workspace .github/workflows/_*.yml -uv.lock \ No newline at end of file +uv.lock +docker/.env copy diff --git a/docker/Dockerfile b/docker/Dockerfile index b1ffa9e..997ea3f 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -3,38 +3,19 @@ COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/ RUN apt-get update && \ apt-get upgrade -y && \ - apt-get install -y --no-install-recommends build-essential gcc && \ - apt-get install -y git # for installing directly from git - -WORKDIR /python -RUN uv venv /python/.venv -ENV PATH="/python/.venv/bin:$PATH" -COPY docker/requirements.txt . - -RUN uv pip install -r requirements.txt - -RUN uv pip install "oqd-analog-emulator@git+https://github.com/openquantumdesign/oqd-analog-emulator" -RUN uv pip install "oqd-trical@git+https://github.com/openquantumdesign/oqd-trical" - -######################################################################################## - -FROM python:3.12-slim-bookworm as app -COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/ - -RUN apt update && \ apt install -y --no-install-recommends supervisor && \ + apt-get install -y git && \ apt-get install -y gcc g++ # needed from Cython -COPY --from=build /python/.venv /python/.venv - -ENV PATH="/python/.venv/bin:$PATH" -ENV PYTHONPATH="/app/src" - COPY . ./app +ENV PYTHONPATH="/app/src" WORKDIR /app -RUN uv pip install .[server] -# COPY ./supervisord.conf /etc/supervisor/conf.d/supervisord.conf +RUN uv venv +ENV PATH=".venv/bin:$PATH" + +RUN uv pip install ".[server]" + COPY ./docker/supervisord.conf /etc/supervisor/conf.d/supervisord.conf CMD ["/usr/bin/supervisord", "-c", "/etc/supervisor/conf.d/supervisord.conf"] diff --git a/docker/docker-compose.yaml b/docker/docker-compose.yaml index 4e2d091..905bddc 100644 --- a/docker/docker-compose.yaml +++ b/docker/docker-compose.yaml @@ -5,9 +5,6 @@ version: "3.9" networks: internal: name: oqd-cloud-server-internal - # minionetwork: - # driver: bridge - volumes: redis_volume: @@ -22,21 +19,22 @@ volumes: services: minio: - image: docker.io/bitnami/minio:2022 + image: minio/minio container_name: oqd-cloud-server-minio + restart: always ports: - '9000:9000' - '9001:9001' + # network_mode: "host" networks: - # - minionetwork internal: volumes: - 'minio_volume:/data' environment: - MINIO_ROOT_USER=${MINIO_ROOT_USER} - - MINIO_ROOT_PASSWORD=${REDIS_PASSWORD} - - MINIO_DEFAULT_BUCKETS=${REDIS_BUCKET_NAME} - + - MINIO_ROOT_PASSWORD=${MINIO_ROOT_PASSWORD} + - MINIO_DEFAULT_BUCKETS=${MINIO_DEFAULT_BUCKETS} + command: server /data --console-address ":9001" redis: image: redis @@ -53,6 +51,7 @@ services: command: ["sh", "-c", "redis-server --requirepass $${REDIS_PASSWORD} --save 20 1 --loglevel notice --appendonly yes --appendfsync everysec"] expose: - "6379" + # network_mode: "host" networks: internal: volumes: @@ -74,11 +73,50 @@ services: retries: 5 expose: - "5432" + # network_mode: "host" networks: internal: volumes: - postgres_volume:/var/lib/postgres/data + + # app: + # image: oqd-cloud-server # Use existing built image + # container_name: oqd-cloud-server + # restart: always + # environment: + # REDIS_HOST: ${REDIS_HOST} + # REDIS_PASSWORD: ${REDIS_PASSWORD} + # POSTGRES_HOST: ${POSTGRES_HOST} + # POSTGRES_USER: ${POSTGRES_USER} + # POSTGRES_DB: ${POSTGRES_DB} + # POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} + # JWT_SECRET_KEY: ${JWT_SECRET_KEY} + # JWT_ALGORITHM: ${JWT_ALGORITHM} + # JWT_ACCESS_TOKEN_EXPIRE_MINUTES: ${JWT_ACCESS_TOKEN_EXPIRE_MINUTES} + # RQ_WORKERS: 4 + # MINIO_ROOT_USER: ${MINIO_ROOT_USER} + # MINIO_ROOT_PASSWORD: ${MINIO_ROOT_PASSWORD} + # MINIO_DEFAULT_BUCKETS: ${MINIO_DEFAULT_BUCKETS} + # ports: + # - "8000:8000" + # networks: + # - internal + # volumes: + # - ../:/app # Bind-mount your code + # working_dir: /app # Set working directory inside container + # command: > + # /bin/sh -c " + # source .venv/bin/activate + # uvicorn main:app --host 0.0.0.0 --port 8000 --reload + # " + # depends_on: + # redis: + # condition: service_healthy + # postgres: + # condition: service_healthy + + app: build: context: ../ diff --git a/docker/requirements.txt b/docker/requirements.txt deleted file mode 100644 index 38eb276..0000000 --- a/docker/requirements.txt +++ /dev/null @@ -1,6 +0,0 @@ -pydantic>=2.4 -sqlalchemy -fastapi -redis -rq -python-dotenv \ No newline at end of file diff --git a/src/oqd_cloud/client.py b/src/oqd_cloud/client.py index 117fb66..b0dc840 100644 --- a/src/oqd_cloud/client.py +++ b/src/oqd_cloud/client.py @@ -13,7 +13,7 @@ # limitations under the License. -from typing import Literal, Optional +from typing import Literal, Optional, Sequence import requests from oqd_core.backend.task import Task @@ -21,7 +21,11 @@ from oqd_cloud.provider import Provider -__all__ = ["Job", "Client"] +__all__ = ["Job", "Client", "Backends"] + + +class Backends(BaseModel): + available: Sequence[str] class Job(BaseModel): @@ -129,13 +133,18 @@ def connect(self, provider: Provider, username: str, password: str): # self.connect(self, self.provider) # pass - def submit_job(self, task: Task, backend: Literal["analog-qutip",]): + def submit_job( + self, + task: Task, + backend: str + ): """Submit a Task as an AnalogCircuit, DigitalCircuit, or AtomicCircuit to a backend.""" response = requests.post( self.provider.job_submission_url(backend=backend), json=task.model_dump(), headers=self.authorization_header, ) + print(response) job = Job.model_validate(response.json()) if response.status_code == 200: diff --git a/src/oqd_cloud/provider.py b/src/oqd_cloud/provider.py index dfbefdd..86be27a 100644 --- a/src/oqd_cloud/provider.py +++ b/src/oqd_cloud/provider.py @@ -12,6 +12,8 @@ # See the License for the specific language governing permissions and # limitations under the License. +import requests + class Provider: def __init__(self, url: str = "http://localhost:8000"): @@ -25,12 +27,18 @@ def __init__(self, url: str = "http://localhost:8000"): @property def available_backends(self): # todo: get available backends from url - if hasattr(self, "_available_backends"): - return self._available_backends - else: - return [ - "oqd-analog-emulator", - ] + response = requests.post( + self.url + "/available_backends" + ) + if response.status_code == 200: + return response['backends'] + + # if hasattr(self, "_available_backends"): + # return self._available_backends + # else: + # return [ + # "oqd-analog-emulator", + # ] @property def registration_url(self): diff --git a/src/oqd_cloud/server/model.py b/src/oqd_cloud/server/model.py index 9a2212d..98ed279 100644 --- a/src/oqd_cloud/server/model.py +++ b/src/oqd_cloud/server/model.py @@ -12,7 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from typing import Optional +from typing import Optional, Sequence from pydantic import BaseModel, ConfigDict @@ -44,3 +44,7 @@ class Job(BaseModel): status: str result: Optional[str] = None user_id: str + + +class Backends(BaseModel): + available: Sequence[str] \ No newline at end of file diff --git a/src/oqd_cloud/server/route/job.py b/src/oqd_cloud/server/route/job.py index dd6175e..76d6531 100644 --- a/src/oqd_cloud/server/route/job.py +++ b/src/oqd_cloud/server/route/job.py @@ -18,7 +18,8 @@ from fastapi import status as http_status ######################################################################################## -from oqd_analog_emulator.qutip_backend import QutipBackend +import oqd_analog_emulator #.qutip_backend import QutipBackend +import oqd_trical from oqd_core.backend.task import Task from rq.job import Callback from rq.job import Job as RQJob @@ -32,38 +33,43 @@ report_stopped, report_success, ) -from oqd_cloud.server.model import Job +from oqd_cloud.server.model import Job, Backends from oqd_cloud.server.route.auth import user_dependency ######################################################################################## -job_router = APIRouter(tags=["Job"]) +# _backends = ["oqd-analog-emulator-qutip", "oqd-trical-qutip", "oqd-trical-dynamiqs"] +_backends = { + "oqd-analog-emulator-qutip": oqd_analog_emulator.qutip_backend.QutipBackend(), + "oqd-trical-qutip": oqd_trical.backend.qutip.QutipBackend(), + "oqd-trical-dynamiqs": oqd_trical.backend.dynamiqs.DynamiqsBackend(), +} +backends = Backends(available=list(_backends.keys())) +job_router = APIRouter(tags=["Job"]) +@job_router.post("/available_backends", tags=["Job"]) +async def available_backends(): + return backends + + @job_router.post("/submit/{backend}", tags=["Job"]) async def submit_job( task: Task, - backend: Literal["analog-qutip",], + # backend: Literal["oqd-analog-emulator-qutip", "oqd-trical-qutip", "oqd-trical-dynamiqs"], + backend: Literal[tuple(backends.available)], user: user_dependency, db: db_dependency, ): print(task) print(f"Queueing {task} on server {backend} backend. {len(queue)} jobs in queue.") - backends = { - "analog-qutip": QutipBackend(), - # "tensorcircuit": TensorCircuitBackend() - } - # backends_run = { - # "analog-qutip": lambda task: backends["analog-qutip"].run(task=task) + # backends = { + # "oqd-analog-emulator-qutip": oqd_analog_emulator.qutip_backend.QutipBackend(), + # "oqd-trical-qutip": oqd_trical.backend.qutip.QutipBackend(), + # "oqd-trical-dynamiqs": oqd_trical.backend.dynamiqs.DynamiqsBackend(), # } - if backend == "analog-qutip": - try: - expt, args = backends[backend].compile(task=task) - except Exception: - raise Exception("Cannot properly compile to the QutipBackend.") - job = queue.enqueue( backends[backend].run, task, diff --git a/tests/test_client.py b/tests/test_client.py index 1312220..4d0e419 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -14,10 +14,10 @@ # %% -import matplotlib.pyplot as plt -import numpy as np -import seaborn as sns -from oqd_analog_emulator.qutip_backend import QutipBackend +# import matplotlib.pyplot as plt +# import numpy as np +# import seaborn as sns +# from oqd_analog_emulator.qutip_backend import QutipBackend from oqd_core.backend.metric import Expectation from oqd_core.backend.task import Task, TaskArgsAnalog from oqd_core.interface.analog.operation import AnalogCircuit, AnalogGate @@ -70,46 +70,46 @@ task.model_dump_json() # %% -backend = QutipBackend() -expt, args = backend.compile(task=task) +# backend = QutipBackend() +# expt, args = backend.compile(task=task) # results = backend.run(experiment=expt, args=args) -a = {"experiment": expt, "args": args} -results = backend.run(task=task) +# a = {"experiment": expt, "args": args} +# results = backend.run(task=task) # %% -fig, ax = plt.subplots(1, 1, figsize=[6, 3]) -colors = sns.color_palette(palette="crest", n_colors=4) +# fig, ax = plt.subplots(1, 1, figsize=[6, 3]) +# colors = sns.color_palette(palette="crest", n_colors=4) -for k, (name, metric) in enumerate(results.metrics.items()): - ax.plot(results.times, metric, label=f"$\\langle {name} \\rangle$", color=colors[k]) -ax.legend() -# plt.show() +# for k, (name, metric) in enumerate(results.metrics.items()): +# ax.plot(results.times, metric, label=f"$\\langle {name} \\rangle$", color=colors[k]) +# ax.legend() +# # plt.show() -# %% -fig, axs = plt.subplots(4, 1, sharex=True, figsize=[5, 9]) +# # %% +# fig, axs = plt.subplots(4, 1, sharex=True, figsize=[5, 9]) -state = np.array([basis.real + 1j * basis.imag for basis in results.state]) -bases = ["0", "1"] -counts = {basis: results.counts.get(basis, 0) for basis in bases} +# state = np.array([basis.real + 1j * basis.imag for basis in results.state]) +# bases = ["0", "1"] +# counts = {basis: results.counts.get(basis, 0) for basis in bases} -ax = axs[0] -ax.bar(x=bases, height=np.abs(state) ** 2, color=colors[0]) -ax.set(ylabel="Probability") +# ax = axs[0] +# ax.bar(x=bases, height=np.abs(state) ** 2, color=colors[0]) +# ax.set(ylabel="Probability") -ax = axs[1] -ax.bar(x=bases, height=list(counts.values()), color=colors[1]) -ax.set(ylabel="Count") +# ax = axs[1] +# ax.bar(x=bases, height=list(counts.values()), color=colors[1]) +# ax.set(ylabel="Count") -ax = axs[2] -ax.bar(x=bases, height=state.real, color=colors[2]) -ax.set(ylabel="Amplitude (real)") +# ax = axs[2] +# ax.bar(x=bases, height=state.real, color=colors[2]) +# ax.set(ylabel="Amplitude (real)") -ax = axs[3] -ax.bar(x=bases, height=state.imag, color=colors[3]) -ax.set(xlabel="Basis state", ylabel="Amplitude (imag)", ylim=[-np.pi, np.pi]) +# ax = axs[3] +# ax.bar(x=bases, height=state.imag, color=colors[3]) +# ax.set(xlabel="Basis state", ylabel="Amplitude (imag)", ylim=[-np.pi, np.pi]) -# plt.show() +# # plt.show() # %% client = Client() @@ -117,9 +117,13 @@ client.connect(provider=provider, username="ben", password="pwd") client.status_report +#%% +backends = provider.available_backends +print(backends) + # %% print(client.jobs) -job = client.submit_job(task=task, backend="analog-qutip") +job = client.submit_job(task=task, backend="oqd-analog-emulator-qutip") # %% client.retrieve_job(job_id=job.job_id) From f5ebb6b07e73c8fef9af53f58ab10797b3b6159e Mon Sep 17 00:00:00 2001 From: Benjamin MacLellan Date: Wed, 26 Feb 2025 22:51:49 -0500 Subject: [PATCH 04/11] Get available backends from server route, add atomic JSON example --- src/oqd_cloud/provider.py | 20 ++- tests/atomic.json | 266 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 275 insertions(+), 11 deletions(-) create mode 100644 tests/atomic.json diff --git a/src/oqd_cloud/provider.py b/src/oqd_cloud/provider.py index 86be27a..d6a60f1 100644 --- a/src/oqd_cloud/provider.py +++ b/src/oqd_cloud/provider.py @@ -13,6 +13,7 @@ # limitations under the License. import requests +from oqd_cloud.server.model import Backends class Provider: @@ -24,22 +25,19 @@ def __init__(self, url: str = "http://localhost:8000"): """ self.url = url - @property - def available_backends(self): - # todo: get available backends from url + # get available backends + self.backends = Backends(available=[]) response = requests.post( self.url + "/available_backends" ) + backends = Backends.model_validate(response.json()) if response.status_code == 200: - return response['backends'] - - # if hasattr(self, "_available_backends"): - # return self._available_backends - # else: - # return [ - # "oqd-analog-emulator", - # ] + self.backends = backends + @property + def available_backends(self): + return self.backends.available + @property def registration_url(self): return self.url + "/auth/register" diff --git a/tests/atomic.json b/tests/atomic.json new file mode 100644 index 0000000..a2e3b90 --- /dev/null +++ b/tests/atomic.json @@ -0,0 +1,266 @@ +{ + "class_": "AtomicCircuit", + "system": { + "class_": "System", + "ions": [ + { + "class_": "Ion", + "mass": 171.0, + "charge": 1.0, + "levels": [ + { + "class_": "Level", + "label": "q0", + "principal": 6, + "spin": 0.5, + "orbital": 0.0, + "nuclear": 0.5, + "spin_orbital": 0.5, + "spin_orbital_nuclear": 0.0, + "spin_orbital_nuclear_magnetization": 0.0, + "energy": 0.0 + }, + { + "class_": "Level", + "label": "q1", + "principal": 6, + "spin": 0.5, + "orbital": 0.0, + "nuclear": 0.5, + "spin_orbital": 0.5, + "spin_orbital_nuclear": 1.0, + "spin_orbital_nuclear_magnetization": 0.0, + "energy": 62.83185307179586 + }, + { + "class_": "Level", + "label": "e0", + "principal": 5, + "spin": 0.5, + "orbital": 1.0, + "nuclear": 0.5, + "spin_orbital": 0.5, + "spin_orbital_nuclear": 1.0, + "spin_orbital_nuclear_magnetization": -1.0, + "energy": 628.3185307179587 + }, + { + "class_": "Level", + "label": "e1", + "principal": 5, + "spin": 0.5, + "orbital": 1.0, + "nuclear": 0.5, + "spin_orbital": 0.5, + "spin_orbital_nuclear": 1.0, + "spin_orbital_nuclear_magnetization": 1.0, + "energy": 691.1503837897545 + } + ], + "transitions": [ + { + "class_": "Transition", + "label": "q0->e0", + "level1": { + "class_": "Level", + "label": "q0", + "principal": 6, + "spin": 0.5, + "orbital": 0.0, + "nuclear": 0.5, + "spin_orbital": 0.5, + "spin_orbital_nuclear": 0.0, + "spin_orbital_nuclear_magnetization": 0.0, + "energy": 0.0 + }, + "level2": { + "class_": "Level", + "label": "e0", + "principal": 5, + "spin": 0.5, + "orbital": 1.0, + "nuclear": 0.5, + "spin_orbital": 0.5, + "spin_orbital_nuclear": 1.0, + "spin_orbital_nuclear_magnetization": -1.0, + "energy": 628.3185307179587 + }, + "einsteinA": 1.0, + "multipole": "E1" + }, + { + "class_": "Transition", + "label": "q0->e1", + "level1": { + "class_": "Level", + "label": "q0", + "principal": 6, + "spin": 0.5, + "orbital": 0.0, + "nuclear": 0.5, + "spin_orbital": 0.5, + "spin_orbital_nuclear": 0.0, + "spin_orbital_nuclear_magnetization": 0.0, + "energy": 0.0 + }, + "level2": { + "class_": "Level", + "label": "e1", + "principal": 5, + "spin": 0.5, + "orbital": 1.0, + "nuclear": 0.5, + "spin_orbital": 0.5, + "spin_orbital_nuclear": 1.0, + "spin_orbital_nuclear_magnetization": 1.0, + "energy": 691.1503837897545 + }, + "einsteinA": 1.0, + "multipole": "E1" + }, + { + "class_": "Transition", + "label": "q1->e0", + "level1": { + "class_": "Level", + "label": "q1", + "principal": 6, + "spin": 0.5, + "orbital": 0.0, + "nuclear": 0.5, + "spin_orbital": 0.5, + "spin_orbital_nuclear": 1.0, + "spin_orbital_nuclear_magnetization": 0.0, + "energy": 62.83185307179586 + }, + "level2": { + "class_": "Level", + "label": "e0", + "principal": 5, + "spin": 0.5, + "orbital": 1.0, + "nuclear": 0.5, + "spin_orbital": 0.5, + "spin_orbital_nuclear": 1.0, + "spin_orbital_nuclear_magnetization": -1.0, + "energy": 628.3185307179587 + }, + "einsteinA": 1.0, + "multipole": "E1" + }, + { + "class_": "Transition", + "label": "q1->e1", + "level1": { + "class_": "Level", + "label": "q1", + "principal": 6, + "spin": 0.5, + "orbital": 0.0, + "nuclear": 0.5, + "spin_orbital": 0.5, + "spin_orbital_nuclear": 1.0, + "spin_orbital_nuclear_magnetization": 0.0, + "energy": 62.83185307179586 + }, + "level2": { + "class_": "Level", + "label": "e1", + "principal": 5, + "spin": 0.5, + "orbital": 1.0, + "nuclear": 0.5, + "spin_orbital": 0.5, + "spin_orbital_nuclear": 1.0, + "spin_orbital_nuclear_magnetization": 1.0, + "energy": 691.1503837897545 + }, + "einsteinA": 1.0, + "multipole": "E1" + } + ], + "position": [ + 0.0, + 0.0, + 0.0 + ] + } + ], + "modes": [ + { + "class_": "Phonon", + "energy": 0.1, + "eigenvector": [ + 1.0, + 0.0, + 0.0 + ] + } + ] + }, + "protocol": { + "class_": "SequentialProtocol", + "sequence": [ + { + "class_": "Pulse", + "beam": { + "class_": "Beam", + "transition": { + "class_": "Transition", + "label": "q0->e0", + "level1": { + "class_": "Level", + "label": "q0", + "principal": 6, + "spin": 0.5, + "orbital": 0.0, + "nuclear": 0.5, + "spin_orbital": 0.5, + "spin_orbital_nuclear": 0.0, + "spin_orbital_nuclear_magnetization": 0.0, + "energy": 0.0 + }, + "level2": { + "class_": "Level", + "label": "e0", + "principal": 5, + "spin": 0.5, + "orbital": 1.0, + "nuclear": 0.5, + "spin_orbital": 0.5, + "spin_orbital_nuclear": 1.0, + "spin_orbital_nuclear_magnetization": -1.0, + "energy": 628.3185307179587 + }, + "einsteinA": 1.0, + "multipole": "E1" + }, + "rabi": { + "class_": "MathNum", + "value": 6.283185307179586 + }, + "detuning": { + "class_": "MathNum", + "value": 0 + }, + "phase": { + "class_": "MathNum", + "value": 0 + }, + "polarization": [ + 1.0, + 0.0, + 0.0 + ], + "wavevector": [ + 0.0, + 1.0, + 0.0 + ], + "target": 0 + }, + "duration": 10.0 + } + ] + } + } \ No newline at end of file From 6c9eecc1763017a26fc867b7c279f1027c1cfa41 Mon Sep 17 00:00:00 2001 From: Benjamin MacLellan Date: Thu, 27 Feb 2025 09:53:39 -0500 Subject: [PATCH 05/11] Set up local development --- docker/docker-compose.yaml | 94 ++++++++++++++++-------------- pyproject.toml | 16 +++++ src/oqd_cloud/provider.py | 3 +- src/oqd_cloud/server/main.py | 2 +- src/oqd_cloud/server/route/job.py | 2 +- src/oqd_cloud/server/route/user.py | 2 +- tests/test_client.py | 34 ++++++++++- 7 files changed, 104 insertions(+), 49 deletions(-) diff --git a/docker/docker-compose.yaml b/docker/docker-compose.yaml index 905bddc..184085b 100644 --- a/docker/docker-compose.yaml +++ b/docker/docker-compose.yaml @@ -20,7 +20,7 @@ volumes: services: minio: image: minio/minio - container_name: oqd-cloud-server-minio + container_name: minio restart: always ports: - '9000:9000' @@ -38,7 +38,7 @@ services: redis: image: redis - container_name: oqd-cloud-server-redis + container_name: redis restart: always healthcheck: test: ["CMD-SHELL", "redis-cli -a $${REDIS_PASSWORD} --raw incr _docker_healthcheck"] @@ -49,8 +49,10 @@ services: environment: REDIS_PASSWORD: ${REDIS_PASSWORD} # Replace command: ["sh", "-c", "redis-server --requirepass $${REDIS_PASSWORD} --save 20 1 --loglevel notice --appendonly yes --appendfsync everysec"] - expose: - - "6379" + ports: + - "6379:6379" + # expose: + # - "6379" # network_mode: "host" networks: internal: @@ -59,7 +61,7 @@ services: postgres: image: postgres - container_name: oqd-cloud-server-postgres + container_name: postgres restart: always environment: POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} # Replace @@ -71,8 +73,10 @@ services: interval: 5s timeout: 25s retries: 5 - expose: - - "5432" + # expose: + # - "5432" + ports: + - "5432:5432" # network_mode: "host" networks: internal: @@ -105,11 +109,13 @@ services: # volumes: # - ../:/app # Bind-mount your code # working_dir: /app # Set working directory inside container - # command: > - # /bin/sh -c " - # source .venv/bin/activate - # uvicorn main:app --host 0.0.0.0 --port 8000 --reload - # " + # # command: uv run src/oqd_cloud/server/main.py + # command: uv run src/oqd_cloud/server/main.py + # # command: > + # # /bin/sh -c " + # # source .venv/bin/activate + # # uvicorn main:app --host 0.0.0.0 --port 8000 --reload + # # " # depends_on: # redis: # condition: service_healthy @@ -117,36 +123,36 @@ services: # condition: service_healthy - app: - build: - context: ../ - dockerfile: docker/Dockerfile - args: - GITHUB_TOKEN: ${GITHUB_TOKEN} - image: oqd-cloud-server - container_name: oqd-cloud-server - restart: always - environment: - REDIS_HOST: ${REDIS_HOST} - REDIS_PASSWORD: ${REDIS_PASSWORD} - POSTGRES_HOST: ${POSTGRES_HOST} - POSTGRES_USER: ${POSTGRES_USER} - POSTGRES_DB: ${POSTGRES_DB} - POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} - JWT_SECRET_KEY: ${JWT_SECRET_KEY} - JWT_ALGORITHM: ${JWT_ALGORITHM} - JWT_ACCESS_TOKEN_EXPIRE_MINUTES: ${JWT_ACCESS_TOKEN_EXPIRE_MINUTES} - RQ_WORKERS: 4 - MINIO_ROOT_USER: ${MINIO_ROOT_USER} - MINIO_ROOT_PASSWORD: ${MINIO_ROOT_PASSWORD} - MINIO_DEFAULT_BUCKETS: ${MINIO_DEFAULT_BUCKETS} - ports: - - "8000:8000" - networks: - internal: - depends_on: - redis: - condition: service_healthy - postgres: - condition: service_healthy + # app: + # build: + # context: ../ + # dockerfile: docker/Dockerfile + # args: + # GITHUB_TOKEN: ${GITHUB_TOKEN} + # image: oqd-cloud-server + # container_name: oqd-cloud-server + # restart: always + # environment: + # REDIS_HOST: ${REDIS_HOST} + # REDIS_PASSWORD: ${REDIS_PASSWORD} + # POSTGRES_HOST: ${POSTGRES_HOST} + # POSTGRES_USER: ${POSTGRES_USER} + # POSTGRES_DB: ${POSTGRES_DB} + # POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} + # JWT_SECRET_KEY: ${JWT_SECRET_KEY} + # JWT_ALGORITHM: ${JWT_ALGORITHM} + # JWT_ACCESS_TOKEN_EXPIRE_MINUTES: ${JWT_ACCESS_TOKEN_EXPIRE_MINUTES} + # RQ_WORKERS: 4 + # MINIO_ROOT_USER: ${MINIO_ROOT_USER} + # MINIO_ROOT_PASSWORD: ${MINIO_ROOT_PASSWORD} + # MINIO_DEFAULT_BUCKETS: ${MINIO_DEFAULT_BUCKETS} + # ports: + # - "8000:8000" + # networks: + # internal: + # depends_on: + # redis: + # condition: service_healthy + # postgres: + # condition: service_healthy diff --git a/pyproject.toml b/pyproject.toml index 48b6ecb..4297849 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -30,6 +30,22 @@ dependencies = [ "requests", "pydantic>=2.4", "oqd-core@git+https://github.com/OpenQuantumDesign/oqd-core", + # + "asyncpg", + "uvicorn", + "python-jose", + "passlib", + "python-multipart", + "pydantic>=2.4", + "sqlalchemy", + "fastapi", + "redis", + "rq", + "python-dotenv", + "minio", + # + "oqd-analog-emulator@git+https://github.com/OpenQuantumDesign/oqd-analog-emulator", + "oqd-trical@git+https://github.com/OpenQuantumDesign/oqd-trical", ] [project.optional-dependencies] diff --git a/src/oqd_cloud/provider.py b/src/oqd_cloud/provider.py index d6a60f1..f569bef 100644 --- a/src/oqd_cloud/provider.py +++ b/src/oqd_cloud/provider.py @@ -17,12 +17,13 @@ class Provider: - def __init__(self, url: str = "http://localhost:8000"): + def __init__(self, host: str = "http://localhost", port: int = 8000): """ Args: url: URL for the server """ + url = f"{host}:{port}" self.url = url # get available backends diff --git a/src/oqd_cloud/server/main.py b/src/oqd_cloud/server/main.py index 29465e2..4d1a22e 100644 --- a/src/oqd_cloud/server/main.py +++ b/src/oqd_cloud/server/main.py @@ -15,4 +15,4 @@ if __name__ == "__main__": import uvicorn - uvicorn.run("app:app", host="0.0.0.0", port=8000) + uvicorn.run("app:app", host="0.0.0.0", port=8007) diff --git a/src/oqd_cloud/server/route/job.py b/src/oqd_cloud/server/route/job.py index 76d6531..e4d90ed 100644 --- a/src/oqd_cloud/server/route/job.py +++ b/src/oqd_cloud/server/route/job.py @@ -71,7 +71,7 @@ async def submit_job( # } job = queue.enqueue( - backends[backend].run, + _backends[backend].run, task, on_success=Callback(report_success), on_failure=Callback(report_failure), diff --git a/src/oqd_cloud/server/route/user.py b/src/oqd_cloud/server/route/user.py index c752503..8ceb431 100644 --- a/src/oqd_cloud/server/route/user.py +++ b/src/oqd_cloud/server/route/user.py @@ -21,7 +21,7 @@ from oqd_cloud.server.database import JobInDB, UserInDB, db_dependency from oqd_cloud.server.model import ( Job, # todo: proper import - UserRegistrationForm, # , Job + UserRegistrationForm, ) ######################################################################################## diff --git a/tests/test_client.py b/tests/test_client.py index 4d0e419..981cb8e 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -23,6 +23,8 @@ from oqd_core.interface.analog.operation import AnalogCircuit, AnalogGate from oqd_core.interface.analog.operator import PauliX, PauliZ +from oqd_core.interface.atomic.circuit import AtomicCircuit + from oqd_cloud.client import Client from oqd_cloud.provider import Provider @@ -113,7 +115,7 @@ # %% client = Client() -provider = Provider() +provider = Provider(port=8007) client.connect(provider=provider, username="ben", password="pwd") client.status_report @@ -127,3 +129,33 @@ # %% client.retrieve_job(job_id=job.job_id) + +# %% + +with open("./tests/atomic.json", "r") as f: + circuit = AtomicCircuit.model_validate_json(f.read()) + +print(circuit) + +# %% +from minio import Minio +import os + +os.getenv("127.0.0.1:9000") + +client = Minio( + "127.0.0.1:9000", + access_key="admin", + secret_key="password", + secure=False +) + +# %% +source_file = "./tests/atomic.json" +bucket_name = "oqd-cloud-bucket" +destination_file = "my-test-file.txt" + +client.fput_object( + bucket_name, destination_file, source_file, +) +# %% From 1bcb57de11b626599ad426eed00a7b3584552f24 Mon Sep 17 00:00:00 2001 From: Benjamin MacLellan Date: Thu, 27 Feb 2025 21:34:12 -0500 Subject: [PATCH 06/11] Ad tags as body arg for submit_job route --- src/oqd_cloud/client.py | 6 +++- src/oqd_cloud/provider.py | 2 +- src/oqd_cloud/server/database.py | 1 + src/oqd_cloud/server/main.py | 2 +- src/oqd_cloud/server/model.py | 3 +- src/oqd_cloud/server/route/job.py | 27 +++++++++------- src/oqd_cloud/server/storage.py | 50 +++++++++++++++++++++++++++++ tests/test_client.py | 52 ++----------------------------- 8 files changed, 77 insertions(+), 66 deletions(-) create mode 100644 src/oqd_cloud/server/storage.py diff --git a/src/oqd_cloud/client.py b/src/oqd_cloud/client.py index b0dc840..cd44c0c 100644 --- a/src/oqd_cloud/client.py +++ b/src/oqd_cloud/client.py @@ -16,6 +16,7 @@ from typing import Literal, Optional, Sequence import requests +import json from oqd_core.backend.task import Task from pydantic import BaseModel, ConfigDict @@ -41,6 +42,7 @@ class Job(BaseModel): status: str result: Optional[str] = None user_id: str + # tags: Optional[Sequence[str]] class Client: @@ -136,12 +138,14 @@ def connect(self, provider: Provider, username: str, password: str): def submit_job( self, task: Task, - backend: str + backend: str, + tags: Sequence[str] ): """Submit a Task as an AnalogCircuit, DigitalCircuit, or AtomicCircuit to a backend.""" response = requests.post( self.provider.job_submission_url(backend=backend), json=task.model_dump(), + # json={"task": task.model_dump(), "tags": tags}, headers=self.authorization_header, ) print(response) diff --git a/src/oqd_cloud/provider.py b/src/oqd_cloud/provider.py index f569bef..7415b2a 100644 --- a/src/oqd_cloud/provider.py +++ b/src/oqd_cloud/provider.py @@ -28,7 +28,7 @@ def __init__(self, host: str = "http://localhost", port: int = 8000): # get available backends self.backends = Backends(available=[]) - response = requests.post( + response = requests.get( self.url + "/available_backends" ) backends = Backends.model_validate(response.json()) diff --git a/src/oqd_cloud/server/database.py b/src/oqd_cloud/server/database.py index 751e819..a6ff01b 100644 --- a/src/oqd_cloud/server/database.py +++ b/src/oqd_cloud/server/database.py @@ -65,6 +65,7 @@ class JobInDB(Base): backend: Mapped[str] status: Mapped[str] result: Mapped[Optional[str]] + tags: Mapped[Optional[str]] user_id: Mapped[int] = mapped_column(ForeignKey("users.user_id")) user: Mapped["UserInDB"] = relationship(back_populates="jobs") diff --git a/src/oqd_cloud/server/main.py b/src/oqd_cloud/server/main.py index 4d1a22e..41b47f7 100644 --- a/src/oqd_cloud/server/main.py +++ b/src/oqd_cloud/server/main.py @@ -15,4 +15,4 @@ if __name__ == "__main__": import uvicorn - uvicorn.run("app:app", host="0.0.0.0", port=8007) + uvicorn.run("app:app", host="0.0.0.0", port=8007, log_level='debug') diff --git a/src/oqd_cloud/server/model.py b/src/oqd_cloud/server/model.py index 98ed279..c7eefc1 100644 --- a/src/oqd_cloud/server/model.py +++ b/src/oqd_cloud/server/model.py @@ -44,7 +44,8 @@ class Job(BaseModel): status: str result: Optional[str] = None user_id: str - + # tags: Optional[Sequence[str]] + class Backends(BaseModel): available: Sequence[str] \ No newline at end of file diff --git a/src/oqd_cloud/server/route/job.py b/src/oqd_cloud/server/route/job.py index e4d90ed..7f7d2de 100644 --- a/src/oqd_cloud/server/route/job.py +++ b/src/oqd_cloud/server/route/job.py @@ -12,11 +12,12 @@ # See the License for the specific language governing permissions and # limitations under the License. -from typing import Literal +from typing import Literal, Sequence, Optional from fastapi import APIRouter, HTTPException from fastapi import status as http_status - +import logging +logger = logging.getLogger('uvicorn.error') ######################################################################################## import oqd_analog_emulator #.qutip_backend import QutipBackend import oqd_trical @@ -38,7 +39,6 @@ ######################################################################################## -# _backends = ["oqd-analog-emulator-qutip", "oqd-trical-qutip", "oqd-trical-dynamiqs"] _backends = { "oqd-analog-emulator-qutip": oqd_analog_emulator.qutip_backend.QutipBackend(), "oqd-trical-qutip": oqd_trical.backend.qutip.QutipBackend(), @@ -48,7 +48,8 @@ job_router = APIRouter(tags=["Job"]) -@job_router.post("/available_backends", tags=["Job"]) + +@job_router.get("/available_backends") async def available_backends(): return backends @@ -56,20 +57,21 @@ async def available_backends(): @job_router.post("/submit/{backend}", tags=["Job"]) async def submit_job( task: Task, - # backend: Literal["oqd-analog-emulator-qutip", "oqd-trical-qutip", "oqd-trical-dynamiqs"], backend: Literal[tuple(backends.available)], + # tags: Optional[Sequence[str]], user: user_dependency, db: db_dependency, ): print(task) print(f"Queueing {task} on server {backend} backend. {len(queue)} jobs in queue.") - - # backends = { - # "oqd-analog-emulator-qutip": oqd_analog_emulator.qutip_backend.QutipBackend(), - # "oqd-trical-qutip": oqd_trical.backend.qutip.QutipBackend(), - # "oqd-trical-dynamiqs": oqd_trical.backend.dynamiqs.DynamiqsBackend(), - # } - + # logger.debug(f'debug message {tags}') + + _backends = { + "oqd-analog-emulator-qutip": oqd_analog_emulator.qutip_backend.QutipBackend(), + "oqd-trical-qutip": oqd_trical.backend.qutip.QutipBackend(), + "oqd-trical-dynamiqs": oqd_trical.backend.dynamiqs.DynamiqsBackend(), + } + job = queue.enqueue( _backends[backend].run, task, @@ -84,6 +86,7 @@ async def submit_job( backend=backend, status=job.get_status(), result=None, + # tags=tags, user_id=user.user_id, ) db.add(job_in_db) diff --git a/src/oqd_cloud/server/storage.py b/src/oqd_cloud/server/storage.py new file mode 100644 index 0000000..2143d97 --- /dev/null +++ b/src/oqd_cloud/server/storage.py @@ -0,0 +1,50 @@ +# Copyright 2024-2025 Open Quantum Design + +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at + +# http://www.apache.org/licenses/LICENSE-2.0 + +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from typing import Literal +from minio import Minio +import os + +from fastapi import APIRouter, HTTPException +from fastapi import status as http_status + +######################################################################################## +import oqd_analog_emulator #.qutip_backend import QutipBackend +import oqd_trical +from oqd_core.backend.task import Task +from rq.job import Callback +from rq.job import Job as RQJob +from sqlalchemy import select + +from oqd_cloud.server.database import JobInDB, db_dependency +from oqd_cloud.server.model import Job, Backends +from oqd_cloud.server.route.auth import user_dependency + +######################################################################################## + +minio_client = Minio( + "127.0.0.1:9000", + access_key="admin", + secret_key="password", + secure=False +) + +# %% +source_file = "./tests/atomic.json" +bucket_name = "oqd-cloud-bucket" +destination_file = "my-test-file.txt" + +minio_client.fput_object( + bucket_name, destination_file, source_file, +) \ No newline at end of file diff --git a/tests/test_client.py b/tests/test_client.py index 981cb8e..4072212 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -28,7 +28,6 @@ from oqd_cloud.client import Client from oqd_cloud.provider import Provider -# %% X = PauliX() Z = PauliZ() @@ -36,17 +35,14 @@ Hx = AnalogGate(hamiltonian=(2 * X + Z)) -# %% op = 2 * X + Z print(op.model_dump_json()) -# %% print(Hx) print(Hx.model_dump_json()) print(AnalogGate.model_validate_json(Hx.model_dump_json())) -# %% circuit = AnalogCircuit() circuit.evolve(duration=10, gate=Hx) @@ -54,10 +50,8 @@ print(circuit.model_dump_json()) print(AnalogCircuit.model_validate_json(circuit.model_dump_json())) -# %% circuit.model_json_schema() -# %% # define task args args = TaskArgsAnalog( n_shots=100, @@ -71,49 +65,8 @@ task = Task(program=circuit, args=args) task.model_dump_json() -# %% -# backend = QutipBackend() -# expt, args = backend.compile(task=task) -# results = backend.run(experiment=expt, args=args) -# a = {"experiment": expt, "args": args} -# results = backend.run(task=task) - -# %% -# fig, ax = plt.subplots(1, 1, figsize=[6, 3]) -# colors = sns.color_palette(palette="crest", n_colors=4) - -# for k, (name, metric) in enumerate(results.metrics.items()): -# ax.plot(results.times, metric, label=f"$\\langle {name} \\rangle$", color=colors[k]) -# ax.legend() -# # plt.show() - -# # %% -# fig, axs = plt.subplots(4, 1, sharex=True, figsize=[5, 9]) -# state = np.array([basis.real + 1j * basis.imag for basis in results.state]) -# bases = ["0", "1"] -# counts = {basis: results.counts.get(basis, 0) for basis in bases} - -# ax = axs[0] -# ax.bar(x=bases, height=np.abs(state) ** 2, color=colors[0]) -# ax.set(ylabel="Probability") - - -# ax = axs[1] -# ax.bar(x=bases, height=list(counts.values()), color=colors[1]) -# ax.set(ylabel="Count") - -# ax = axs[2] -# ax.bar(x=bases, height=state.real, color=colors[2]) -# ax.set(ylabel="Amplitude (real)") - -# ax = axs[3] -# ax.bar(x=bases, height=state.imag, color=colors[3]) -# ax.set(xlabel="Basis state", ylabel="Amplitude (imag)", ylim=[-np.pi, np.pi]) - -# # plt.show() - -# %% +#%% client = Client() provider = Provider(port=8007) client.connect(provider=provider, username="ben", password="pwd") @@ -125,13 +78,12 @@ # %% print(client.jobs) -job = client.submit_job(task=task, backend="oqd-analog-emulator-qutip") +job = client.submit_job(task=task, backend="oqd-analog-emulator-qutip", tags=['a', 'b']) # %% client.retrieve_job(job_id=job.job_id) # %% - with open("./tests/atomic.json", "r") as f: circuit = AtomicCircuit.model_validate_json(f.read()) From 48d4b98c37d36552a274a63740f8ba2b25db30bc Mon Sep 17 00:00:00 2001 From: Benjamin MacLellan Date: Thu, 27 Feb 2025 22:47:33 -0500 Subject: [PATCH 07/11] Add tags --- src/oqd_cloud/client.py | 8 ++++---- src/oqd_cloud/server/model.py | 2 +- src/oqd_cloud/server/route/job.py | 23 +++++++-------------- tests/test_client.py | 33 +++---------------------------- 4 files changed, 15 insertions(+), 51 deletions(-) diff --git a/src/oqd_cloud/client.py b/src/oqd_cloud/client.py index cd44c0c..6190055 100644 --- a/src/oqd_cloud/client.py +++ b/src/oqd_cloud/client.py @@ -42,7 +42,7 @@ class Job(BaseModel): status: str result: Optional[str] = None user_id: str - # tags: Optional[Sequence[str]] + tags: Optional[str] = None class Client: @@ -139,13 +139,13 @@ def submit_job( self, task: Task, backend: str, - tags: Sequence[str] + tags: str ): """Submit a Task as an AnalogCircuit, DigitalCircuit, or AtomicCircuit to a backend.""" response = requests.post( self.provider.job_submission_url(backend=backend), - json=task.model_dump(), - # json={"task": task.model_dump(), "tags": tags}, + # json=task.model_dump(), + json={"task": task.model_dump(), "tags": tags}, headers=self.authorization_header, ) print(response) diff --git a/src/oqd_cloud/server/model.py b/src/oqd_cloud/server/model.py index c7eefc1..e545ddc 100644 --- a/src/oqd_cloud/server/model.py +++ b/src/oqd_cloud/server/model.py @@ -44,7 +44,7 @@ class Job(BaseModel): status: str result: Optional[str] = None user_id: str - # tags: Optional[Sequence[str]] + tags: Optional[str] = None class Backends(BaseModel): diff --git a/src/oqd_cloud/server/route/job.py b/src/oqd_cloud/server/route/job.py index 7f7d2de..583c147 100644 --- a/src/oqd_cloud/server/route/job.py +++ b/src/oqd_cloud/server/route/job.py @@ -12,12 +12,11 @@ # See the License for the specific language governing permissions and # limitations under the License. -from typing import Literal, Sequence, Optional +from typing import Literal, Sequence, Optional, Annotated -from fastapi import APIRouter, HTTPException +from fastapi import APIRouter, HTTPException, Body from fastapi import status as http_status -import logging -logger = logging.getLogger('uvicorn.error') + ######################################################################################## import oqd_analog_emulator #.qutip_backend import QutipBackend import oqd_trical @@ -56,22 +55,14 @@ async def available_backends(): @job_router.post("/submit/{backend}", tags=["Job"]) async def submit_job( - task: Task, backend: Literal[tuple(backends.available)], - # tags: Optional[Sequence[str]], + task: Task, + tags: Annotated[str, Body()], user: user_dependency, db: db_dependency, ): - print(task) print(f"Queueing {task} on server {backend} backend. {len(queue)} jobs in queue.") - # logger.debug(f'debug message {tags}') - - _backends = { - "oqd-analog-emulator-qutip": oqd_analog_emulator.qutip_backend.QutipBackend(), - "oqd-trical-qutip": oqd_trical.backend.qutip.QutipBackend(), - "oqd-trical-dynamiqs": oqd_trical.backend.dynamiqs.DynamiqsBackend(), - } - + job = queue.enqueue( _backends[backend].run, task, @@ -86,7 +77,7 @@ async def submit_job( backend=backend, status=job.get_status(), result=None, - # tags=tags, + tags=tags, user_id=user.user_id, ) db.add(job_in_db) diff --git a/tests/test_client.py b/tests/test_client.py index 4072212..dc6e424 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -27,6 +27,7 @@ from oqd_cloud.client import Client from oqd_cloud.provider import Provider +from rich.pretty import pprint X = PauliX() Z = PauliZ() @@ -78,36 +79,8 @@ # %% print(client.jobs) -job = client.submit_job(task=task, backend="oqd-analog-emulator-qutip", tags=['a', 'b']) +job = client.submit_job(task=task, backend="oqd-analog-emulator-qutip", tags='a') +pprint(job) # %% client.retrieve_job(job_id=job.job_id) - -# %% -with open("./tests/atomic.json", "r") as f: - circuit = AtomicCircuit.model_validate_json(f.read()) - -print(circuit) - -# %% -from minio import Minio -import os - -os.getenv("127.0.0.1:9000") - -client = Minio( - "127.0.0.1:9000", - access_key="admin", - secret_key="password", - secure=False -) - -# %% -source_file = "./tests/atomic.json" -bucket_name = "oqd-cloud-bucket" -destination_file = "my-test-file.txt" - -client.fput_object( - bucket_name, destination_file, source_file, -) -# %% From b3a9fc862a41c7a696213850289708f6ca716401 Mon Sep 17 00:00:00 2001 From: Benjamin MacLellan Date: Sat, 1 Mar 2025 15:41:51 -0500 Subject: [PATCH 08/11] Add s3 storage functions --- src/oqd_cloud/server/jobqueue.py | 7 +++- src/oqd_cloud/server/storage.py | 62 +++++++++++++++++++------------- 2 files changed, 43 insertions(+), 26 deletions(-) diff --git a/src/oqd_cloud/server/jobqueue.py b/src/oqd_cloud/server/jobqueue.py index a0a5398..ddcbcbb 100644 --- a/src/oqd_cloud/server/jobqueue.py +++ b/src/oqd_cloud/server/jobqueue.py @@ -23,6 +23,7 @@ ######################################################################################## from oqd_cloud.server.database import JobInDB, get_db +from oqd_cloud.server.storage import save_obj, get_temp_link ######################################################################################## REDIS_HOST = os.environ["REDIS_HOST"] @@ -38,7 +39,11 @@ async def _report_success(job, connection, result, *args, **kwargs): async with asynccontextmanager(get_db)() as db: - status_update = dict(status="finished", result=result.model_dump_json()) + + save_obj(job, result) + url = get_temp_link(job) + status_update = dict(status="finished", result=url) + # status_update = dict(status="finished", result=result.model_dump_json()) query = await db.execute(select(JobInDB).filter(JobInDB.job_id == job.id)) job_in_db = query.scalars().first() for k, v in status_update.items(): diff --git a/src/oqd_cloud/server/storage.py b/src/oqd_cloud/server/storage.py index 2143d97..15f9b23 100644 --- a/src/oqd_cloud/server/storage.py +++ b/src/oqd_cloud/server/storage.py @@ -15,36 +15,48 @@ from typing import Literal from minio import Minio import os +import io +from datetime import timedelta -from fastapi import APIRouter, HTTPException -from fastapi import status as http_status - -######################################################################################## -import oqd_analog_emulator #.qutip_backend import QutipBackend -import oqd_trical -from oqd_core.backend.task import Task -from rq.job import Callback -from rq.job import Job as RQJob -from sqlalchemy import select - -from oqd_cloud.server.database import JobInDB, db_dependency -from oqd_cloud.server.model import Job, Backends -from oqd_cloud.server.route.auth import user_dependency +from oqd_cloud.server.database import JobInDB, get_db ######################################################################################## minio_client = Minio( - "127.0.0.1:9000", - access_key="admin", - secret_key="password", + f"{os.getenv("MINIO_ENDPOINT")}:9000", + access_key=os.getenv("MINIO_ROOT_USER"), + secret_key=os.getenv("MINIO_ROOT_PASSWORD"), secure=False ) -# %% -source_file = "./tests/atomic.json" -bucket_name = "oqd-cloud-bucket" -destination_file = "my-test-file.txt" - -minio_client.fput_object( - bucket_name, destination_file, source_file, -) \ No newline at end of file +DEFAULT_MINIO_BUCKET = os.getenv("MINIO_DEFAULT_BUCKETS") +RESULT_FILENAME = "result.json" + +def save_obj(job: JobInDB, result): + # if the file is already saved, fput can be used + # minio_client.fput_object( + # BUCKET, destination_file, source_file, + # ) + + # here we dump to json, todo: future version should dump to HDF5 + json_bytes = result.model_dump_json().encode('utf-8') + buffer = io.BytesIO(json_bytes) + + minio_client.put_object( + DEFAULT_MINIO_BUCKET, + f"{job.id}/{RESULT_FILENAME}", + data=buffer, + length=len(json_bytes), + content_type='application/json' + ) + + return + + +def get_temp_link(job: JobInDB): + return minio_client.get_presigned_url( + "GET", + DEFAULT_MINIO_BUCKET, + f"{job.id}/{RESULT_FILENAME}", + expires=timedelta(hours=2), + ) From 1e1e1ab480bfa0169dfd84aef0edaaa19e598a26 Mon Sep 17 00:00:00 2001 From: Benjamin MacLellan Date: Sat, 1 Mar 2025 16:33:57 -0500 Subject: [PATCH 09/11] Test job result retrieval and storage --- src/oqd_cloud/client.py | 5 +++++ src/oqd_cloud/server/jobqueue.py | 1 - src/oqd_cloud/server/route/job.py | 3 ++- src/oqd_cloud/server/storage.py | 10 +++++++++- tests/test_client.py | 5 ++++- 5 files changed, 20 insertions(+), 4 deletions(-) diff --git a/src/oqd_cloud/client.py b/src/oqd_cloud/client.py index 6190055..4af5873 100644 --- a/src/oqd_cloud/client.py +++ b/src/oqd_cloud/client.py @@ -14,6 +14,7 @@ from typing import Literal, Optional, Sequence +import urllib.request import requests import json @@ -167,6 +168,10 @@ def retrieve_job(self, job_id): job = Job.model_validate(response.json()) if response.status_code == 200: + # download result file from temporary link + with urllib.request.urlopen(job.result) as f: + job.result = f.read().decode('utf-8') + self._jobs[job_id] = job return self.jobs[job_id] diff --git a/src/oqd_cloud/server/jobqueue.py b/src/oqd_cloud/server/jobqueue.py index ddcbcbb..401ead4 100644 --- a/src/oqd_cloud/server/jobqueue.py +++ b/src/oqd_cloud/server/jobqueue.py @@ -39,7 +39,6 @@ async def _report_success(job, connection, result, *args, **kwargs): async with asynccontextmanager(get_db)() as db: - save_obj(job, result) url = get_temp_link(job) status_update = dict(status="finished", result=url) diff --git a/src/oqd_cloud/server/route/job.py b/src/oqd_cloud/server/route/job.py index 583c147..11f0ef2 100644 --- a/src/oqd_cloud/server/route/job.py +++ b/src/oqd_cloud/server/route/job.py @@ -61,7 +61,8 @@ async def submit_job( user: user_dependency, db: db_dependency, ): - print(f"Queueing {task} on server {backend} backend. {len(queue)} jobs in queue.") + print(f"Queueing task on server {backend} backend. {len(queue)} jobs in queue.") + # print(f"Queueing {task} on server {backend} backend. {len(queue)} jobs in queue.") job = queue.enqueue( _backends[backend].run, diff --git a/src/oqd_cloud/server/storage.py b/src/oqd_cloud/server/storage.py index 15f9b23..7828f54 100644 --- a/src/oqd_cloud/server/storage.py +++ b/src/oqd_cloud/server/storage.py @@ -23,7 +23,7 @@ ######################################################################################## minio_client = Minio( - f"{os.getenv("MINIO_ENDPOINT")}:9000", + f"localhost:9000", access_key=os.getenv("MINIO_ROOT_USER"), secret_key=os.getenv("MINIO_ROOT_PASSWORD"), secure=False @@ -32,6 +32,14 @@ DEFAULT_MINIO_BUCKET = os.getenv("MINIO_DEFAULT_BUCKETS") RESULT_FILENAME = "result.json" +if not minio_client.bucket_exists(DEFAULT_MINIO_BUCKET): + minio_client.make_bucket(DEFAULT_MINIO_BUCKET) + print("Created bucket", DEFAULT_MINIO_BUCKET) +else: + print("Bucket", DEFAULT_MINIO_BUCKET, "already exists") + + + def save_obj(job: JobInDB, result): # if the file is already saved, fput can be used # minio_client.fput_object( diff --git a/tests/test_client.py b/tests/test_client.py index dc6e424..c4aae38 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -83,4 +83,7 @@ pprint(job) # %% -client.retrieve_job(job_id=job.job_id) +job = client.retrieve_job(job_id=job.job_id) +pprint(job) + +# %% From 5d1bee18e32d991db9f02e4747913d45d6976c50 Mon Sep 17 00:00:00 2001 From: Benjamin MacLellan Date: Sat, 1 Mar 2025 16:34:44 -0500 Subject: [PATCH 10/11] Ruff format and check --- src/oqd_cloud/client.py | 14 ++++---------- src/oqd_cloud/provider.py | 8 +++----- src/oqd_cloud/server/jobqueue.py | 4 ++-- src/oqd_cloud/server/main.py | 2 +- src/oqd_cloud/server/model.py | 4 ++-- src/oqd_cloud/server/route/job.py | 8 ++++---- src/oqd_cloud/server/storage.py | 24 +++++++++++------------- tests/test_client.py | 9 ++++----- 8 files changed, 31 insertions(+), 42 deletions(-) diff --git a/src/oqd_cloud/client.py b/src/oqd_cloud/client.py index 4af5873..7ea3a85 100644 --- a/src/oqd_cloud/client.py +++ b/src/oqd_cloud/client.py @@ -13,11 +13,10 @@ # limitations under the License. -from typing import Literal, Optional, Sequence +from typing import Optional, Sequence import urllib.request import requests -import json from oqd_core.backend.task import Task from pydantic import BaseModel, ConfigDict @@ -136,12 +135,7 @@ def connect(self, provider: Provider, username: str, password: str): # self.connect(self, self.provider) # pass - def submit_job( - self, - task: Task, - backend: str, - tags: str - ): + def submit_job(self, task: Task, backend: str, tags: str): """Submit a Task as an AnalogCircuit, DigitalCircuit, or AtomicCircuit to a backend.""" response = requests.post( self.provider.job_submission_url(backend=backend), @@ -170,8 +164,8 @@ def retrieve_job(self, job_id): if response.status_code == 200: # download result file from temporary link with urllib.request.urlopen(job.result) as f: - job.result = f.read().decode('utf-8') - + job.result = f.read().decode("utf-8") + self._jobs[job_id] = job return self.jobs[job_id] diff --git a/src/oqd_cloud/provider.py b/src/oqd_cloud/provider.py index 7415b2a..cd0e44b 100644 --- a/src/oqd_cloud/provider.py +++ b/src/oqd_cloud/provider.py @@ -12,7 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -import requests +import requests from oqd_cloud.server.model import Backends @@ -28,9 +28,7 @@ def __init__(self, host: str = "http://localhost", port: int = 8000): # get available backends self.backends = Backends(available=[]) - response = requests.get( - self.url + "/available_backends" - ) + response = requests.get(self.url + "/available_backends") backends = Backends.model_validate(response.json()) if response.status_code == 200: self.backends = backends @@ -38,7 +36,7 @@ def __init__(self, host: str = "http://localhost", port: int = 8000): @property def available_backends(self): return self.backends.available - + @property def registration_url(self): return self.url + "/auth/register" diff --git a/src/oqd_cloud/server/jobqueue.py b/src/oqd_cloud/server/jobqueue.py index 401ead4..fd71c6b 100644 --- a/src/oqd_cloud/server/jobqueue.py +++ b/src/oqd_cloud/server/jobqueue.py @@ -41,8 +41,8 @@ async def _report_success(job, connection, result, *args, **kwargs): async with asynccontextmanager(get_db)() as db: save_obj(job, result) url = get_temp_link(job) - status_update = dict(status="finished", result=url) - # status_update = dict(status="finished", result=result.model_dump_json()) + status_update = dict(status="finished", result=url) + # status_update = dict(status="finished", result=result.model_dump_json()) query = await db.execute(select(JobInDB).filter(JobInDB.job_id == job.id)) job_in_db = query.scalars().first() for k, v in status_update.items(): diff --git a/src/oqd_cloud/server/main.py b/src/oqd_cloud/server/main.py index 41b47f7..379c69c 100644 --- a/src/oqd_cloud/server/main.py +++ b/src/oqd_cloud/server/main.py @@ -15,4 +15,4 @@ if __name__ == "__main__": import uvicorn - uvicorn.run("app:app", host="0.0.0.0", port=8007, log_level='debug') + uvicorn.run("app:app", host="0.0.0.0", port=8007, log_level="debug") diff --git a/src/oqd_cloud/server/model.py b/src/oqd_cloud/server/model.py index e545ddc..e82bc2a 100644 --- a/src/oqd_cloud/server/model.py +++ b/src/oqd_cloud/server/model.py @@ -45,7 +45,7 @@ class Job(BaseModel): result: Optional[str] = None user_id: str tags: Optional[str] = None - + class Backends(BaseModel): - available: Sequence[str] \ No newline at end of file + available: Sequence[str] diff --git a/src/oqd_cloud/server/route/job.py b/src/oqd_cloud/server/route/job.py index 11f0ef2..dad3e25 100644 --- a/src/oqd_cloud/server/route/job.py +++ b/src/oqd_cloud/server/route/job.py @@ -12,13 +12,13 @@ # See the License for the specific language governing permissions and # limitations under the License. -from typing import Literal, Sequence, Optional, Annotated +from typing import Literal, Annotated from fastapi import APIRouter, HTTPException, Body from fastapi import status as http_status ######################################################################################## -import oqd_analog_emulator #.qutip_backend import QutipBackend +import oqd_analog_emulator # .qutip_backend import QutipBackend import oqd_trical from oqd_core.backend.task import Task from rq.job import Callback @@ -51,8 +51,8 @@ @job_router.get("/available_backends") async def available_backends(): return backends - - + + @job_router.post("/submit/{backend}", tags=["Job"]) async def submit_job( backend: Literal[tuple(backends.available)], diff --git a/src/oqd_cloud/server/storage.py b/src/oqd_cloud/server/storage.py index 7828f54..0770e94 100644 --- a/src/oqd_cloud/server/storage.py +++ b/src/oqd_cloud/server/storage.py @@ -12,21 +12,20 @@ # See the License for the specific language governing permissions and # limitations under the License. -from typing import Literal from minio import Minio import os import io from datetime import timedelta -from oqd_cloud.server.database import JobInDB, get_db +from oqd_cloud.server.database import JobInDB ######################################################################################## minio_client = Minio( - f"localhost:9000", + "localhost:9000", access_key=os.getenv("MINIO_ROOT_USER"), secret_key=os.getenv("MINIO_ROOT_PASSWORD"), - secure=False + secure=False, ) DEFAULT_MINIO_BUCKET = os.getenv("MINIO_DEFAULT_BUCKETS") @@ -39,23 +38,22 @@ print("Bucket", DEFAULT_MINIO_BUCKET, "already exists") - def save_obj(job: JobInDB, result): # if the file is already saved, fput can be used # minio_client.fput_object( # BUCKET, destination_file, source_file, # ) - - # here we dump to json, todo: future version should dump to HDF5 - json_bytes = result.model_dump_json().encode('utf-8') + + # here we dump to json, todo: future version should dump to HDF5 + json_bytes = result.model_dump_json().encode("utf-8") buffer = io.BytesIO(json_bytes) minio_client.put_object( - DEFAULT_MINIO_BUCKET, - f"{job.id}/{RESULT_FILENAME}", - data=buffer, - length=len(json_bytes), - content_type='application/json' + DEFAULT_MINIO_BUCKET, + f"{job.id}/{RESULT_FILENAME}", + data=buffer, + length=len(json_bytes), + content_type="application/json", ) return diff --git a/tests/test_client.py b/tests/test_client.py index c4aae38..c4acd72 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -23,11 +23,10 @@ from oqd_core.interface.analog.operation import AnalogCircuit, AnalogGate from oqd_core.interface.analog.operator import PauliX, PauliZ -from oqd_core.interface.atomic.circuit import AtomicCircuit from oqd_cloud.client import Client from oqd_cloud.provider import Provider -from rich.pretty import pprint +from rich.pretty import pprint X = PauliX() Z = PauliZ() @@ -67,19 +66,19 @@ task.model_dump_json() -#%% +# %% client = Client() provider = Provider(port=8007) client.connect(provider=provider, username="ben", password="pwd") client.status_report -#%% +# %% backends = provider.available_backends print(backends) # %% print(client.jobs) -job = client.submit_job(task=task, backend="oqd-analog-emulator-qutip", tags='a') +job = client.submit_job(task=task, backend="oqd-analog-emulator-qutip", tags="a") pprint(job) # %% From 3451f5d0cdcde21ecde8225339d96609a7af2577 Mon Sep 17 00:00:00 2001 From: Benjamin MacLellan Date: Fri, 16 May 2025 13:15:19 -0400 Subject: [PATCH 11/11] Test hdf format --- .gitignore | 2 + tests/test_h5.py | 50 ++++++++++++++++++++++++ tests/test_storage.py | 90 +++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 142 insertions(+) create mode 100644 tests/test_h5.py create mode 100644 tests/test_storage.py diff --git a/.gitignore b/.gitignore index 874aba0..81d28c8 100644 --- a/.gitignore +++ b/.gitignore @@ -167,3 +167,5 @@ cython_debug/ uv.lock docker/.env copy + +*.DS_Store \ No newline at end of file diff --git a/tests/test_h5.py b/tests/test_h5.py new file mode 100644 index 0000000..ed25e86 --- /dev/null +++ b/tests/test_h5.py @@ -0,0 +1,50 @@ +# %% +from h5pydantic import H5Dataset, H5Group, H5Int64 + + +class Baseline(H5Group): + temperature: float + humidity: float + + +class Metadata(H5Group): + start: Baseline + end: Baseline + + +class Acquisition(H5Dataset, shape=(3, 5), dtype=H5Int64): + beamstop: H5Int64 + + +class Experiment(H5Group): + metadata: Metadata + data: list[Acquisition] = [] + + +# %% +# from model import Experiment, Acquisition, Baseline, Metadata + +import numpy as np +from pathlib import Path +from rich.pretty import pprint + +experiment = Experiment( + data=[Acquisition(beamstop=11), Acquisition(beamstop=12)], + metadata=Metadata( + start=Baseline(temperature=25.0, humidity=0.4), + end=Baseline(temperature=26.0, humidity=0.4), + ), +) + +with experiment.dump(Path("experiment.hdf")): + experiment.data[0][()] = np.random.randint(255, size=(3, 5)) + experiment.data[1][()] = np.random.randint(255, size=(3, 5)) + +pprint(experiment) + +# %% +experiment + + + +# %% diff --git a/tests/test_storage.py b/tests/test_storage.py new file mode 100644 index 0000000..4b0fed6 --- /dev/null +++ b/tests/test_storage.py @@ -0,0 +1,90 @@ +# %% +# with open("./tests/atomic.json", "r") as f: +# circuit = AtomicCircuit.model_validate_json(f.read()) + +# print(circuit) + +# %% +from minio import Minio +import os + +os.getenv("127.0.0.1:9000") + +user_id = '1234' +job_id = '4321' + +client = Minio( + "127.0.0.1:9000", + access_key="admin", + secret_key="password", + secure=False +) + +# %% +source_file = "./tests/atomic.json" +bucket_name = f"{user_id}" +destination_file = f"{job_id}/artifact1.txt" + +#%% +import io +from oqd_core.interface.atomic import AtomicCircuit + +circuit = AtomicCircuit.parse_file(source_file) +#%% + + + +json_bytes = circuit.model_dump_json().encode('utf-8') +buffer = io.BytesIO(json_bytes) + +client.put_object( + "oqd-cloud-bucket", + "result.txt", + data=buffer, + length=len(json_bytes), + content_type='application/json' +) + +#%% +# Make the bucket if it doesn't exist. +found = client.bucket_exists(bucket_name) +if not found: + client.make_bucket(bucket_name) + print("Created bucket", bucket_name) +else: + print("Bucket", bucket_name, "already exists") + + +client.fput_object( + bucket_name, destination_file, source_file, +) +print( + source_file, "successfully uploaded as object", + destination_file, "to bucket", bucket_name, +) + +#%% +# Server should: +# 1. Run the job, submitting to the requested backend +# 2. The backend returns the result as a HDF5/Pydantic model, which is dumped to as a Minio artifact [jobid.hdf5] +# 3. When client requests results, the server checks if successful, generates a temporary link, and returns the url +# 4. The Client class will automatically + +# %% [SERVER] creates a minio link for the file, with a expiry time +from datetime import timedelta + +url = client.get_presigned_url( + "GET", + bucket_name, + destination_file, + expires=timedelta(hours=2), +) + +#%% [CLIENT] saves to provided filename +import urllib.request +file = urllib.request.urlretrieve(url, "result.txt") + +#%% [CLIENT] loads directly to memory +with urllib.request.urlopen(url) as f: + html = f.read()#.decode('utf-8') +# %%