diff --git a/.gitignore b/.gitignore index fafbe0d..81d28c8 100644 --- a/.gitignore +++ b/.gitignore @@ -165,4 +165,7 @@ cython_debug/ *.code-workspace .github/workflows/_*.yml -uv.lock \ No newline at end of file +uv.lock +docker/.env copy + +*.DS_Store \ No newline at end of file diff --git a/README.md b/README.md index 29ceeef..4b422e1 100644 --- a/README.md +++ b/README.md @@ -41,93 +41,99 @@ pip install .[docs] mkdocs serve ``` - +### Where in the stack ```mermaid block-beta columns 3 - + block:Interface columns 1 InterfaceTitle("Interfaces") - InterfaceDigital["Digital Interface\nQuantum circuits with discrete gates"] + InterfaceDigital["Digital Interface\nQuantum circuits with discrete gates"] space - InterfaceAnalog["Analog Interface\n Continuous-time evolution with Hamiltonians"] + InterfaceAnalog["Analog Interface\n Continuous-time evolution with Hamiltonians"] space InterfaceAtomic["Atomic Interface\nLight-matter interactions between lasers and ions"] space end - + block:IR columns 1 IRTitle("IRs") - IRDigital["Quantum circuit IR\nopenQASM, LLVM+QIR"] + IRDigital["Quantum circuit IR\nopenQASM, LLVM+QIR"] space IRAnalog["openQSIM"] space IRAtomic["openAPL"] space end - + block:Emulator columns 1 EmulatorsTitle("Classical Emulators") - - EmulatorDigital["Pennylane, Qiskit"] + + EmulatorDigital["Pennylane, Qiskit"] space EmulatorAnalog["QuTiP, QuantumOptics.jl"] space EmulatorAtomic["TrICal, QuantumIon.jl"] space end - + space block:RealTime columns 1 RealTimeTitle("Real-Time") space - RTSoftware["ARTIQ, DAX, OQDAX"] + RTSoftware["ARTIQ, DAX, OQDAX"] space RTGateware["Sinara Real-Time Control"] space RTHardware["Lasers, Modulators, Photodetection, Ion Trap"] space - RTApparatus["Trapped-Ion QPU (171Yt+, 133Ba+)"] + RTApparatus["Trapped-Ion QPU (171Yb+, 133Ba+)"] space end space - + InterfaceDigital --> IRDigital InterfaceAnalog --> IRAnalog InterfaceAtomic --> IRAtomic - + IRDigital --> IRAnalog IRAnalog --> IRAtomic - + IRDigital --> EmulatorDigital IRAnalog --> EmulatorAnalog IRAtomic --> EmulatorAtomic - + IRAtomic --> RealTimeTitle - + RTSoftware --> RTGateware RTGateware --> RTHardware RTHardware --> RTApparatus - - classDef title fill:#d6d4d4,stroke:#333,color:#333; - classDef digital fill:#E7E08B,stroke:#333,color:#333; - classDef analog fill:#E4E9B2,stroke:#333,color:#333; - classDef atomic fill:#D2E4C4,stroke:#333,color:#333; - classDef realtime fill:#B5CBB7,stroke:#333,color:#333; - - classDef highlight fill:#f2bbbb,stroke:#333,color:#333,stroke-dasharray: 5 5; - + + classDef title fill:#23627D,stroke:#141414,color:#FFFFFF; + classDef digital fill:#c3e1ee,stroke:#141414,color:#141414; + classDef analog fill:#afd7e9,stroke:#141414,color:#141414; + classDef atomic fill:#9ccee3,stroke:#141414,color:#141414; + classDef realtime fill:#88c4dd,stroke:#141414,color:#141414; + + classDef highlight fill:#F19D19,stroke:#141414,color:#141414,stroke-dasharray: 5 5; + classDef normal fill:#fcebcf,stroke:#141414,color:#141414; + class InterfaceTitle,IRTitle,EmulatorsTitle,RealTimeTitle title class InterfaceDigital,IRDigital,EmulatorDigital digital class InterfaceAnalog,IRAnalog,EmulatorAnalog analog class InterfaceAtomic,IRAtomic,EmulatorAtomic atomic class RTSoftware,RTGateware,RTHardware,RTApparatus realtime + + class Emulator highlight + + class Interface normal + class RealTime normal + class IR normal - class Emulator highlight ``` The tools in this repository allow for self-hosting a server to run quantum programs on classical emulators, highlighted in the stack diagram in red. 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..997ea3f 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -1,62 +1,22 @@ -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 - -WORKDIR /python - -RUN python -m 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 - - -######################################################################################## - -FROM python:3.10.13-slim-bookworm as app - -RUN apt update && \ apt install -y --no-install-recommends supervisor && \ + apt-get install -y git && \ apt-get install -y gcc g++ # needed from Cython - -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 PYTHONPATH="/app/src" - COPY . ./app +ENV PYTHONPATH="/app/src" WORKDIR /app -RUN pip install . -#RUN pip install .[all] -#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 uv venv +ENV PATH=".venv/bin:$PATH" -# 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 +RUN uv pip install ".[server]" -# ENV PATH "$PATH:/opt/julia-1.9.3/bin" +COPY ./docker/supervisord.conf /etc/supervisor/conf.d/supervisord.conf +CMD ["/usr/bin/supervisord", "-c", "/etc/supervisor/conf.d/supervisord.conf"] -# 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..184085b 100644 --- a/docker/docker-compose.yaml +++ b/docker/docker-compose.yaml @@ -6,12 +6,39 @@ networks: internal: name: oqd-cloud-server-internal +volumes: + redis_volume: + driver: local + postgres_volume: + driver: local + minio_volume: + driver: local + + ################################################################################ services: + minio: + image: minio/minio + container_name: minio + restart: always + ports: + - '9000:9000' + - '9001:9001' + # network_mode: "host" + networks: + internal: + volumes: + - 'minio_volume:/data' + environment: + - MINIO_ROOT_USER=${MINIO_ROOT_USER} + - MINIO_ROOT_PASSWORD=${MINIO_ROOT_PASSWORD} + - MINIO_DEFAULT_BUCKETS=${MINIO_DEFAULT_BUCKETS} + command: server /data --console-address ":9001" + 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"] @@ -22,8 +49,11 @@ 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: volumes: @@ -31,7 +61,7 @@ services: postgres: image: postgres - container_name: oqd-cloud-server-postgres + container_name: postgres restart: always environment: POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} # Replace @@ -43,45 +73,86 @@ services: interval: 5s timeout: 25s retries: 5 - expose: - - "5432" + # expose: + # - "5432" + ports: + - "5432:5432" + # network_mode: "host" networks: internal: volumes: - postgres_volume:/var/lib/postgres/data - 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 - ports: - - "8000:8000" - networks: - internal: - depends_on: - redis: - condition: service_healthy - postgres: - condition: service_healthy -volumes: - redis_volume: - driver: local - postgres_volume: - driver: local + # 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: 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 + # 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/docker/requirements.txt b/docker/requirements.txt deleted file mode 100644 index 0468d34..0000000 --- a/docker/requirements.txt +++ /dev/null @@ -1,9 +0,0 @@ -qutip~=5.0.1 -numpy~=1.0 -pydantic>=2.4 - -sqlalchemy -fastapi -redis -rq -python-dotenv \ No newline at end of file diff --git a/docs/img/oqd-icon.png b/docs/img/oqd-icon.png new file mode 100755 index 0000000..5571ea8 Binary files /dev/null and b/docs/img/oqd-icon.png differ diff --git a/docs/img/oqd-logo-black.png b/docs/img/oqd-logo-black.png new file mode 100755 index 0000000..e884f2a Binary files /dev/null and b/docs/img/oqd-logo-black.png differ diff --git a/docs/img/oqd-logo-text.png b/docs/img/oqd-logo-text.png deleted file mode 100644 index 8ae1e94..0000000 Binary files a/docs/img/oqd-logo-text.png and /dev/null differ diff --git a/docs/img/oqd-logo-white.png b/docs/img/oqd-logo-white.png new file mode 100755 index 0000000..c9ae062 Binary files /dev/null and b/docs/img/oqd-logo-white.png differ diff --git a/docs/img/oqd-logo.png b/docs/img/oqd-logo.png deleted file mode 100644 index e3aaf16..0000000 Binary files a/docs/img/oqd-logo.png and /dev/null differ diff --git a/docs/index.md b/docs/index.md index f6f5888..71eac93 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1,113 +1,119 @@ -# ![Open Quantum Design](./img/oqd-logo-text.png) +# + +

+ Logo + Logo +

- Open Quantum Design: Cloud + Open Quantum Design: Cloud

-![Python](https://img.shields.io/badge/Python-3.10_|_3.11_|_3.12-blue) -[![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/ambv/black) - -/// admonition | Note - type: note -Welcome to the Open Quantum Design. -This documentation is still under development, we welcome contributions! © Open Quantum Design -/// +[![PyPI Version](https://img.shields.io/pypi/v/oqd-cloud)](https://pypi.org/project/oqd-cloud) +[![CI](https://github.com/OpenQuantumDesign/oqd-cloud/actions/workflows/pytest.yml/badge.svg)](https://github.com/OpenQuantumDesign/oqd-cloud/actions/workflows/pytest.yml) +![versions](https://img.shields.io/badge/python-3.10%20%7C%203.11%20%7C%203.12-blue) +[![License: Apache 2.0](https://img.shields.io/badge/license-Apache%202.0-brightgreen.svg)](https://opensource.org/licenses/Apache-2.0) +[![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff) ## What's Here This repository contains the software needed to submit jobs to a remote, cloud server for classical simulations of quantum programs. In addition, it provides a Docker script to self-host a simulation server of the OQD emulator backends. - - ```mermaid block-beta columns 3 - + block:Interface columns 1 InterfaceTitle("Interfaces") - InterfaceDigital["Digital Interface\nQuantum circuits with discrete gates"] + InterfaceDigital["Digital Interface\nQuantum circuits with discrete gates"] space - InterfaceAnalog["Analog Interface\n Continuous-time evolution with Hamiltonians"] + InterfaceAnalog["Analog Interface\n Continuous-time evolution with Hamiltonians"] space InterfaceAtomic["Atomic Interface\nLight-matter interactions between lasers and ions"] space end - + block:IR columns 1 IRTitle("IRs") - IRDigital["Quantum circuit IR\nopenQASM, LLVM+QIR"] + IRDigital["Quantum circuit IR\nopenQASM, LLVM+QIR"] space IRAnalog["openQSIM"] space IRAtomic["openAPL"] space end - + block:Emulator columns 1 EmulatorsTitle("Classical Emulators") - - EmulatorDigital["Pennylane, Qiskit"] + + EmulatorDigital["Pennylane, Qiskit"] space EmulatorAnalog["QuTiP, QuantumOptics.jl"] space EmulatorAtomic["TrICal, QuantumIon.jl"] space end - + space block:RealTime columns 1 RealTimeTitle("Real-Time") space - RTSoftware["ARTIQ, DAX, OQDAX"] + RTSoftware["ARTIQ, DAX, OQDAX"] space RTGateware["Sinara Real-Time Control"] space RTHardware["Lasers, Modulators, Photodetection, Ion Trap"] space - RTApparatus["Trapped-Ion QPU (171Yt+, 133Ba+)"] + RTApparatus["Trapped-Ion QPU (171Yb+, 133Ba+)"] space end space - + InterfaceDigital --> IRDigital InterfaceAnalog --> IRAnalog InterfaceAtomic --> IRAtomic - + IRDigital --> IRAnalog IRAnalog --> IRAtomic - + IRDigital --> EmulatorDigital IRAnalog --> EmulatorAnalog IRAtomic --> EmulatorAtomic - + IRAtomic --> RealTimeTitle - + RTSoftware --> RTGateware RTGateware --> RTHardware RTHardware --> RTApparatus - - classDef title fill:#d6d4d4,stroke:#333,color:#333; - classDef digital fill:#E7E08B,stroke:#333,color:#333; - classDef analog fill:#E4E9B2,stroke:#333,color:#333; - classDef atomic fill:#D2E4C4,stroke:#333,color:#333; - classDef realtime fill:#B5CBB7,stroke:#333,color:#333; - - classDef highlight fill:#f2bbbb,stroke:#333,color:#333,stroke-dasharray: 5 5; - + + classDef title fill:#23627D,stroke:#141414,color:#FFFFFF; + classDef digital fill:#c3e1ee,stroke:#141414,color:#141414; + classDef analog fill:#afd7e9,stroke:#141414,color:#141414; + classDef atomic fill:#9ccee3,stroke:#141414,color:#141414; + classDef realtime fill:#88c4dd,stroke:#141414,color:#141414; + + classDef highlight fill:#F19D19,stroke:#141414,color:#141414,stroke-dasharray: 5 5; + classDef normal fill:#fcebcf,stroke:#141414,color:#141414; + class InterfaceTitle,IRTitle,EmulatorsTitle,RealTimeTitle title class InterfaceDigital,IRDigital,EmulatorDigital digital class InterfaceAnalog,IRAnalog,EmulatorAnalog analog class InterfaceAtomic,IRAtomic,EmulatorAtomic atomic class RTSoftware,RTGateware,RTHardware,RTApparatus realtime + + class Emulator highlight + + class Interface normal + class RealTime normal + class IR normal - class Emulator highlight ``` The tools in this repository allow for self-hosting a server to run quantum programs on classical emulators, highlighted in the stack diagram in red. diff --git a/docs/stylesheets/brand.css b/docs/stylesheets/brand.css new file mode 100644 index 0000000..d2fdeeb --- /dev/null +++ b/docs/stylesheets/brand.css @@ -0,0 +1,116 @@ +/* Load custom fonts from Google Fonts */ +@import url('https://fonts.googleapis.com/css2?family=Raleway:wght@600;700&family=Source+Serif+Pro&display=swap'); + +/* Base typography */ +body, .md-typeset { + font-family: 'Source Serif Pro', serif; +} + +/* Headings */ +h1, h2, h3, h4, h5, h6, +.md-typeset h1, .md-typeset h2, .md-typeset h3, +.md-typeset h4, .md-typeset h5, .md-typeset h6 { + font-family: 'Raleway', sans-serif; + font-weight: 700; /* Bold for headings */ +} + +/* Subheadings */ +.md-typeset h2, .md-typeset h3 { + font-weight: 600; /* SemiBold for subheadings */ +} + +/* Light mode overrides */ +[data-md-color-scheme="default"] { + --md-default-bg-color: #FFFFFF; + --md-primary-fg-color: #23627D; /* blue accent */ + --md-accent-fg-color: #23627D; +} + +/* Dark mode overrides */ +[data-md-color-scheme="slate"] { + --md-default-bg-color: #141414; + --md-primary-fg-color: #23627D; + --md-accent-fg-color: #23627D; +} + + + +/* Apply Raleway to all navigation and sidebar elements */ +.md-nav, +.md-nav__title, +.md-nav__link, +.md-header, +.md-tabs, +.md-sidebar, +.md-sidebar__inner, +.md-nav__item, +.md-footer, +.md-footer__inner { + font-family: 'Raleway', sans-serif; + font-weight: 600; /* SemiBold for ToC/nav for clarity */ +} + + + + +/* Page heading accent color for light mode */ +[data-md-color-scheme="default"] .md-typeset h1, +[data-md-color-scheme="default"] .md-typeset h2, +[data-md-color-scheme="default"] .md-typeset h3, +[data-md-color-scheme="default"] .md-typeset h4, +[data-md-color-scheme="default"] .md-typeset h5, +[data-md-color-scheme="default"] .md-typeset h6 { + color: #7B2328; /* Warm yellow/orange for light mode */ +} + +/* Page heading accent color for dark mode */ +[data-md-color-scheme="slate"] .md-typeset h1, +[data-md-color-scheme="slate"] .md-typeset h2, +[data-md-color-scheme="slate"] .md-typeset h3, +[data-md-color-scheme="slate"] .md-typeset h4, +[data-md-color-scheme="slate"] .md-typeset h5, +[data-md-color-scheme="slate"] .md-typeset h6 { + /* color: #F19D19; Deep red for dark mode */ + color: #E45B68; /* Deep red for dark mode */ +} + + + + + +/* Light mode nav/ToC font color */ +[data-md-color-scheme="default"] .md-nav, +[data-md-color-scheme="default"] .md-nav__link, +[data-md-color-scheme="default"] .md-header, +[data-md-color-scheme="default"] .md-tabs { + /*color: #222; /* Dark gray or your preferred shade */ + color: #141414; +} + +/* Dark mode nav/ToC font color */ +[data-md-color-scheme="slate"] .md-nav, +[data-md-color-scheme="slate"] .md-nav__link, +[data-md-color-scheme="slate"] .md-header, +[data-md-color-scheme="slate"] .md-tabs { + color: #f0f0f0; /* Light gray or white */ +} + + + +/* Top navigation bar text and icons should be light-colored */ +.md-header, +.md-header .md-header__title, +.md-header .md-header__button, +.md-header .md-tabs, +.md-header .md-tabs__link, +.md-header .md-header__topic, +.md-header .md-header__option { + color: #f0f0f0 !important; /* Light gray or white text */ + fill: #f0f0f0 !important; /* Icons (SVG) */ +} + +/* Hover state for links in header */ +.md-header .md-tabs__link:hover { + color: #ffffff !important; + text-decoration: underline; +} \ No newline at end of file diff --git a/mkdocs.yaml b/mkdocs.yaml index 9ba3209..d8b874a 100644 --- a/mkdocs.yaml +++ b/mkdocs.yaml @@ -27,22 +27,21 @@ nav: theme: name: material - - logo: img/oqd-logo.png - favicon: img/oqd-logo.png + logo: img/oqd-icon.png + favicon: img/oqd-icon.png palette: - media: "(prefers-color-scheme: light)" scheme: default - primary: teal - accent: pink + primary: custom + accent: custom toggle: icon: material/weather-sunny name: Switch to dark mode - media: "(prefers-color-scheme: dark)" scheme: slate - primary: teal - accent: pink + primary: custom + accent: custom toggle: icon: material/weather-night name: Switch to light mode @@ -126,3 +125,4 @@ extra_javascript: extra_css: - stylesheets/headers.css - stylesheets/admonitions.css + - stylesheets/brand.css diff --git a/pyproject.toml b/pyproject.toml index 7518e11..4297849 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -29,7 +29,23 @@ classifiers = [ dependencies = [ "requests", "pydantic>=2.4", - "oqd-core@git+https://github.com/openquantumdesign/oqd-core", + "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] @@ -49,10 +65,16 @@ 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..7ea3a85 100644 --- a/src/oqd_cloud/client.py +++ b/src/oqd_cloud/client.py @@ -13,7 +13,8 @@ # limitations under the License. -from typing import Literal, Optional +from typing import Optional, Sequence +import urllib.request import requests from oqd_core.backend.task import Task @@ -21,7 +22,11 @@ from oqd_cloud.provider import Provider -__all__ = ["Job", "Client"] +__all__ = ["Job", "Client", "Backends"] + + +class Backends(BaseModel): + available: Sequence[str] class Job(BaseModel): @@ -37,6 +42,7 @@ class Job(BaseModel): status: str result: Optional[str] = None user_id: str + tags: Optional[str] = None class Client: @@ -52,7 +58,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") ``` """ @@ -129,13 +135,15 @@ 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, 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.model_dump(), + json={"task": task.model_dump(), "tags": tags}, headers=self.authorization_header, ) + print(response) job = Job.model_validate(response.json()) if response.status_code == 200: @@ -154,6 +162,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/provider.py b/src/oqd_cloud/provider.py index 8bc583e..cd0e44b 100644 --- a/src/oqd_cloud/provider.py +++ b/src/oqd_cloud/provider.py @@ -12,25 +12,30 @@ # See the License for the specific language governing permissions and # limitations under the License. +import requests +from oqd_cloud.server.model import Backends + 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 + self.backends = Backends(available=[]) + response = requests.get(self.url + "/available_backends") + backends = Backends.model_validate(response.json()) + if response.status_code == 200: + self.backends = backends + @property def available_backends(self): - # todo: get available backends from url - if hasattr(self, "_available_backends"): - return self._available_backends - else: - return [ - "analog-qutip", - ] + return self.backends.available @property def registration_url(self): 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/jobqueue.py b/src/oqd_cloud/server/jobqueue.py index a0a5398..fd71c6b 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,10 @@ 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/main.py b/src/oqd_cloud/server/main.py index 29465e2..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=8000) + 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 9a2212d..e82bc2a 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,8 @@ class Job(BaseModel): status: str result: Optional[str] = None user_id: str + tags: Optional[str] = None + + +class Backends(BaseModel): + available: Sequence[str] diff --git a/src/oqd_cloud/server/route/job.py b/src/oqd_cloud/server/route/job.py index dd6175e..dad3e25 100644 --- a/src/oqd_cloud/server/route/job.py +++ b/src/oqd_cloud/server/route/job.py @@ -12,13 +12,14 @@ # See the License for the specific language governing permissions and # limitations under the License. -from typing import Literal +from typing import Literal, Annotated -from fastapi import APIRouter, HTTPException +from fastapi import APIRouter, HTTPException, Body 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,40 +33,39 @@ 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 ######################################################################################## +_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.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)], task: Task, - backend: Literal["analog-qutip",], + 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.") - - backends = { - "analog-qutip": QutipBackend(), - # "tensorcircuit": TensorCircuitBackend() - } - # backends_run = { - # "analog-qutip": lambda task: backends["analog-qutip"].run(task=task) - # } - - if backend == "analog-qutip": - try: - expt, args = backends[backend].compile(task=task) - except Exception: - raise Exception("Cannot properly compile to the QutipBackend.") + 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, + _backends[backend].run, task, on_success=Callback(report_success), on_failure=Callback(report_failure), @@ -78,6 +78,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/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/src/oqd_cloud/server/storage.py b/src/oqd_cloud/server/storage.py new file mode 100644 index 0000000..0770e94 --- /dev/null +++ b/src/oqd_cloud/server/storage.py @@ -0,0 +1,68 @@ +# 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 minio import Minio +import os +import io +from datetime import timedelta + +from oqd_cloud.server.database import JobInDB + +######################################################################################## + +minio_client = Minio( + "localhost:9000", + access_key=os.getenv("MINIO_ROOT_USER"), + secret_key=os.getenv("MINIO_ROOT_PASSWORD"), + secure=False, +) + +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( + # 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), + ) 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 diff --git a/tests/test_client.py b/tests/test_client.py index 1312220..c4acd72 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -14,19 +14,20 @@ # %% -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 from oqd_core.interface.analog.operator import PauliX, PauliZ + from oqd_cloud.client import Client from oqd_cloud.provider import Provider +from rich.pretty import pprint -# %% X = PauliX() Z = PauliZ() @@ -34,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) @@ -52,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, @@ -69,57 +65,24 @@ 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() +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="analog-qutip") +job = client.submit_job(task=task, backend="oqd-analog-emulator-qutip", tags="a") +pprint(job) + +# %% +job = client.retrieve_job(job_id=job.job_id) +pprint(job) # %% -client.retrieve_job(job_id=job.job_id) 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') +# %%