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: Cloud
+ Open Quantum Design: Cloud
-
-[](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
-///
+[](https://pypi.org/project/oqd-cloud)
+[](https://github.com/OpenQuantumDesign/oqd-cloud/actions/workflows/pytest.yml)
+
+[](https://opensource.org/licenses/Apache-2.0)
+[](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')
+# %%