├── tierkreis ├── tests │ ├── __init__.py │ ├── py.typed │ ├── cli │ │ ├── __init__.py │ │ ├── data │ │ │ ├── n │ │ │ ├── input1 │ │ │ ├── input2 │ │ │ ├── data.json │ │ │ ├── factorial │ │ │ └── sample_graph │ │ └── test_run_workflow.py │ ├── controller │ │ ├── __init__.py │ │ ├── test_graphdata.py │ │ ├── main.py │ │ ├── test_codegen.py │ │ ├── partial_graphdata.py │ │ ├── test_models.py │ │ └── test_read_loop_trace.py │ ├── errors │ │ ├── __init__.py │ │ └── failing_worker │ │ │ ├── README.md │ │ │ ├── pyproject.toml │ │ │ ├── main.py │ │ │ └── stubs.py │ ├── executor │ │ ├── __init__.py │ │ ├── stubs.py │ │ └── test_hpc_executor.py │ ├── test_labels.py │ ├── idl │ │ ├── namespace1.tsp │ │ ├── namespace1.py │ │ ├── test_idl.py │ │ └── stubs_output.py │ └── conftest.py ├── tierkreis │ ├── py.typed │ ├── cli │ │ ├── __init__.py │ │ └── run_workflow.py │ ├── worker │ │ ├── __init__.py │ │ └── storage │ │ │ ├── __init__.py │ │ │ ├── protocol.py │ │ │ ├── in_memory.py │ │ │ └── filestorage.py │ ├── builtins │ │ └── __init__.py │ ├── controller │ │ ├── data │ │ │ ├── __init__.py │ │ │ └── core.py │ │ ├── executor │ │ │ ├── __init__.py │ │ │ ├── protocol.py │ │ │ ├── multiple.py │ │ │ └── in_memory_executor.py │ │ ├── storage │ │ │ ├── __init__.py │ │ │ ├── adjacency.py │ │ │ ├── in_memory.py │ │ │ └── filestorage.py │ │ └── consts.py │ ├── _version.py │ ├── exceptions.py │ ├── hpc.py │ ├── consts.py │ ├── value.py │ ├── __init__.py │ ├── models.py │ ├── labels.py │ ├── executor.py │ ├── idl │ │ └── spec.py │ └── storage.py ├── .gitignore ├── docs │ ├── api-docs │ │ ├── .gitignore │ │ ├── _templates │ │ │ └── autosummary │ │ │ │ ├── base.rst │ │ │ │ ├── class.rst │ │ │ │ └── module.rst │ │ ├── index.rst │ │ └── conf.py │ └── _static │ │ ├── Quantinuum_logo_black.png │ │ ├── Quantinuum_logo_white.png │ │ └── custom.css ├── pyproject.toml ├── CITATION.bib └── CHANGELOG.md ├── tierkreis_workers ├── __init__.py ├── aer_worker │ ├── __init__.py │ ├── README.md │ └── pyproject.toml ├── nexus_worker │ ├── __init__.py │ ├── .gitignore │ ├── README.md │ └── pyproject.toml ├── pytket_worker │ ├── __init__.py │ ├── README.md │ ├── pyproject.toml │ └── test_main.py ├── ibmq_worker │ ├── README.md │ └── pyproject.toml ├── quantinuum_worker │ ├── README.md │ └── pyproject.toml ├── README.md └── qulacs_worker │ ├── pyproject.toml │ └── main.py ├── examples ├── example_workers │ ├── openssl_worker │ │ ├── test │ │ │ ├── _done │ │ │ ├── numbits │ │ │ ├── .gitignore │ │ │ ├── passphrase │ │ │ ├── args │ │ │ └── tierkreis.sh │ │ └── main.sh │ ├── qsci_worker │ │ ├── qsci │ │ │ ├── __init__.py │ │ │ └── active_space.py │ │ └── pyproject.toml │ ├── chemistry_worker │ │ ├── chemistry │ │ │ ├── __init__.py │ │ │ └── active_space.py │ │ ├── stubs.py │ │ └── main.py │ ├── genrsa │ ├── scipy_worker │ │ ├── pyproject.toml │ │ └── main.py │ ├── substitution_worker │ │ ├── pyproject.toml │ │ ├── main.py │ │ └── stubs.py │ ├── auth_worker │ │ ├── pyproject.toml │ │ └── stubs.py │ ├── error_worker │ │ ├── stubs.py │ │ └── main.py │ └── hello_world_worker │ │ ├── stubs.py │ │ └── main.py ├── data │ ├── simple.qasm │ └── ghz_state_n23.qasm ├── non_standard_tkr_dir.py ├── nexus_polling.py ├── scipy_graph.py ├── simulate_parallel.py ├── hello_world_graph.py ├── error_handling_graph.py └── pytket_graph.py ├── tierkreis_visualization ├── tierkreis_visualization │ ├── py.typed │ ├── __init__.py │ ├── data │ │ ├── __init__.py │ │ ├── models.py │ │ ├── function.py │ │ ├── map.py │ │ ├── workflows.py │ │ └── loop.py │ ├── routers │ │ ├── __init__.py │ │ ├── models.py │ │ ├── frontend.py │ │ └── navigation.py │ ├── static │ │ ├── dist │ │ │ └── .gitignore │ │ ├── eventstream.js │ │ ├── tokens.js │ │ ├── favicon.svg │ │ ├── macros.breadcrumbs.html.css │ │ ├── network.js │ │ ├── utils.js │ │ ├── workflows.html.css │ │ └── _types.js │ ├── templates │ │ ├── index.html │ │ ├── unavailable.jinja │ │ ├── loop.jinja │ │ ├── map.jinja │ │ ├── macros │ │ │ ├── common_head.html │ │ │ └── header.html │ │ ├── workflows.html │ │ ├── function.jinja │ │ ├── fallback.jinja │ │ └── eval.jinja │ ├── config.py │ ├── cli.py │ ├── visualize_graph.py │ ├── main.py │ ├── app_config.py │ ├── app.py │ └── storage.py ├── frontend │ ├── src │ │ ├── vite-env.d.ts │ │ ├── data │ │ │ ├── constants.ts │ │ │ └── logs.ts │ │ ├── lib │ │ │ └── utils.ts │ │ ├── edges │ │ │ ├── types.ts │ │ │ └── index.ts │ │ ├── components │ │ │ ├── ui │ │ │ │ ├── skeleton.tsx │ │ │ │ ├── separator.tsx │ │ │ │ ├── input.tsx │ │ │ │ ├── hover-card.tsx │ │ │ │ └── tooltip.tsx │ │ │ ├── types.ts │ │ │ ├── info.tsx │ │ │ ├── layout.tsx │ │ │ └── workflow-sidebar.tsx │ │ ├── nodes │ │ │ ├── index.ts │ │ │ ├── types.ts │ │ │ └── hide_children.ts │ │ ├── hooks │ │ │ └── use-mobile.ts │ │ ├── main.tsx │ │ └── error.tsx │ ├── tsconfig.node.json │ ├── .gitignore │ ├── index.html │ ├── vite.config.ts │ ├── .eslintrc.cjs │ ├── components.json │ ├── tsconfig.json │ ├── LICENSE │ ├── package.json │ └── README.md ├── .gitignore ├── README.md └── pyproject.toml ├── pyrightconfig.json ├── devenv.yaml ├── docs ├── source │ ├── _static │ │ └── visualization │ │ │ ├── Expanded.png │ │ │ ├── overview.png │ │ │ ├── Debugging.png │ │ │ └── node_states.png │ ├── worker │ │ ├── native_workers │ │ │ └── index.md │ │ ├── hello_stubs.py │ │ └── index.md │ ├── installation.md │ ├── index.md │ ├── getting_started.md │ ├── conf.py │ └── tutorial │ │ ├── auth_stubs.py │ │ └── eval.md └── justfile ├── infra └── slurm_local │ ├── register_cluster.sh │ ├── .env │ ├── update_slurmfiles.sh │ ├── sbatch │ ├── USAGE.md │ ├── slurmdbd.conf │ ├── LICENSE │ ├── main.py │ └── docker-entrypoint.sh ├── .github ├── dependabot.yml ├── ISSUE_TEMPLATE │ ├── feature_request.md │ └── bug_report.md ├── workflows │ ├── push_main.yml │ ├── run_check_matrix.yml │ ├── release.yml │ ├── pr_main.yml │ └── ci.yml └── actions │ └── setup_deps │ └── action.yml ├── .dockerignore ├── .cargo └── config.toml ├── .envrc ├── pyproject.toml ├── devenv.nix └── README.md /tierkreis/tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tierkreis/tests/py.typed: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tierkreis/tierkreis/py.typed: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tierkreis/.gitignore: -------------------------------------------------------------------------------- 1 | ./venv 2 | -------------------------------------------------------------------------------- /tierkreis/tests/cli/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tierkreis/tests/cli/data/n: -------------------------------------------------------------------------------- 1 | 5 2 | -------------------------------------------------------------------------------- /tierkreis_workers/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tierkreis/tests/controller/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tierkreis/tests/errors/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tierkreis/tests/executor/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tierkreis/tierkreis/cli/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tierkreis/tierkreis/worker/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tierkreis/tests/cli/data/input1: -------------------------------------------------------------------------------- 1 | test 2 | -------------------------------------------------------------------------------- /tierkreis/tierkreis/builtins/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tierkreis_workers/aer_worker/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tierkreis_workers/nexus_worker/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tierkreis_workers/pytket_worker/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tierkreis/tierkreis/controller/data/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tierkreis/tierkreis/worker/storage/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/example_workers/openssl_worker/test/_done: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/example_workers/qsci_worker/qsci/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tierkreis/tierkreis/controller/executor/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tierkreis/tierkreis/controller/storage/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/example_workers/openssl_worker/test/numbits: -------------------------------------------------------------------------------- 1 | 4096 -------------------------------------------------------------------------------- /tierkreis/tests/cli/data/input2: -------------------------------------------------------------------------------- 1 | {"a": 5, "b": "string"} -------------------------------------------------------------------------------- /tierkreis/tierkreis/_version.py: -------------------------------------------------------------------------------- 1 | __version__ = "0.7.3" 2 | -------------------------------------------------------------------------------- /tierkreis_visualization/tierkreis_visualization/py.typed: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tierkreis_workers/aer_worker/README.md: -------------------------------------------------------------------------------- 1 | # aer_worker 2 | -------------------------------------------------------------------------------- /tierkreis_workers/nexus_worker/.gitignore: -------------------------------------------------------------------------------- 1 | .env 2 | venv -------------------------------------------------------------------------------- /examples/example_workers/chemistry_worker/chemistry/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/example_workers/openssl_worker/test/.gitignore: -------------------------------------------------------------------------------- 1 | outputs -------------------------------------------------------------------------------- /tierkreis_visualization/tierkreis_visualization/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tierkreis_workers/pytket_worker/README.md: -------------------------------------------------------------------------------- 1 | # pytket_worker 2 | -------------------------------------------------------------------------------- /tierkreis_visualization/tierkreis_visualization/data/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tierkreis_visualization/tierkreis_visualization/routers/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/example_workers/openssl_worker/test/passphrase: -------------------------------------------------------------------------------- 1 | "dummypassphrase" -------------------------------------------------------------------------------- /tierkreis/docs/api-docs/.gitignore: -------------------------------------------------------------------------------- 1 | _autosummary 2 | _build 3 | generated -------------------------------------------------------------------------------- /pyrightconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "reportUnnecessaryIsInstance": "none" 3 | } 4 | -------------------------------------------------------------------------------- /tierkreis_workers/ibmq_worker/README.md: -------------------------------------------------------------------------------- 1 | # IBMQ Backend Compilation Worker 2 | -------------------------------------------------------------------------------- /tierkreis/tests/errors/failing_worker/README.md: -------------------------------------------------------------------------------- 1 | A simple worker that just fails. -------------------------------------------------------------------------------- /tierkreis_visualization/tierkreis_visualization/static/dist/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore -------------------------------------------------------------------------------- /devenv.yaml: -------------------------------------------------------------------------------- 1 | inputs: 2 | nixpkgs: 3 | url: github:NixOS/nixpkgs/nixpkgs-unstable 4 | -------------------------------------------------------------------------------- /tierkreis_visualization/frontend/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /tierkreis_workers/quantinuum_worker/README.md: -------------------------------------------------------------------------------- 1 | # Quantinuum Backend Compilation Worker 2 | -------------------------------------------------------------------------------- /tierkreis/tests/cli/data/data.json: -------------------------------------------------------------------------------- 1 | { 2 | "a_value" : 5, 3 | "a_string": "string" 4 | } 5 | 6 | -------------------------------------------------------------------------------- /tierkreis_workers/README.md: -------------------------------------------------------------------------------- 1 | # tierkreis_workers 2 | 3 | A collection of workers for tierkreis. 4 | -------------------------------------------------------------------------------- /examples/example_workers/genrsa: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | openssl genrsa --aes128 --passout stdin 4096 3 | -------------------------------------------------------------------------------- /tierkreis/tierkreis/exceptions.py: -------------------------------------------------------------------------------- 1 | class TierkreisError(Exception): 2 | """An error thrown in the Tierkreis library.""" 3 | -------------------------------------------------------------------------------- /docs/source/_static/visualization/Expanded.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Quantinuum/tierkreis/HEAD/docs/source/_static/visualization/Expanded.png -------------------------------------------------------------------------------- /docs/source/_static/visualization/overview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Quantinuum/tierkreis/HEAD/docs/source/_static/visualization/overview.png -------------------------------------------------------------------------------- /docs/source/_static/visualization/Debugging.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Quantinuum/tierkreis/HEAD/docs/source/_static/visualization/Debugging.png -------------------------------------------------------------------------------- /tierkreis/docs/_static/Quantinuum_logo_black.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Quantinuum/tierkreis/HEAD/tierkreis/docs/_static/Quantinuum_logo_black.png -------------------------------------------------------------------------------- /tierkreis/docs/_static/Quantinuum_logo_white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Quantinuum/tierkreis/HEAD/tierkreis/docs/_static/Quantinuum_logo_white.png -------------------------------------------------------------------------------- /docs/source/_static/visualization/node_states.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Quantinuum/tierkreis/HEAD/docs/source/_static/visualization/node_states.png -------------------------------------------------------------------------------- /tierkreis/docs/api-docs/_templates/autosummary/base.rst: -------------------------------------------------------------------------------- 1 | {{ name | escape | underline}} 2 | 3 | .. currentmodule:: {{ module }} 4 | 5 | .. auto{{ objtype }}:: {{ objname }} 6 | -------------------------------------------------------------------------------- /tierkreis/tierkreis/hpc.py: -------------------------------------------------------------------------------- 1 | from tierkreis.controller.executor.hpc.job_spec import JobSpec, ResourceSpec, MpiSpec 2 | 3 | __all__ = ["JobSpec", "ResourceSpec", "MpiSpec"] 4 | -------------------------------------------------------------------------------- /tierkreis/tierkreis/controller/consts.py: -------------------------------------------------------------------------------- 1 | import os 2 | from pathlib import Path 3 | 4 | BODY_PORT = "body" 5 | PACKAGE_PATH = Path(os.path.dirname(os.path.realpath(__file__))) 6 | -------------------------------------------------------------------------------- /tierkreis/tierkreis/consts.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | 3 | 4 | PACKAGE_PATH = Path(__file__).parent.parent 5 | TESTS_PATH = PACKAGE_PATH / "tests" 6 | TKR_DIR_KEY = "TKR_DIR" 7 | -------------------------------------------------------------------------------- /tierkreis/tests/test_labels.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from tierkreis import Labels 4 | 5 | 6 | def test_instantiate_raises() -> None: 7 | with pytest.raises(RuntimeError): 8 | Labels() 9 | -------------------------------------------------------------------------------- /infra/slurm_local/register_cluster.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | docker exec slurmctld bash -c "/usr/bin/sacctmgr --immediate add cluster name=linux" && \ 5 | docker-compose restart slurmdbd slurmctld 6 | -------------------------------------------------------------------------------- /tierkreis_visualization/.gitignore: -------------------------------------------------------------------------------- 1 | # Python-generated files 2 | __pycache__/ 3 | *.py[oc] 4 | build/ 5 | dist/* 6 | wheels/ 7 | *.egg-info 8 | .idea 9 | 10 | # Virtual environments 11 | .venv 12 | 13 | -------------------------------------------------------------------------------- /tierkreis/tierkreis/value.py: -------------------------------------------------------------------------------- 1 | from typing import Generic, TypeVar 2 | 3 | from pydantic import BaseModel 4 | 5 | 6 | T = TypeVar("T") 7 | 8 | 9 | class Value(BaseModel, Generic[T]): 10 | value: T 11 | -------------------------------------------------------------------------------- /tierkreis_visualization/frontend/src/data/constants.ts: -------------------------------------------------------------------------------- 1 | export const URL = "http://localhost:8000/workflows"; 2 | export const nodeName = "-"; 3 | export const nodeWidth = 200; 4 | export const nodeHeight = 150; 5 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "uv" 4 | directory: "/" 5 | schedule: 6 | interval: "weekly" 7 | labels: 8 | - "python" 9 | - "dependencies" 10 | -------------------------------------------------------------------------------- /tierkreis_visualization/frontend/src/lib/utils.ts: -------------------------------------------------------------------------------- 1 | import { clsx, type ClassValue } from "clsx" 2 | import { twMerge } from "tailwind-merge" 3 | 4 | export function cn(...inputs: ClassValue[]) { 5 | return twMerge(clsx(inputs)) 6 | } 7 | -------------------------------------------------------------------------------- /tierkreis_visualization/frontend/src/edges/types.ts: -------------------------------------------------------------------------------- 1 | export type PyEdge = { 2 | id: string | number; 3 | from_node: string | number; 4 | to_node: string | number; 5 | from_port: string; 6 | to_port: string; 7 | value?: unknown; 8 | }; 9 | -------------------------------------------------------------------------------- /tierkreis_workers/nexus_worker/README.md: -------------------------------------------------------------------------------- 1 | # Nexus worker 2 | 3 | ## Authentication 4 | 5 | The Nexus API requires re-authentication every 30 days. 6 | 7 | ```bash 8 | uv run python -c "from qnexus.client.auth import login; login()" 9 | ``` 10 | -------------------------------------------------------------------------------- /tierkreis_visualization/tierkreis_visualization/routers/models.py: -------------------------------------------------------------------------------- 1 | from pydantic import BaseModel 2 | from tierkreis_visualization.data.models import PyNode, PyEdge 3 | 4 | 5 | class PyGraph(BaseModel): 6 | nodes: list[PyNode] 7 | edges: list[PyEdge] 8 | -------------------------------------------------------------------------------- /tierkreis/tierkreis/__init__.py: -------------------------------------------------------------------------------- 1 | from tierkreis.labels import Labels 2 | from tierkreis.worker.worker import Worker 3 | from tierkreis.value import Value 4 | from tierkreis.controller import run_graph 5 | 6 | __all__ = ["Labels", "Worker", "Value", "run_graph"] 7 | -------------------------------------------------------------------------------- /infra/slurm_local/.env: -------------------------------------------------------------------------------- 1 | # Slurm git repo tag. See https://github.com/SchedMD/slurm/tags 2 | SLURM_TAG=slurm-21-08-6-1 3 | 4 | # Image version used to tag the container at build time (Typically matches the 5 | # Slurm tag in semantic version form) 6 | IMAGE_TAG=21.08.6 7 | -------------------------------------------------------------------------------- /tierkreis/tierkreis/models.py: -------------------------------------------------------------------------------- 1 | from tierkreis.controller.data.core import EmptyModel 2 | from tierkreis.controller.data.models import TKR, portmapping 3 | from tierkreis.controller.data.types import Struct 4 | 5 | __all__ = ["EmptyModel", "TKR", "portmapping", "Struct"] 6 | -------------------------------------------------------------------------------- /docs/justfile: -------------------------------------------------------------------------------- 1 | sphinx_opts := "" 2 | sphinx_build := "uv run sphinx-build" 3 | source_dir := "source" 4 | build_dir := "build" 5 | 6 | default: 7 | @just --list 8 | 9 | build: 10 | {{sphinx_build}} -M html "{{source_dir}}" "{{build_dir}}" {{sphinx_opts}} 11 | -------------------------------------------------------------------------------- /examples/data/simple.qasm: -------------------------------------------------------------------------------- 1 | OPENQASM 2.0; 2 | include "hqslib1.inc"; 3 | 4 | qreg q[2]; 5 | creg c[2]; 6 | U1q(0.20000000000000012*pi,0.0*pi) q[0]; 7 | U1q(0.5*pi,0.5*pi) q[1]; 8 | RZZ(0.5*pi) q[0],q[1]; 9 | measure q[0] -> c[0]; 10 | U1q(0.5*pi,0.0*pi) q[1]; 11 | measure q[1] -> c[1]; -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | 2 | **/target 3 | **/.git 4 | **/.venv 5 | **/venv 6 | **/.vscode 7 | **/.pytest-cache 8 | **/.mypy-cache 9 | **/__pycache__ 10 | **/.direnv 11 | **/.DS_Store 12 | **/Thumbs.db 13 | 14 | **/.ruff_cache 15 | **/build 16 | **/tests 17 | **/Dockerfile 18 | .env 19 | cqcpython-secret.txt -------------------------------------------------------------------------------- /examples/example_workers/scipy_worker/pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "scipy-worker" 3 | version = "0.1.0" 4 | requires-python = ">=3.12" 5 | dependencies = [ 6 | "scipy>=1.16.3", 7 | "tierkreis", 8 | ] 9 | 10 | [tool.uv.sources] 11 | tierkreis = { path = "../../../tierkreis", editable = true } 12 | -------------------------------------------------------------------------------- /tierkreis_visualization/frontend/src/edges/index.ts: -------------------------------------------------------------------------------- 1 | import type { Edge, EdgeTypes } from "@xyflow/react"; 2 | import CustomEdge from "./customEdge"; 3 | export const initialEdges = [] as Edge[]; 4 | export const edgeTypes = { 5 | "custom-edge": CustomEdge, 6 | // Add your custom edge types here! 7 | } satisfies EdgeTypes; 8 | -------------------------------------------------------------------------------- /examples/example_workers/substitution_worker/pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "substitution-worker" 3 | version = "0.1.0" 4 | requires-python = ">=3.12" 5 | dependencies = [ 6 | "pydantic>=2.11.7", 7 | "pytket>=2.9.3", 8 | "tierkreis", 9 | ] 10 | 11 | [tool.uv.sources] 12 | tierkreis = { path = "../../../tierkreis" } 13 | -------------------------------------------------------------------------------- /tierkreis/tests/controller/test_graphdata.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from tierkreis.exceptions import TierkreisError 3 | from tierkreis.controller.data.graph import GraphData 4 | 5 | 6 | def test_only_one_output(): 7 | with pytest.raises(TierkreisError): 8 | g = GraphData() 9 | g.output({"one": g.const(1)}) 10 | g.output({"two": g.const(2)}) 11 | -------------------------------------------------------------------------------- /examples/example_workers/auth_worker/pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "auth-worker" 3 | version = "0.1.0" 4 | requires-python = ">=3.12" 5 | dependencies = [ 6 | "cryptography>=45.0.7", 7 | "pydantic>=2.11.7", 8 | "pyscrypt>=1.6.2", 9 | "tierkreis", 10 | ] 11 | 12 | [tool.uv.sources] 13 | tierkreis = { path = "../../../tierkreis", editable = true } 14 | -------------------------------------------------------------------------------- /tierkreis/tierkreis/labels.py: -------------------------------------------------------------------------------- 1 | """Core Tierkreis Label type.""" 2 | 3 | 4 | class Labels: 5 | """Special port labels used by builtin functions.""" 6 | 7 | def __init__(self): 8 | raise RuntimeError("Do not instantiate") 9 | 10 | THUNK = "thunk" 11 | VALUE = "value" 12 | VARIANT_VALUE = "variant_value" 13 | CONTINUE = "continue" 14 | BREAK = "break" 15 | -------------------------------------------------------------------------------- /tierkreis_visualization/frontend/src/components/ui/skeleton.tsx: -------------------------------------------------------------------------------- 1 | import { cn } from "@/lib/utils" 2 | 3 | function Skeleton({ className, ...props }: React.ComponentProps<"div">) { 4 | return ( 5 |
10 | ) 11 | } 12 | 13 | export { Skeleton } 14 | -------------------------------------------------------------------------------- /tierkreis/tierkreis/executor.py: -------------------------------------------------------------------------------- 1 | from tierkreis.controller.executor.shell_executor import ShellExecutor 2 | from tierkreis.controller.executor.uv_executor import UvExecutor 3 | from tierkreis.controller.executor.multiple import MultipleExecutor 4 | from tierkreis.controller.executor.hpc.pjsub import PJSUBExecutor 5 | 6 | 7 | __all__ = ["ShellExecutor", "UvExecutor", "MultipleExecutor", "PJSUBExecutor"] 8 | -------------------------------------------------------------------------------- /examples/example_workers/openssl_worker/main.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | ## To test run source ./test/tierkreis.sh ./test/args && ./main.sh 4 | 5 | numbits=$(cat $input_numbits_file) 6 | openssl genrsa -out $output_private_key_file -aes128 -passout "file:$input_passphrase_file" $numbits 7 | openssl rsa -in $output_private_key_file -passin "file:$input_passphrase_file" -pubout -out $output_public_key_file 8 | -------------------------------------------------------------------------------- /examples/example_workers/qsci_worker/pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "qsci-worker" 3 | version = "0.1.0" 4 | requires-python = ">=3.12" 5 | dependencies = [ 6 | "pydantic>=2.11.9", 7 | "pytket>=2.9.3", 8 | "pytket-qiskit>=0.71.0", 9 | "qiskit-ibm-runtime==0.41.1", 10 | "tierkreis", 11 | ] 12 | 13 | [tool.uv.sources] 14 | tierkreis = { path = "../../../tierkreis", editable = true } 15 | -------------------------------------------------------------------------------- /tierkreis/tests/errors/failing_worker/pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "failing_worker" 3 | version = "0.1.0" 4 | description = "Worker that will always fail. Used for error testing." 5 | readme = "README.md" 6 | requires-python = ">=3.12" 7 | dependencies = [ 8 | "pydantic>=2.10.6", 9 | "tierkreis", 10 | ] 11 | 12 | [tool.uv.sources] 13 | tierkreis = { path = "../../../", editable = true } 14 | -------------------------------------------------------------------------------- /tierkreis_visualization/frontend/tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "skipLibCheck": true, 5 | "module": "ESNext", 6 | "moduleResolution": "bundler", 7 | "allowSyntheticDefaultImports": true 8 | }, 9 | "include": ["vite.config.ts"], 10 | "baseUrl": ".", 11 | "paths": { 12 | "@/*": [ 13 | "./src/*" 14 | ] 15 | } 16 | 17 | } 18 | -------------------------------------------------------------------------------- /tierkreis_visualization/frontend/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | !lib 26 | -------------------------------------------------------------------------------- /tierkreis_visualization/frontend/src/components/types.ts: -------------------------------------------------------------------------------- 1 | export interface InfoProps { 2 | type: "Logs" | "Errors"; 3 | content: string; 4 | } 5 | export interface Workflow { 6 | id: string; 7 | name: string; 8 | start_time: string; 9 | } 10 | 11 | export interface HandleProps { 12 | handles: string[]; 13 | id: string; 14 | isOpen: boolean; 15 | hoveredId: string; 16 | setHoveredId: (id: string) => void; 17 | } 18 | -------------------------------------------------------------------------------- /infra/slurm_local/update_slurmfiles.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -e 4 | 5 | restart=false 6 | 7 | for var in "$@" 8 | do 9 | if [ "$var" = "slurmdbd.conf" ] || [ "$var" = "slurm.conf" ] 10 | then 11 | export SLURM_TMP=$(cat $var) 12 | docker exec slurmctld bash -c "echo \"$SLURM_TMP\" >/etc/slurm/\"$var\"" 13 | restart=true 14 | fi 15 | done 16 | if $restart; then docker-compose restart; fi 17 | -------------------------------------------------------------------------------- /tierkreis_visualization/frontend/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Tierkreis Frontend 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /docs/source/worker/native_workers/index.md: -------------------------------------------------------------------------------- 1 | # Prepackaged workers 2 | 3 | These workers are natively available as stubs for use in Tierkreis Graphs. 4 | 5 | ```{warning} 6 | Currently only stubs are included. To use the workers during execution they need to be installed first. 7 | ``` 8 | 9 | ```{toctree} 10 | aer_worker.md 11 | ibmq_worker.md 12 | nexus_worker.md 13 | pytket_worker.md 14 | quantinuum_worker.md 15 | qulacs_worker.md 16 | ``` 17 | -------------------------------------------------------------------------------- /tierkreis/docs/api-docs/index.rst: -------------------------------------------------------------------------------- 1 | Tierkreis Python API Documentation 2 | ================================== 3 | 4 | This is the API documentation for the Tierkreis Python package. 5 | 6 | 7 | .. autosummary:: 8 | :toctree: generated 9 | :template: autosummary/module.rst 10 | :recursive: 11 | 12 | tierkreis 13 | 14 | 15 | Indices and tables 16 | ~~~~~~~~~~~~~~~~~~ 17 | 18 | * :ref:`genindex` 19 | * :ref:`modindex` 20 | * :ref:`search` 21 | -------------------------------------------------------------------------------- /.cargo/config.toml: -------------------------------------------------------------------------------- 1 | [registries.artifactory] 2 | index = "https://quantinuumsw.jfrog.io/artifactory/git/rust_local.git" 3 | 4 | [net] 5 | git-fetch-with-cli = true 6 | 7 | # This allows building the type-check (pyo3) module on MacOSX "Apple Silicon" 8 | # (cargo build from root directory ignores any config.toml in subdirectories.) 9 | [target.aarch64-apple-darwin] 10 | rustflags = [ 11 | "-C", "link-arg=-undefined", 12 | "-C", "link-arg=dynamic_lookup", 13 | ] 14 | -------------------------------------------------------------------------------- /examples/example_workers/openssl_worker/test/args: -------------------------------------------------------------------------------- 1 | { 2 | "function_name": "genrsa", 3 | "inputs": { 4 | "passphrase": "./test/passphrase", 5 | "numbits": "./test/numbits" 6 | }, 7 | "outputs": { 8 | "public_key": "./test/outputs/public_key", 9 | "private_key": "./test/outputs/private_key" 10 | }, 11 | "output_dir": "./test/outputs", 12 | "done_path": "./test/_done", 13 | "error_path": "./test/_error", 14 | "logs_path": "./test/logs" 15 | } 16 | -------------------------------------------------------------------------------- /tierkreis_visualization/frontend/src/nodes/index.ts: -------------------------------------------------------------------------------- 1 | import type { NodeTypes } from "@xyflow/react"; 2 | 3 | import { DefaultNode } from "./default"; 4 | import { EvalNode } from "./eval"; 5 | import { MapNode } from "./map"; 6 | import { AppNode } from "./types"; 7 | 8 | export const initialNodes = [] as AppNode[]; 9 | 10 | export const nodeTypes = { 11 | "eval-node": EvalNode, 12 | "map-node": MapNode, 13 | "default-node": DefaultNode, 14 | } satisfies NodeTypes; 15 | -------------------------------------------------------------------------------- /examples/example_workers/error_worker/stubs.py: -------------------------------------------------------------------------------- 1 | """Code generated from error_worker namespace. Please do not edit.""" 2 | 3 | from typing import NamedTuple 4 | from tierkreis.controller.data.models import TKR 5 | 6 | 7 | class fail(NamedTuple): 8 | @staticmethod 9 | def out() -> type[TKR[str]]: # noqa: F821 # fmt: skip 10 | return TKR[str] # noqa: F821 # fmt: skip 11 | 12 | @property 13 | def namespace(self) -> str: 14 | return "error_worker" 15 | -------------------------------------------------------------------------------- /docs/source/installation.md: -------------------------------------------------------------------------------- 1 | # Installation 2 | 3 | Tierkreis consist of multiple packages: 4 | 5 | - `tierkreis`: the core functionality including the 6 | - `tierkreis-visualization`: the visualization library which is necessary for the debug view 7 | - `tierkreis-*-worker`: independent [workers](./core_concepts) 8 | 9 | The libraries can be installed with pip: 10 | ``` 11 | pip install tierkreis 12 | pip install tierkreis-visualization 13 | pip install tkr-*-worker 14 | ``` 15 | 16 | -------------------------------------------------------------------------------- /tierkreis_visualization/tierkreis_visualization/templates/index.html: -------------------------------------------------------------------------------- 1 | {% from "macros/common_head.html" import common_head_macro with context %} 2 | {% from "macros/header.html" import header_macro with context %} 3 | 4 | 5 | Tierkreis visualization 6 | {{ common_head_macro() }} 7 | 8 | 9 | 10 | {{ header_macro() }} 11 |
12 | All running workflows 13 |
14 | 15 | 16 | -------------------------------------------------------------------------------- /tierkreis/tests/executor/stubs.py: -------------------------------------------------------------------------------- 1 | """Code generated from slurm_mpi_worker namespace. Please do not edit.""" 2 | 3 | from typing import NamedTuple 4 | from types import NoneType 5 | from tierkreis.controller.data.models import TKR 6 | 7 | 8 | class mpi_rank_info(NamedTuple): 9 | @staticmethod 10 | def out() -> type[TKR[str | NoneType]]: # noqa: F821 # fmt: skip 11 | return TKR[str | NoneType] # noqa: F821 # fmt: skip 12 | 13 | @property 14 | def namespace(self) -> str: 15 | return "slurm_mpi_worker" 16 | -------------------------------------------------------------------------------- /tierkreis_visualization/frontend/vite.config.ts: -------------------------------------------------------------------------------- 1 | import path from "path"; 2 | import tailwindcss from "@tailwindcss/vite"; 3 | import { defineConfig } from "vite"; 4 | import react from "@vitejs/plugin-react"; 5 | 6 | // https://vitejs.dev/config/ 7 | export default defineConfig({ 8 | plugins: [react(), tailwindcss()], 9 | build: { 10 | outDir: "../tierkreis_visualization/static/dist", 11 | }, 12 | resolve: { 13 | alias: { 14 | "@": path.resolve(__dirname, "./src"), 15 | }, 16 | }, 17 | base: "", 18 | }); 19 | -------------------------------------------------------------------------------- /tierkreis_visualization/tierkreis_visualization/config.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | 3 | from pydantic_settings import BaseSettings 4 | from starlette.templating import Jinja2Templates 5 | 6 | 7 | class Settings(BaseSettings): 8 | tierkreis_path: Path = Path.home() / ".tierkreis" / "checkpoints" 9 | graph_specifier: str | None = None 10 | 11 | 12 | CONFIG = Settings() 13 | CONFIG.tierkreis_path.mkdir(parents=True, exist_ok=True) 14 | templates = Jinja2Templates(directory=(Path(__file__).parent / "templates").absolute()) 15 | -------------------------------------------------------------------------------- /tierkreis_visualization/tierkreis_visualization/static/eventstream.js: -------------------------------------------------------------------------------- 1 | function connectToStream(url, nodes, edges) { 2 | let eventSource = new EventSource(url); 3 | eventSource.onopen = (ev) => { 4 | console.log("opening event source"); 5 | console.log(ev); 6 | }; 7 | eventSource.addEventListener("message", (ev) => { 8 | data = JSON.parse(ev["data"]); 9 | for (let node in data.nodes) { 10 | jsNode = createJSNode(data.nodes[node]); 11 | nodes.update(jsNode); 12 | network.redraw(); 13 | } 14 | }); 15 | } 16 | -------------------------------------------------------------------------------- /tierkreis_visualization/frontend/.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { browser: true, es2020: true }, 4 | extends: [ 5 | 'eslint:recommended', 6 | 'plugin:@typescript-eslint/recommended', 7 | 'plugin:react-hooks/recommended', 8 | ], 9 | ignorePatterns: ['dist', '.eslintrc.cjs'], 10 | parser: '@typescript-eslint/parser', 11 | plugins: ['react-refresh'], 12 | rules: { 13 | 'react-refresh/only-export-components': [ 14 | 'warn', 15 | { allowConstantExport: true }, 16 | ], 17 | }, 18 | } 19 | -------------------------------------------------------------------------------- /tierkreis_visualization/frontend/components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "new-york", 4 | "rsc": false, 5 | "tsx": true, 6 | "tailwind": { 7 | "config": "", 8 | "css": "src/index.css", 9 | "baseColor": "neutral", 10 | "cssVariables": true, 11 | "prefix": "" 12 | }, 13 | "aliases": { 14 | "components": "@/components", 15 | "utils": "@/lib/utils", 16 | "ui": "@/components/ui", 17 | "lib": "@/lib", 18 | "hooks": "@/hooks" 19 | }, 20 | "iconLibrary": "lucide" 21 | } -------------------------------------------------------------------------------- /tierkreis/tests/errors/failing_worker/main.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from sys import argv 3 | from tierkreis import Worker 4 | 5 | logger = logging.getLogger(__name__) 6 | worker = Worker("failing_worker") 7 | 8 | 9 | @worker.task() 10 | def fail() -> int: 11 | logger.error("Raising an error now...") 12 | raise ValueError("Worker failed!") 13 | 14 | 15 | @worker.task() 16 | def wont_fail() -> int: 17 | return 0 18 | 19 | 20 | @worker.task() 21 | def exit_code_1() -> int: 22 | exit(1) 23 | 24 | 25 | if __name__ == "__main__": 26 | worker.app(argv) 27 | -------------------------------------------------------------------------------- /tierkreis_visualization/tierkreis_visualization/routers/frontend.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | from fastapi import APIRouter 3 | from fastapi.responses import FileResponse 4 | from fastapi.staticfiles import StaticFiles 5 | 6 | PACKAGE_DIR = Path(__file__).parent.parent.absolute() 7 | assets = StaticFiles(directory=PACKAGE_DIR / "static" / "dist" / "assets", html=True) 8 | router = APIRouter() 9 | 10 | 11 | @router.get("/{path:path}") 12 | def read_root(): 13 | return FileResponse( 14 | PACKAGE_DIR / "static" / "dist" / "index.html", media_type="text/html" 15 | ) 16 | -------------------------------------------------------------------------------- /docs/source/worker/hello_stubs.py: -------------------------------------------------------------------------------- 1 | """Code generated from hello_world_worker namespace. Please do not edit.""" 2 | 3 | from typing import NamedTuple 4 | from tierkreis.controller.data.models import TKR 5 | 6 | 7 | class greet(NamedTuple): 8 | greeting: TKR[str] # noqa: F821 # fmt: skip 9 | subject: TKR[str] # noqa: F821 # fmt: skip 10 | 11 | @staticmethod 12 | def out() -> type[TKR[str]]: # noqa: F821 # fmt: skip 13 | return TKR[str] # noqa: F821 # fmt: skip 14 | 15 | @property 16 | def namespace(self) -> str: 17 | return "hello_world_worker" 18 | -------------------------------------------------------------------------------- /examples/example_workers/substitution_worker/main.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from sys import argv 3 | 4 | 5 | from tierkreis import Worker 6 | from pytket._tket.circuit import Circuit 7 | from sympy import Symbol 8 | 9 | logger = logging.getLogger(__name__) 10 | 11 | worker = Worker("substitution_worker") 12 | 13 | 14 | @worker.task() 15 | def substitute(circuit: Circuit, a: float, b: float, c: float) -> Circuit: 16 | circuit.symbol_substitution({Symbol("a"): a, Symbol("b"): b, Symbol("c"): c}) 17 | return circuit 18 | 19 | 20 | if __name__ == "__main__": 21 | worker.app(argv) 22 | -------------------------------------------------------------------------------- /examples/example_workers/error_worker/main.py: -------------------------------------------------------------------------------- 1 | # /// script 2 | # requires-python = ">=3.12" 3 | # dependencies = ["pydantic", "tierkreis"] 4 | # 5 | # [tool.uv.sources] 6 | # tierkreis = { path = "../../../tierkreis", editable = true } 7 | # /// 8 | import logging 9 | from sys import argv 10 | from tierkreis import Worker 11 | 12 | logger = logging.getLogger(__name__) 13 | worker = Worker("error_worker") 14 | 15 | 16 | @worker.task() 17 | def fail() -> str: 18 | raise Exception("I refuse!") 19 | return "I failed to refuse" 20 | 21 | 22 | if __name__ == "__main__": 23 | worker.app(argv) 24 | -------------------------------------------------------------------------------- /examples/example_workers/hello_world_worker/stubs.py: -------------------------------------------------------------------------------- 1 | """Code generated from hello_world_worker namespace. Please do not edit.""" 2 | 3 | from typing import NamedTuple 4 | from tierkreis.controller.data.models import TKR 5 | 6 | 7 | class greet(NamedTuple): 8 | greeting: TKR[str] # noqa: F821 # fmt: skip 9 | subject: TKR[str] # noqa: F821 # fmt: skip 10 | 11 | @staticmethod 12 | def out() -> type[TKR[str]]: # noqa: F821 # fmt: skip 13 | return TKR[str] # noqa: F821 # fmt: skip 14 | 15 | @property 16 | def namespace(self) -> str: 17 | return "hello_world_worker" 18 | -------------------------------------------------------------------------------- /tierkreis_visualization/tierkreis_visualization/static/tokens.js: -------------------------------------------------------------------------------- 1 | const colors = { 2 | neutral :{ 3 | 0: "#ffffff", 4 | 50 :"#fafafa", 5 | 100 :"#f4f4f5", 6 | 200 :"#e4e4e7", 7 | 300 :"#d4d4d8", 8 | 400 :"#a1a1aa", 9 | 500 :"#71717a", 10 | 600 :"#52525b", 11 | 700 :"#3f3f46", 12 | 800: "#27272a", 13 | 900 :"#18181b", 14 | 950 :"#09090b", 15 | }, 16 | green: { 17 | primary: "#bbf7d0" 18 | }, 19 | red: { 20 | primary: "#fca5a5" 21 | }, 22 | amber: { 23 | primary: "#fde68a" 24 | } 25 | } 26 | 27 | -------------------------------------------------------------------------------- /tierkreis/tests/idl/namespace1.tsp: -------------------------------------------------------------------------------- 1 | model ListItem { 2 | i: uint8; 3 | } 4 | 5 | model NestedType { 6 | city: string; 7 | } 8 | 9 | model IncludedType { 10 | nested: NestedType; 11 | } 12 | 13 | @portmapping 14 | model A { 15 | name: Record; 16 | age: uint8; 17 | } 18 | 19 | model B { 20 | name: Record; 21 | age: uint8; 22 | } 23 | 24 | model C { 25 | a: Array; 26 | b: B; 27 | t: T; 28 | included: IncludedType; 29 | ol: Array; 30 | } 31 | 32 | interface TestNamespace { 33 | foo(a: integer, b: string): A; 34 | bar(): B; 35 | z(c: C): C; 36 | } 37 | -------------------------------------------------------------------------------- /tierkreis_visualization/tierkreis_visualization/static/favicon.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /tierkreis_visualization/tierkreis_visualization/data/models.py: -------------------------------------------------------------------------------- 1 | from typing import Literal, Any 2 | from pydantic import BaseModel 3 | 4 | NodeStatus = Literal["Not started", "Started", "Error", "Finished"] 5 | 6 | 7 | class PyNode(BaseModel): 8 | id: int 9 | status: NodeStatus 10 | function_name: str 11 | node_location: str = "" 12 | value: Any | None = None 13 | started_time: str 14 | finished_time: str 15 | 16 | 17 | class PyEdge(BaseModel): 18 | from_node: int 19 | from_port: str 20 | to_node: int 21 | to_port: str 22 | value: Any | None = None 23 | conditional: bool = False 24 | -------------------------------------------------------------------------------- /tierkreis/tests/conftest.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | 4 | def pytest_addoption(parser): 5 | parser.addoption( 6 | "--runslow", action="store_true", default=False, help="run slow tests" 7 | ) 8 | 9 | 10 | def pytest_configure(config): 11 | config.addinivalue_line("markers", "slow: mark test as slow to run") 12 | 13 | 14 | def pytest_collection_modifyitems(config, items): 15 | if config.getoption("--runslow"): 16 | return 17 | skip_slow = pytest.mark.skip(reason="need --runslow option to run") 18 | for item in items: 19 | if "slow" in item.keywords: 20 | item.add_marker(skip_slow) 21 | -------------------------------------------------------------------------------- /tierkreis_visualization/tierkreis_visualization/static/macros.breadcrumbs.html.css: -------------------------------------------------------------------------------- 1 | .badge { 2 | padding: 0.25rem 0.5rem; 3 | background-color: var(--neutral-0); 4 | border-radius: 0.5rem; 5 | color: var(--neutral-700); 6 | font-size: 0.75rem; 7 | font-weight: 500; 8 | letter-spacing: -0.02em; 9 | text-decoration: none; 10 | display: block; 11 | width: min-content; 12 | text-wrap: nowrap; 13 | border: 1px solid var(--neutral-300); 14 | 15 | } 16 | .badge:hover { 17 | background-color: var(--neutral-100); 18 | transition-property: background-color; 19 | transition-duration: 150ms; 20 | } 21 | -------------------------------------------------------------------------------- /tierkreis_workers/qulacs_worker/pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "tkr_qulacs_worker" 3 | description = "A tierkreis worker for Qulacs." 4 | version = "0.1.0" 5 | requires-python = ">=3.12" 6 | dependencies = [ 7 | "pytket-qulacs>=0.41.0", 8 | "tierkreis", 9 | ] 10 | 11 | [build-system] 12 | requires = ["hatchling"] 13 | build-backend = "hatchling.build" 14 | 15 | [tool.hatch.build.targets.sdist] 16 | include = [ 17 | "*.py", 18 | ] 19 | [tool.hatch.build.targets.wheel] 20 | include = [ 21 | "*.py", 22 | ] 23 | 24 | [tool.uv.sources] 25 | tierkreis = { workspace = true } 26 | 27 | [project.scripts] 28 | tkr_aer_worker = "main:main" -------------------------------------------------------------------------------- /examples/example_workers/hello_world_worker/main.py: -------------------------------------------------------------------------------- 1 | # /// script 2 | # requires-python = ">=3.12" 3 | # dependencies = ["pydantic", "tierkreis"] 4 | # 5 | # [tool.uv.sources] 6 | # tierkreis = { path = "../../../tierkreis", editable = true } 7 | # /// 8 | import logging 9 | from sys import argv 10 | from tierkreis import Worker 11 | 12 | logger = logging.getLogger(__name__) 13 | worker = Worker("hello_world_worker") 14 | 15 | 16 | @worker.task() 17 | def greet(greeting: str, subject: str) -> str: 18 | logger.info("%s %s", greeting, subject) 19 | return greeting + subject 20 | 21 | 22 | if __name__ == "__main__": 23 | worker.app(argv) 24 | -------------------------------------------------------------------------------- /tierkreis/tierkreis/worker/storage/protocol.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | from typing import Protocol 3 | 4 | from tierkreis.controller.data.location import WorkerCallArgs 5 | 6 | 7 | class WorkerStorage(Protocol): 8 | def resolve(self, path: Path | str) -> Path: ... 9 | def read_call_args(self, path: Path) -> WorkerCallArgs: ... 10 | def read_input(self, path: Path) -> bytes: ... 11 | def write_output(self, path: Path, value: bytes) -> None: ... 12 | def glob(self, path_string: str) -> list[str]: ... 13 | def mark_done(self, path: Path) -> None: ... 14 | def write_error(self, path: Path, error_logs: str) -> None: ... 15 | -------------------------------------------------------------------------------- /tierkreis_workers/aer_worker/pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "tkr_aer_worker" 3 | version = "0.1.4" 4 | description = "A tierkreis worker for Aer." 5 | readme = "README.md" 6 | requires-python = ">=3.12" 7 | dependencies = [ 8 | "tierkreis", 9 | "pytket-qiskit", 10 | ] 11 | 12 | [build-system] 13 | requires = ["hatchling"] 14 | build-backend = "hatchling.build" 15 | 16 | [tool.hatch.build.targets.sdist] 17 | include = [ 18 | "*.py", 19 | ] 20 | [tool.hatch.build.targets.wheel] 21 | include = [ 22 | "*.py", 23 | ] 24 | 25 | [tool.uv.sources] 26 | tierkreis = { workspace = true } 27 | 28 | [project.scripts] 29 | tkr_aer_worker = "main:main" 30 | -------------------------------------------------------------------------------- /tierkreis_visualization/README.md: -------------------------------------------------------------------------------- 1 | # Tierkreis visualizer 2 | 3 | WIP browser based visualizer. 4 | 5 | ``` 6 | uv run fastapi dev tierkreis_visualization/main.py 7 | ``` 8 | 9 | for the development server. 10 | 11 | ## bun-react-template 12 | 13 | To install dependencies: 14 | 15 | ```bash 16 | bun install 17 | ``` 18 | 19 | To start a development server: 20 | 21 | ```bash 22 | bun dev 23 | ``` 24 | 25 | To run for production: 26 | 27 | ```bash 28 | bun start 29 | ``` 30 | 31 | This project was created using `bun init` in bun v1.2.11. [Bun](https://bun.sh) is a fast all-in-one JavaScript runtime. 32 | 33 | To run with fast api: 34 | ```bash 35 | just prod serve 36 | ``` 37 | -------------------------------------------------------------------------------- /tierkreis_workers/nexus_worker/pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "tkr_nexus_worker" 3 | version = "0.1.4" 4 | description = "A tierkreis worker for interacting with Nexus." 5 | readme = "README.md" 6 | requires-python = ">=3.12" 7 | dependencies = [ 8 | "tierkreis", 9 | "qnexus==0.18.2", 10 | ] 11 | 12 | [build-system] 13 | requires = ["hatchling"] 14 | build-backend = "hatchling.build" 15 | 16 | [tool.hatch.build.targets.sdist] 17 | include = [ 18 | "*.py", 19 | ] 20 | [tool.hatch.build.targets.wheel] 21 | include = [ 22 | "*.py", 23 | ] 24 | 25 | [tool.uv.sources] 26 | tierkreis = { workspace = true } 27 | 28 | [project.scripts] 29 | tkr_nexus_worker = "main:main" 30 | -------------------------------------------------------------------------------- /tierkreis_visualization/frontend/src/hooks/use-mobile.ts: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | 3 | const MOBILE_BREAKPOINT = 768 4 | 5 | export function useIsMobile() { 6 | const [isMobile, setIsMobile] = React.useState(undefined) 7 | 8 | React.useEffect(() => { 9 | const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`) 10 | const onChange = () => { 11 | setIsMobile(window.innerWidth < MOBILE_BREAKPOINT) 12 | } 13 | mql.addEventListener("change", onChange) 14 | setIsMobile(window.innerWidth < MOBILE_BREAKPOINT) 15 | return () => mql.removeEventListener("change", onChange) 16 | }, []) 17 | 18 | return !!isMobile 19 | } 20 | -------------------------------------------------------------------------------- /tierkreis_visualization/tierkreis_visualization/cli.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | import argparse 3 | 4 | 5 | from tierkreis_visualization.main import start 6 | 7 | 8 | class TierkreisVizCli: 9 | @staticmethod 10 | def add_subcommand( 11 | main_parser: argparse._SubParsersAction[argparse.ArgumentParser], 12 | ) -> None: 13 | parser: argparse.ArgumentParser = main_parser.add_parser( 14 | "viz", 15 | description="Runs the tierkreis visualization.", 16 | ) 17 | parser.set_defaults(func=TierkreisVizCli.execute) 18 | 19 | @staticmethod 20 | def execute(args: argparse.Namespace) -> None: 21 | start() 22 | -------------------------------------------------------------------------------- /tierkreis_visualization/tierkreis_visualization/visualize_graph.py: -------------------------------------------------------------------------------- 1 | import uvicorn 2 | 3 | from tierkreis.builder import GraphBuilder 4 | from tierkreis.controller.data.graph import GraphData 5 | 6 | from tierkreis_visualization.app import app_from_graph_data 7 | 8 | 9 | def visualize_graph(graph_data: GraphData | GraphBuilder) -> None: 10 | """Visualize a computation graph in a web browser. 11 | 12 | :param graph_data: The computation graph to visualize. 13 | :type graph_data: GraphData | GraphBuilder 14 | """ 15 | if isinstance(graph_data, GraphBuilder): 16 | graph_data = graph_data.get_data() 17 | app = app_from_graph_data(graph_data) 18 | uvicorn.run(app) 19 | -------------------------------------------------------------------------------- /.envrc: -------------------------------------------------------------------------------- 1 | layout_uv() { 2 | if [[ -d ".venv" ]]; then 3 | VIRTUAL_ENV="$(pwd)/.devenv/state/venv" 4 | fi 5 | 6 | if [[ -z $VIRTUAL_ENV || ! -d $VIRTUAL_ENV ]]; then 7 | log_status "No virtual environment exists. Executing \`uv venv\` to create one." 8 | uv venv 9 | VIRTUAL_ENV="$(pwd)/.devenv/state/venv" 10 | fi 11 | 12 | PATH_add "$VIRTUAL_ENV/bin" 13 | export UV_ACTIVE=1 # or VENV_ACTIVE=1 14 | export VIRTUAL_ENV 15 | } 16 | 17 | source_url "https://raw.githubusercontent.com/cachix/devenv/d1f7b48e35e6dee421cfd0f51481d17f77586997/direnvrc" "sha256-YBzqskFZxmNb3kYVoKD9ZixoPXJh1C9ZvTLGFRkauZ0=" 18 | 19 | use devenv 20 | layout_uv 21 | 22 | unset PS1 23 | -------------------------------------------------------------------------------- /tierkreis_visualization/frontend/src/components/info.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | DialogContent, 3 | DialogDescription, 4 | DialogHeader, 5 | DialogTitle, 6 | } from "@/components/ui/dialog"; 7 | import { InfoProps } from "./types"; 8 | 9 | export function NodeInfo(props: { info: InfoProps }) { 10 | return ( 11 | 12 | 13 | {props.info.type} 14 | 15 | 16 |
17 |
{props.info.content}
18 |
19 |
20 | ); 21 | } 22 | -------------------------------------------------------------------------------- /tierkreis/tests/controller/main.py: -------------------------------------------------------------------------------- 1 | # /// script 2 | # requires-python = ">=3.12" 3 | # dependencies = ["pydantic", "tierkreis"] 4 | # 5 | # [tool.uv.sources] 6 | # tierkreis = { path = "../../../tierkreis", editable = true } 7 | # /// 8 | from pathlib import Path 9 | from time import sleep 10 | from sys import argv 11 | 12 | from tierkreis import Worker, Value 13 | 14 | worker = Worker("tests_worker") 15 | 16 | 17 | @worker.task() 18 | def sleep_and_return[T](*, output: T) -> Value[T]: 19 | sleep(10) 20 | return Value(value=output) 21 | 22 | 23 | def main() -> None: 24 | node_definition_path = argv[1] 25 | worker.run(Path(node_definition_path)) 26 | 27 | 28 | if __name__ == "__main__": 29 | main() 30 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for Tierkreis 4 | title: "[Feat]" 5 | labels: enhancement 6 | assignees: '' 7 | 8 | --- 9 | 10 | ## Is your feature request related to a problem? Please describe. 11 | A clear and concise description of what the problem is. 12 | *Example I'm always frustrated when [...]* 13 | 14 | ## Describe the solution you'd like 15 | A clear and concise description of what you want to happen. 16 | 17 | ## Describe alternatives you've considered 18 | A clear and concise description of any alternative solutions or features you've considered. 19 | 20 | ## Additional context 21 | Add any other context or screenshots about the feature request here. 22 | -------------------------------------------------------------------------------- /tierkreis_visualization/tierkreis_visualization/routers/navigation.py: -------------------------------------------------------------------------------- 1 | from uuid import UUID 2 | 3 | from tierkreis.controller.data.location import Loc 4 | 5 | 6 | def breadcrumb_links(crumbs: list[str]) -> list[tuple[str, str]]: 7 | url_path = "" 8 | links: list[tuple[str, str]] = [] 9 | for crumb in crumbs: 10 | url_path = url_path + crumb 11 | links.append((crumb, url_path)) 12 | 13 | return links 14 | 15 | 16 | def breadcrumbs(workflow_id: UUID, node_location: Loc) -> list[tuple[str, str]]: 17 | node_location_strs: list[str] = [f".{x}" for x in node_location.split(".")] 18 | static_links = ["/workflows", f"/{workflow_id}/nodes/-"] 19 | return breadcrumb_links(static_links + node_location_strs[1:]) 20 | -------------------------------------------------------------------------------- /.github/workflows/push_main.yml: -------------------------------------------------------------------------------- 1 | name: Merge to main 2 | 3 | on: 4 | workflow_dispatch: 5 | push: 6 | branches: 7 | - main 8 | 9 | jobs: 10 | docs: 11 | runs-on: ubuntu-24.04 12 | permissions: 13 | pages: write 14 | id-token: write 15 | 16 | steps: 17 | - name: Checkout code 18 | uses: actions/checkout@v4 19 | 20 | - name: Set up deps 21 | uses: ./.github/actions/setup_deps 22 | 23 | - name: Build docs 24 | run: just docs 25 | 26 | - name: Upload static files as artifact 27 | uses: actions/upload-pages-artifact@v3 28 | with: 29 | path: docs/build/html 30 | 31 | - name: Deploy to GitHub Pages 32 | uses: actions/deploy-pages@v4 33 | -------------------------------------------------------------------------------- /tierkreis_visualization/pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "tierkreis_visualization" 3 | version = "0.1.6" 4 | description = "Browser based visualization tool for tierkreis" 5 | readme = "README.md" 6 | requires-python = ">=3.12" 7 | dependencies = [ 8 | "fastapi[standard]>=0.115.11", 9 | "jinja2>=3.1.6", 10 | "pydantic-settings>=2.8.1", 11 | "tierkreis", 12 | "watchfiles>=1.0.4", 13 | ] 14 | 15 | [build-system] 16 | requires = ["hatchling"] 17 | build-backend = "hatchling.build" 18 | 19 | [tool.uv.sources] 20 | tierkreis = { workspace = true } 21 | 22 | [project.scripts] 23 | tkr-vis = "tierkreis_visualization.main:start" 24 | tkr-vis-dev = "tierkreis_visualization.main:dev" 25 | tkr-vis-graph = "tierkreis_visualization.main:graph" 26 | -------------------------------------------------------------------------------- /infra/slurm_local/sbatch: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | if [ -z "$5" ]; then 3 | echo "Usage: sbatch " -o output.log -e error.log script_file 4 | echo "Any further arguments will be discarded" 5 | exit 1 6 | fi 7 | SCRIPT_FILE=$(basename "$5") 8 | 9 | NODE_PATH=$(sed -n -E '$ s#.*([0-9a-fA-F-]{36}/[^/]+)/definition$#\1#p' "$5") 10 | WORKFLOW_ID=$(echo "$NODE_PATH" | cut -d'/' -f1) 11 | docker cp "$5" slurmctld:/data/"$SCRIPT_FILE" 12 | docker exec slurmctld sh -c 'rm -rf /data/*.log && \ 13 | ln -s $HOME/.tierkreis/checkpoints/$2/logs /data/logs.log && \ 14 | ln -s $HOME/.tierkreis/checkpoints/$3/errors /data/errors.log && \ 15 | sbatch --output=logs.log --error=errors.log --chdir=/data /data/$1' sh "$SCRIPT_FILE" "$WORKFLOW_ID" "$NODE_PATH" 16 | -------------------------------------------------------------------------------- /tierkreis_visualization/tierkreis_visualization/main.py: -------------------------------------------------------------------------------- 1 | from sys import argv 2 | 3 | import uvicorn 4 | 5 | 6 | def start() -> None: 7 | uvicorn.run("tierkreis_visualization.app:get_filestorage_app") 8 | 9 | 10 | def dev() -> None: 11 | uvicorn.run("tierkreis_visualization.app:get_filestorage_app", reload=True) 12 | 13 | 14 | def graph() -> None: 15 | """Visualize a computation graph in a web browser. 16 | 17 | Entrypoint for the project script tkr-vis-graph. 18 | """ 19 | reload_path = argv[1].split(":", 1)[0] 20 | uvicorn.run( 21 | "tierkreis_visualization.app:get_graph_data_app", 22 | reload=True, 23 | reload_includes=reload_path, 24 | ) 25 | 26 | 27 | if __name__ == "__main__": 28 | start() 29 | -------------------------------------------------------------------------------- /tierkreis/docs/_static/custom.css: -------------------------------------------------------------------------------- 1 | .wy-side-nav-search, 2 | .wy-nav-top { 3 | background: #5A46BE; 4 | } 5 | 6 | .wy-grid-for-nav, 7 | .wy-body-for-nav, 8 | .wy-nav-side, 9 | .wy-side-scroll, 10 | .wy-menu, 11 | .wy-menu-vertical { 12 | background-color: #FFFFFF; 13 | } 14 | 15 | .wy-menu-vertical a:hover { 16 | background-color: #d9d9d9; 17 | } 18 | 19 | .btn-link:visited, 20 | .btn-link, 21 | a:visited, 22 | .a.reference.external, 23 | .a.reference.internal, 24 | .wy-menu-vertical a, 25 | .wy-menu-vertical li, 26 | .wy-menu-vertical ul, 27 | .span.pre, 28 | .sig-param, 29 | .std.std-ref, 30 | 31 | 32 | html[data-theme=light] { 33 | --pst-color-inline-code: rgb(199, 37, 78) !important; 34 | } 35 | 36 | .sig-name { 37 | font-size: 1.25rem; 38 | } 39 | -------------------------------------------------------------------------------- /tierkreis_workers/ibmq_worker/pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "tkr_ibmq_worker" 3 | version = "0.1.0" 4 | description = "A tierkreis worker for compilation of ibmq backends." 5 | readme = "README.md" 6 | requires-python = ">=3.12" 7 | dependencies = [ 8 | "tierkreis", 9 | "pytket", 10 | "pytket-qiskit", 11 | ] 12 | 13 | [build-system] 14 | requires = ["hatchling"] 15 | build-backend = "hatchling.build" 16 | 17 | [tool.hatch.build.targets.sdist] 18 | include = [ 19 | "*.py", 20 | ] 21 | [tool.hatch.build.targets.wheel] 22 | include = [ 23 | "*.py", 24 | ] 25 | 26 | [tool.uv.sources] 27 | tierkreis = { workspace = true } 28 | pytket-qiskit = { git = "https://github.com/CQCL/pytket-qiskit" } 29 | 30 | [project.scripts] 31 | tkr_ibmq_worker = "main:main" 32 | -------------------------------------------------------------------------------- /tierkreis_visualization/tierkreis_visualization/data/function.py: -------------------------------------------------------------------------------- 1 | from pydantic import BaseModel 2 | from tierkreis.controller.data.location import Loc 3 | from tierkreis.controller.storage.protocol import ControllerStorage 4 | from tierkreis.exceptions import TierkreisError 5 | 6 | 7 | class FunctionDefinition(BaseModel): 8 | has_error: bool = False 9 | error_message: str | None = None 10 | 11 | 12 | def get_function_node(storage: ControllerStorage, loc: Loc) -> FunctionDefinition: 13 | parent = loc.parent() 14 | if parent is None: 15 | raise TierkreisError("Func node must have parent.") 16 | if not storage.node_has_error(loc): 17 | return FunctionDefinition() 18 | return FunctionDefinition(has_error=True, error_message=storage.read_errors(loc)) 19 | -------------------------------------------------------------------------------- /tierkreis_visualization/tierkreis_visualization/templates/unavailable.jinja: -------------------------------------------------------------------------------- 1 | {% from "macros/breadcrumbs.html" import breadcrumbs_macro with context %} 2 | {% from "macros/common_head.html" import common_head_macro with context %} 3 | {% from "macros/header.html" import header_macro with context %} 4 | 5 | 6 | Tierkreis visualization: node unavailable 7 | {{ common_head_macro() }} 8 | 9 | 10 | {{ header_macro() }} 11 |
12 | {{ breadcrumbs_macro(breadcrumbs) }} 13 |

Node unavailable

15 | 16 |

The node at the path {{ node_location }} is not started and has no data available.

17 |
18 | 19 | 20 | -------------------------------------------------------------------------------- /.github/workflows/run_check_matrix.yml: -------------------------------------------------------------------------------- 1 | name: Reusable setup for command 2 | 3 | on: 4 | workflow_call: 5 | inputs: 6 | command_name: 7 | required: true 8 | type: string 9 | command: 10 | required: true 11 | type: string 12 | 13 | jobs: 14 | run: 15 | name: ${{ inputs.command_name }} 16 | runs-on: ubuntu-24.04 17 | strategy: 18 | matrix: 19 | python-version: ["3.12", "3.13"] 20 | 21 | steps: 22 | - name: Checkout code 23 | uses: actions/checkout@v4 24 | 25 | - name: Setup deps 26 | uses: ./.github/actions/setup_deps 27 | with: 28 | python_version: ${{ matrix.python-version }} 29 | 30 | - name: ${{ inputs.command_name }} 31 | run: ${{ inputs.command }} 32 | -------------------------------------------------------------------------------- /tierkreis_workers/quantinuum_worker/pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "tkr_quantinuum_worker" 3 | version = "0.1.0" 4 | description = "A tierkreis worker for compilation of quanitnuum backends." 5 | readme = "README.md" 6 | requires-python = ">=3.12" 7 | dependencies = [ 8 | "tierkreis", 9 | "pytket", 10 | "pytket-quantinuum", 11 | ] 12 | 13 | [build-system] 14 | requires = ["hatchling"] 15 | build-backend = "hatchling.build" 16 | 17 | [tool.hatch.build.targets.sdist] 18 | include = [ 19 | "*.py", 20 | ] 21 | [tool.hatch.build.targets.wheel] 22 | include = [ 23 | "*.py", 24 | ] 25 | 26 | [tool.uv.sources] 27 | tierkreis = { workspace = true } 28 | pytket-quantinuum = { git = "https://github.com/CQCL/pytket-quantinuum" } 29 | 30 | [project.scripts] 31 | tkr_quantinuum_worker = "main:main" 32 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.uv.workspace] 2 | members = [ 3 | "tierkreis", 4 | "tierkreis_visualization", 5 | "tierkreis_workers/*worker", 6 | ] 7 | 8 | 9 | [dependency-groups] 10 | # Default dev dependency group 11 | dev = [ 12 | { include-group = "lint" }, 13 | { include-group = "test" }, 14 | { include-group = "docs" }, 15 | { include-group = "examples"} 16 | ] 17 | lint = [ 18 | "isort>=6.0.1", 19 | "ruff>=0.12.5", 20 | "pyright>=1.1.400", 21 | ] 22 | test = [ 23 | "pytest>=6.2,<9", 24 | "pytest-asyncio>=0.16,<0.17", 25 | "pytest-cov>=5.0,<6", 26 | "pydantic>=2.9.2", 27 | ] 28 | docs = [ 29 | "sphinx>=8.2", 30 | "myst-parser>=4.0.1", 31 | "sphinx-autodoc2>=0.5.0", 32 | "myst-nb>=1.3.0", 33 | "furo>=2025.7.19", 34 | ] 35 | examples = ["pyscf>=2.9.0"] 36 | -------------------------------------------------------------------------------- /examples/example_workers/substitution_worker/stubs.py: -------------------------------------------------------------------------------- 1 | """Code generated from substitution_worker namespace. Please do not edit.""" 2 | 3 | from typing import NamedTuple 4 | from tierkreis.controller.data.models import TKR, OpaqueType 5 | 6 | 7 | class substitute(NamedTuple): 8 | circuit: TKR[OpaqueType["pytket._tket.circuit.Circuit"]] # noqa: F821 # fmt: skip 9 | a: TKR[float] # noqa: F821 # fmt: skip 10 | b: TKR[float] # noqa: F821 # fmt: skip 11 | c: TKR[float] # noqa: F821 # fmt: skip 12 | 13 | @staticmethod 14 | def out() -> type[TKR[OpaqueType["pytket._tket.circuit.Circuit"]]]: # noqa: F821 # fmt: skip 15 | return TKR[OpaqueType["pytket._tket.circuit.Circuit"]] # noqa: F821 # fmt: skip 16 | 17 | @property 18 | def namespace(self) -> str: 19 | return "substitution_worker" 20 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | workflow_dispatch: 5 | release: 6 | types: [created] 7 | 8 | jobs: 9 | release: 10 | runs-on: ubuntu-24.04 11 | environment: pypi 12 | permissions: 13 | id-token: write 14 | 15 | steps: 16 | - name: Checkout code 17 | uses: actions/checkout@v4 18 | 19 | - name: Set up deps 20 | uses: ./.github/actions/setup_deps 21 | with: 22 | include_node: "yes" 23 | 24 | - name: Build frontend 25 | run: just prod 26 | 27 | - name: Build Python package 28 | run: uv build --all-packages 29 | 30 | - name: Publish package distributions to PyPI 31 | uses: pypa/gh-action-pypi-publish@release/v1 32 | with: 33 | verbose: true 34 | skip-existing: true 35 | -------------------------------------------------------------------------------- /tierkreis_visualization/tierkreis_visualization/templates/loop.jinja: -------------------------------------------------------------------------------- 1 | {% from "macros/breadcrumbs.html" import breadcrumbs_macro with context %} 2 | {% from "macros/common_head.html" import common_head_macro with context %} 3 | {% from "macros/header.html" import header_macro with context %} 4 | 5 | 6 | 7 | Tierkreis visualization: LOOP node 8 | {{ common_head_macro() }} 9 | 10 | 11 | 12 | 13 | {{ header_macro() }} 14 |
15 | {{ breadcrumbs_macro(breadcrumbs, true) }} 16 |
17 | 18 | 22 |
23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /tierkreis_visualization/frontend/src/components/layout.tsx: -------------------------------------------------------------------------------- 1 | import { NodeInfo } from "@/components/info"; 2 | import { Dialog } from "@/components/ui/dialog"; 3 | import { SidebarProvider } from "@/components/ui/sidebar"; 4 | import { WorkflowSidebar } from "@/components/workflow-sidebar"; 5 | import { InfoProps, Workflow } from "@/components/types"; 6 | 7 | export default function Layout({ 8 | children, 9 | workflows, 10 | workflowId, 11 | info, 12 | }: { 13 | children: React.ReactNode; 14 | workflows: Workflow[]; 15 | workflowId: string; 16 | info: InfoProps; 17 | }) { 18 | return ( 19 | 20 | 21 | 22 | 23 |
{children}
24 |
25 |
26 | ); 27 | } 28 | -------------------------------------------------------------------------------- /tierkreis_visualization/frontend/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | 4 | /* Tailwind */ 5 | "baseUrl": ".", 6 | "paths": { 7 | "@/*": ["./src/*"] 8 | }, 9 | 10 | "target": "ES2020", 11 | "useDefineForClassFields": true, 12 | "lib": ["ES2020", "DOM", "DOM.Iterable"], 13 | "module": "ESNext", 14 | "skipLibCheck": true, 15 | 16 | /* Bundler mode */ 17 | "moduleResolution": "bundler", 18 | "allowImportingTsExtensions": true, 19 | "resolveJsonModule": true, 20 | "isolatedModules": true, 21 | "noEmit": true, 22 | "jsx": "react-jsx", 23 | 24 | /* Linting */ 25 | "strict": true, 26 | "noUnusedLocals": true, 27 | "noUnusedParameters": true, 28 | "noFallthroughCasesInSwitch": true 29 | }, 30 | "include": ["src"], 31 | "references": [{ "path": "./tsconfig.node.json" }] 32 | } 33 | -------------------------------------------------------------------------------- /docs/source/index.md: -------------------------------------------------------------------------------- 1 | # tierkreis documentation 2 | 3 | tierkreis `[ˈtiːɐ̯ˌkʁaɪ̯s]` is an HPC native workflow management system (WMS) for asynchronous hybrid jobs. 4 | tierkreis combines task-based workers with context dependent execution on various storage implementations. 5 | 6 | | WMS | Multi-Language | Multi-Executor | HPC-Support | Visualization | Repeat until convergence | Proprietary Workers | 7 | | --------- | :------------: | :------------: | :---------: | :-----------: | :----------------------: | :-----------------: | 8 | | tierkreis | ✔ | ✔ | ✔ | ✔ | ✔ | ✔ | 9 | 10 | ## Quick Start 11 | 12 | ## Contents 13 | 14 | ```{toctree} 15 | :maxdepth: 3 16 | installation.md 17 | core_concepts.md 18 | getting_started.md 19 | visualization.md 20 | worker/index 21 | apidocs/index 22 | ``` 23 | -------------------------------------------------------------------------------- /infra/slurm_local/USAGE.md: -------------------------------------------------------------------------------- 1 | # How to run the slurm test 2 | 3 | For setup see the [README](README.md). 4 | In short: 5 | 6 | - install docker 7 | - From `infra/slurm_local` 8 | - make the local sbatch script executable `chmod +x sbatch` 9 | - build the containers with `docker compose build` 10 | - ensure the containers are running `docker compose up -d` 11 | - This will mount `~/.tierkreis` inside the containers on `/root/tierkreis/` and the tierkreis directory to `/tierkreis` 12 | 13 | To run the test we emulate `sbatch`. 14 | The devenv contains a script for this (same as the sbatch script), if you want, link it to a location in your path to make it available elsewhere. 15 | 16 | Run the test: 17 | 18 | ``` 19 | pytest tierkreis/tests/executor/test_hpc_executor.py 20 | ``` 21 | 22 | **Caveats**: 23 | 24 | - mpi workers is nondeterministic due to allocation from slurm 25 | -------------------------------------------------------------------------------- /tierkreis_visualization/frontend/src/components/ui/separator.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as SeparatorPrimitive from "@radix-ui/react-separator" 5 | 6 | import { cn } from "@/lib/utils" 7 | 8 | function Separator({ 9 | className, 10 | orientation = "horizontal", 11 | decorative = true, 12 | ...props 13 | }: React.ComponentProps) { 14 | return ( 15 | 25 | ) 26 | } 27 | 28 | export { Separator } 29 | -------------------------------------------------------------------------------- /infra/slurm_local/slurmdbd.conf: -------------------------------------------------------------------------------- 1 | # 2 | # Example slurmdbd.conf file. 3 | # 4 | # See the slurmdbd.conf man page for more information. 5 | # 6 | # Archive info 7 | #ArchiveJobs=yes 8 | #ArchiveDir="/tmp" 9 | #ArchiveSteps=yes 10 | #ArchiveScript= 11 | #JobPurge=12 12 | #StepPurge=1 13 | # 14 | # Authentication info 15 | AuthType=auth/munge 16 | #AuthInfo=/var/run/munge/munge.socket.2 17 | # 18 | # slurmDBD info 19 | DbdAddr=slurmdbd 20 | DbdHost=slurmdbd 21 | #DbdPort=6819 22 | SlurmUser=slurm 23 | #MessageTimeout=300 24 | DebugLevel=4 25 | #DefaultQOS=normal,standby 26 | LogFile=/var/log/slurm/slurmdbd.log 27 | PidFile=/var/run/slurmdbd/slurmdbd.pid 28 | #PluginDir=/usr/lib/slurm 29 | #PrivateData=accounts,users,usage,jobs 30 | #TrackWCKey=yes 31 | # 32 | # Database info 33 | StorageType=accounting_storage/mysql 34 | StorageHost=mysql 35 | StorageUser=slurm 36 | StoragePass=password 37 | #StorageLoc=slurm_acct_db 38 | -------------------------------------------------------------------------------- /tierkreis_visualization/tierkreis_visualization/templates/map.jinja: -------------------------------------------------------------------------------- 1 | {% from "macros/breadcrumbs.html" import breadcrumbs_macro with context %} 2 | {% from "macros/common_head.html" import common_head_macro with context %} 3 | {% from "macros/header.html" import header_macro with context %} 4 | 5 | 6 | 7 | Tierkreis visualization: MAP node 8 | {{ common_head_macro() }} 9 | 10 | 11 | 12 | 13 | {{ header_macro() }} 14 |
15 | {{ breadcrumbs_macro(breadcrumbs, true) }} 16 |
17 | 18 | 25 |
26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /tierkreis/tests/controller/test_codegen.py: -------------------------------------------------------------------------------- 1 | from types import NoneType 2 | import pytest 3 | from tierkreis.codegen import format_generic_type 4 | from tierkreis.controller.data.types import PType 5 | from tierkreis.idl.models import GenericType 6 | 7 | formats: list[tuple[type[PType], str]] = [ 8 | (bool, "bool"), 9 | (int, "int"), 10 | (float, "float"), 11 | (str, "str"), 12 | (bytes, "bytes"), 13 | (NoneType, "NoneType"), 14 | (list[str], "list[str]"), 15 | (list[str | list[str | int]], "list[Union[str, list[Union[str, int]]]]"), 16 | (tuple[str], "tuple[str]"), 17 | ( 18 | tuple[str | list[str | int], NoneType], 19 | "tuple[Union[str, list[Union[str, int]]], NoneType]", 20 | ), 21 | ] 22 | 23 | 24 | @pytest.mark.parametrize("ttype,expected", formats) 25 | def test_format_ttype(ttype: type[PType], expected: str): 26 | generic_type = GenericType.from_type(ttype) 27 | 28 | assert format_generic_type(generic_type, False, False) == expected 29 | -------------------------------------------------------------------------------- /tierkreis_visualization/tierkreis_visualization/templates/macros/common_head.html: -------------------------------------------------------------------------------- 1 | {% macro common_head_macro() -%} 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | {%- endmacro %} 18 | -------------------------------------------------------------------------------- /tierkreis/tierkreis/controller/executor/protocol.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | from typing import Protocol 3 | 4 | 5 | class ControllerExecutor(Protocol): 6 | """The executor protocol defines how to run workers. 7 | 8 | An executor is responsible for running a worker(binary) in a given environment. 9 | 10 | """ 11 | 12 | def run(self, launcher_name: str, worker_call_args_path: Path) -> None: 13 | """Run the node defined by the node_definition path. 14 | 15 | Specifies the worker to run by its launcher name. 16 | For example the function "builtins.iadd" will call the builtins worker's iadd function. 17 | The call arguments for the function call are retrieved retrieved from its location. 18 | 19 | :param launcher_name: module description of launcher to run. 20 | :type launcher_name: str 21 | :param worker_call_args_path: Location of the worker call args. 22 | :type worker_call_args_path: Path 23 | """ 24 | 25 | ... 26 | -------------------------------------------------------------------------------- /tierkreis/tests/idl/namespace1.py: -------------------------------------------------------------------------------- 1 | from typing import NamedTuple 2 | from tierkreis.controller.data.models import portmapping 3 | from tierkreis import Worker 4 | from tierkreis.controller.data.types import PType 5 | 6 | worker = Worker("TestNamespace") 7 | 8 | 9 | class ListItem(NamedTuple): 10 | i: int 11 | 12 | 13 | class NestedType(NamedTuple): 14 | city: str 15 | 16 | 17 | class IncludedType(NamedTuple): 18 | nested: NestedType 19 | 20 | 21 | @portmapping 22 | class A(NamedTuple): 23 | name: dict[str, str] 24 | age: int 25 | 26 | 27 | class B(NamedTuple): 28 | name: dict[str, str] 29 | age: int 30 | 31 | 32 | class C[T: PType](NamedTuple): 33 | a: list[int] 34 | b: B 35 | t: T 36 | included: IncludedType 37 | ol: list[ListItem] 38 | 39 | 40 | @worker.task() 41 | def foo(a: int, b: str) -> A: ... 42 | @worker.task() 43 | def bar() -> B: ... 44 | @worker.task() 45 | def z[T: PType](c: C[T]) -> C[T]: ... 46 | 47 | 48 | expected_namespace = worker.namespace 49 | -------------------------------------------------------------------------------- /tierkreis_workers/pytket_worker/pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "tkr_pytket_worker" 3 | version = "0.1.5" 4 | description = "A tierkreis worker for pytket." 5 | readme = "README.md" 6 | requires-python = ">=3.12" 7 | dependencies = [ 8 | "tierkreis", 9 | "pytket", 10 | "qnexus", 11 | "pytket_qirpass", 12 | ] 13 | 14 | [project.optional-dependencies] 15 | backend-qiskit = [ 16 | "pytket-qiskit", 17 | ] 18 | backend-quantinuum = [ 19 | "pytket-quantinuum", 20 | ] 21 | 22 | 23 | [build-system] 24 | requires = ["hatchling"] 25 | build-backend = "hatchling.build" 26 | 27 | [tool.hatch.build.targets.sdist] 28 | include = [ 29 | "*.py", 30 | ] 31 | [tool.hatch.build.targets.wheel] 32 | include = [ 33 | "*.py", 34 | ] 35 | 36 | [tool.uv.sources] 37 | tierkreis = { workspace = true } 38 | pytket-qiskist = { git = "https://github.com/CQCL/pytket-qiskit" } 39 | pytket-quantinuum = { git = "https://github.com/CQCL/pytket-quantinuum" } 40 | 41 | [project.scripts] 42 | tkr_pytket_worker = "main:main" 43 | -------------------------------------------------------------------------------- /tierkreis/docs/api-docs/_templates/autosummary/class.rst: -------------------------------------------------------------------------------- 1 | .. 2 | Custom class template to make sphinx-autosummary list the full API doc after 3 | the summary. See https://github.com/sphinx-doc/sphinx/issues/7912 4 | 5 | {{ name | escape | underline}} 6 | 7 | .. currentmodule:: {{ module }} 8 | 9 | .. autoclass:: {{ objname }} 10 | :members: 11 | :show-inheritance: 12 | :inherited-members: 13 | 14 | {% block methods %} 15 | {% if methods %} 16 | .. rubric:: {{ _('Methods') }} 17 | 18 | .. autosummary:: 19 | :nosignatures: 20 | {% for item in methods %} 21 | {%- if not item.startswith('_') %} 22 | ~{{ name }}.{{ item }} 23 | {%- endif -%} 24 | {%- endfor %} 25 | {% endif %} 26 | {% endblock %} 27 | 28 | {% block attributes %} 29 | {% if attributes %} 30 | .. rubric:: {{ _('Attributes') }} 31 | 32 | .. autosummary:: 33 | {% for item in attributes %} 34 | ~{{ name }}.{{ item }} 35 | {%- endfor %} 36 | {% endif %} 37 | {% endblock %} 38 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve Tierkreis. Thank you! 4 | title: "[BUG]" 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | ## Describe the bug 11 | A clear and concise description of what the bug is 12 | 13 | ## To Reproduce 14 | Steps to reproduce the behavior including a minimal reproducible example: 15 | 16 | ** Configuration 17 | 1. Workflow 18 | 2. Executor 19 | 3. Workers 20 | 21 | ## Expected behavior 22 | A clear and concise description of what you expected to happen. 23 | 24 | 25 | ## Desktop (please complete the following information): 26 | - OS: [e.g. Ubuntu 24 `lsb_release -a`] 27 | - Python Version 28 | - UV Version 29 | 30 | Run `pip freeze` and put the output in the `
` element below. 31 | 32 |
33 | 34 |
35 | 36 | ## Additional context & Logs 37 | Add any other context about the problem here. 38 | - Output of log files in `~/.tierkreis/checkpoints//logs` or `~/.tierkreis/checkpoints///errors` 39 | -------------------------------------------------------------------------------- /docs/source/getting_started.md: -------------------------------------------------------------------------------- 1 | # Getting Started 2 | 3 | ## Step by step tutorials 4 | 5 | A sequence of tutorials that cover the fundamentals of writing and operating Tierkreis workflows. 6 | 7 | [Our first graph](tutorial/builtins.md) 8 | 9 | [Graph inputs and outputs](tutorial/inputs.md) 10 | 11 | [Nested graphs using Eval](tutorial/eval.md) 12 | 13 | [Iteration using Loop](tutorial/loop.md) 14 | 15 | [Parallel computation using Map](tutorial/map.md) 16 | 17 | ## Worker examples 18 | 19 | Tutorials on writing workers that provide additional tasks. 20 | For worker libraries see [this document](workers.md) 21 | 22 | ### Tierkreis Python library 23 | 24 | [Hello world worker](worker/hello_world.md) 25 | 26 | [Complex types in Tierkreis Python workers](worker/complex_types.md) 27 | 28 | ### Other Workers 29 | 30 | [External workers with an IDL](worker/external_workers.md) 31 | 32 | ## Executors 33 | 34 | [Overview](executors/overview.md) 35 | 36 | [Shell Executors](executors/shell.md) 37 | 38 | [HPC Executors](executors/hpc.md) 39 | -------------------------------------------------------------------------------- /tierkreis/tests/errors/failing_worker/stubs.py: -------------------------------------------------------------------------------- 1 | """Code generated from failing_worker namespace. Please do not edit.""" 2 | 3 | from typing import NamedTuple 4 | from tierkreis.controller.data.models import TKR 5 | 6 | 7 | class fail(NamedTuple): 8 | @staticmethod 9 | def out() -> type[TKR[int]]: # noqa: F821 # fmt: skip 10 | return TKR[int] # noqa: F821 # fmt: skip 11 | 12 | @property 13 | def namespace(self) -> str: 14 | return "failing_worker" 15 | 16 | 17 | class wont_fail(NamedTuple): 18 | @staticmethod 19 | def out() -> type[TKR[int]]: # noqa: F821 # fmt: skip 20 | return TKR[int] # noqa: F821 # fmt: skip 21 | 22 | @property 23 | def namespace(self) -> str: 24 | return "failing_worker" 25 | 26 | 27 | class exit_code_1(NamedTuple): 28 | @staticmethod 29 | def out() -> type[TKR[int]]: # noqa: F821 # fmt: skip 30 | return TKR[int] # noqa: F821 # fmt: skip 31 | 32 | @property 33 | def namespace(self) -> str: 34 | return "failing_worker" 35 | -------------------------------------------------------------------------------- /tierkreis/tests/cli/data/factorial: -------------------------------------------------------------------------------- 1 | {"nodes":[{"value":-1,"inputs":{},"type":"const","outputs":["value"]},{"value":1,"inputs":{},"type":"const","outputs":["value"]},{"name":"n","inputs":{},"type":"input","outputs":["n"]},{"name":"factorial","inputs":{},"type":"input","outputs":["factorial"]},{"function_name":"builtins.igt","inputs":{"a":[2,"n"],"b":[1,"value"]},"type":"function","outputs":["value"]},{"function_name":"builtins.iadd","inputs":{"a":[0,"value"],"b":[2,"n"]},"type":"function","outputs":["value"]},{"graph":[3,"factorial"],"inputs":{"n":[5,"value"],"factorial":[3,"factorial"]},"type":"eval","outputs":["factorial_output"]},{"function_name":"builtins.itimes","inputs":{"a":[2,"n"],"b":[6,"factorial_output"]},"type":"function","outputs":["value"]},{"value":1,"inputs":{},"type":"const","outputs":["value"]},{"pred":[4,"value"],"if_true":[7,"value"],"if_false":[8,"value"],"type":"ifelse","inputs":{},"outputs":["value"]},{"inputs":{"factorial_output":[9,"value"]},"type":"output","outputs":[]}],"fixed_inputs":{},"graph_inputs":["n","factorial"],"graph_output_idx":10} -------------------------------------------------------------------------------- /examples/non_standard_tkr_dir.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | from uuid import UUID 3 | 4 | from tierkreis.consts import PACKAGE_PATH 5 | from tierkreis import run_graph 6 | from tierkreis.controller.executor.multiple import MultipleExecutor 7 | from tierkreis.storage import FileStorage, read_outputs 8 | from tierkreis.executor import UvExecutor, ShellExecutor 9 | 10 | from signing_graph import signing_graph 11 | 12 | 13 | storage = FileStorage( 14 | UUID(int=108), 15 | do_cleanup=True, 16 | tierkreis_directory=Path.home() / ".tierkreis" / "checkpoints2", 17 | ) 18 | uv = UvExecutor(PACKAGE_PATH / ".." / "examples" / "example_workers", storage.logs_path) 19 | shell = ShellExecutor( 20 | PACKAGE_PATH / ".." / "examples" / "example_workers", storage.workflow_dir 21 | ) 22 | executor = MultipleExecutor(uv, {"shell": shell}, {"openssl_worker": "shell"}) 23 | 24 | run_graph(storage, executor, signing_graph(), {}, polling_interval_seconds=0.1) 25 | out = read_outputs(signing_graph(), storage) 26 | print(out) 27 | assert isinstance(out, bool) 28 | -------------------------------------------------------------------------------- /tierkreis/tierkreis/idl/spec.py: -------------------------------------------------------------------------------- 1 | """Combinators for the Tierkreis worker IDL. 2 | 3 | We use 4 | https://typespec.io/docs/language-basics/models/ 5 | https://typespec.io/docs/language-basics/interfaces/ 6 | as well as an extra decorator @portmapping. 7 | """ 8 | 9 | from tierkreis.idl.models import Interface, Method, Model, TypedArg 10 | 11 | from tierkreis.idl.parser import lit, seq 12 | from tierkreis.idl.type_symbols import generic_t, ident, type_symbol 13 | 14 | 15 | type_decl = ((ident << lit(":")) & type_symbol).map(lambda x: TypedArg(*x)) 16 | model = seq( 17 | lit("@portmapping").opt().map(lambda x: x is not None) << lit("model"), 18 | generic_t, 19 | lit("{") >> type_decl.rep(lit(";")) << lit("}"), 20 | ).map(lambda x: Model(*x)) 21 | method = seq( 22 | generic_t, 23 | lit("(") >> type_decl.rep(lit(",")) << lit(")") << lit(":"), 24 | type_symbol, 25 | ).map(lambda x: Method(*x)) 26 | interface = ( 27 | (lit("interface") >> ident << lit("{")) & method.rep(lit(";")) << lit("}") 28 | ).map(lambda x: Interface(*x)) 29 | spec = model.rep() & interface 30 | -------------------------------------------------------------------------------- /.github/workflows/pr_main.yml: -------------------------------------------------------------------------------- 1 | name: PR against main 2 | 3 | on: 4 | workflow_dispatch: 5 | pull_request: 6 | branches: 7 | - main 8 | 9 | jobs: 10 | lint: 11 | uses: ./.github/workflows/run_check_matrix.yml 12 | with: 13 | command_name: lint 14 | command: just lint 15 | 16 | test: 17 | uses: ./.github/workflows/run_check_matrix.yml 18 | with: 19 | command_name: test 20 | command: just test 21 | 22 | test-workers: 23 | uses: ./.github/workflows/run_check_matrix.yml 24 | with: 25 | command_name: test-workers 26 | command: just test-workers 27 | 28 | examples: 29 | uses: ./.github/workflows/run_check_matrix.yml 30 | with: 31 | command_name: examples 32 | command: just examples 33 | 34 | docs: 35 | uses: ./.github/workflows/run_check_matrix.yml 36 | with: 37 | command_name: docs 38 | command: just docs 39 | 40 | check-stubs: 41 | uses: ./.github/workflows/run_check_matrix.yml 42 | with: 43 | command_name: check-stubs 44 | command: just check-stubs 45 | -------------------------------------------------------------------------------- /tierkreis_visualization/frontend/src/components/ui/input.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | 3 | import { cn } from "@/lib/utils" 4 | 5 | function Input({ className, type, ...props }: React.ComponentProps<"input">) { 6 | return ( 7 | 18 | ) 19 | } 20 | 21 | export { Input } 22 | -------------------------------------------------------------------------------- /devenv.nix: -------------------------------------------------------------------------------- 1 | { pkgs, lib, ... }: 2 | 3 | { 4 | 5 | packages = [ 6 | pkgs.just 7 | pkgs.graphviz 8 | ] ++ lib.optionals pkgs.stdenv.isDarwin (with pkgs.darwin.apple_sdk; [ 9 | frameworks.CoreServices 10 | frameworks.CoreFoundation 11 | frameworks.Security 12 | frameworks.SystemConfiguration 13 | ]); 14 | 15 | git-hooks.hooks = { 16 | pyright.enable = true; 17 | ruff.enable = true; 18 | ruff-format.enable = true; 19 | }; 20 | 21 | # https://devenv.sh/languages/ 22 | languages.python = { 23 | enable = true; 24 | uv.enable = true; 25 | }; 26 | 27 | languages.rust.enable = true; 28 | 29 | languages.javascript = { 30 | enable = true; 31 | bun.enable = true; 32 | bun.install.enable = true; 33 | directory = "./tierkreis_visualization"; 34 | }; 35 | 36 | # This allows building the type-check (pyo3) module on MacOSX "Apple Silicon" 37 | enterShell = 38 | if pkgs.stdenv.isDarwin && pkgs.stdenv.isAarch64 then '' 39 | export RUSTFLAGS="$RUSTFLAGS -C link-arg=-undefined -C link-arg=dynamic_lookup" 40 | '' else ''''; 41 | 42 | } 43 | -------------------------------------------------------------------------------- /tierkreis_visualization/tierkreis_visualization/templates/workflows.html: -------------------------------------------------------------------------------- 1 | {% from "macros/common_head.html" import common_head_macro with context %} 2 | {% from "macros/header.html" import header_macro with context %} 3 | {% from "macros/breadcrumbs.html" import breadcrumbs_macro with context %} 4 | 5 | 6 | Tierkreis visualization: workflows 7 | {{ common_head_macro() }} 8 | 9 | 10 | {{ header_macro() }} 11 |
12 | {{ breadcrumbs_macro([["/workflows"]]) }} 13 | 29 |
30 | 31 | 32 | -------------------------------------------------------------------------------- /infra/slurm_local/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Giovanni Torres 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /tierkreis_visualization/tierkreis_visualization/static/network.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | /** 4 | * Connect to an event stream. Pass in a nodes and edges list to mutate. 5 | * @param {PyNode[]} nodes - Event stream url to subscribe to. 6 | * @param {PyEdge[]} edges - Network vis library. 7 | * @param {string} name - List of graph edges. 8 | */ 9 | 10 | 11 | 12 | function createNetwork(nodes, edges, name) { 13 | const visnodes = new vis.DataSet(nodes.map(createJSNode)); 14 | const visedges = new vis.DataSet(edges.map(createJSEdge)); 15 | 16 | const container = document.getElementById("mynetwork"); 17 | const data = { nodes: visnodes, edges: visedges }; 18 | const options = { 19 | layout: { hierarchical: { direction: "LR", sortMethod: "directed" } }, 20 | nodes: { 21 | margin: 12, 22 | } 23 | }; 24 | 25 | const network = new vis.Network(container, data, options); 26 | network.on("doubleClick", function (params) { 27 | let nodes = params.nodes; 28 | if (nodes.length === 1) { 29 | window.location.href = `${window.location.href}.${name}${nodes[0]}`; 30 | } 31 | }); 32 | return [network, visnodes, visedges]; 33 | } 34 | -------------------------------------------------------------------------------- /tierkreis/pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "tierkreis" 3 | description = "Python client and utilities for tierkreis." 4 | readme = "README.md" 5 | version = "2.0.9" 6 | authors = [ 7 | { name = "Seyon Sivarajah", email = "seyon.sivarajah@quantinuum.com" }, 8 | { name = "Lukas Heidemann", email = "lukas.heidemann@quantinuum.com" }, 9 | { name = "John Children", email = "john.children@quantinuum.com" }, 10 | { name = "Alan Lawrence", email = "alan.lawrence@quantinuum.com" }, 11 | { name = "Matthew Burke", email = "matthew.burke@quantinuum.com" }, 12 | { name = "Philipp Seitz", email = "philipp.seitz@quantinuum.com" }, 13 | ] 14 | 15 | requires-python = ">=3.12" 16 | dependencies = [ 17 | "pydantic~=2.5", 18 | ] 19 | 20 | [dependency-groups] 21 | dev = [ 22 | "ipykernel>=6.29.5", 23 | "pytest", 24 | ] 25 | build = ["build[uv]"] 26 | 27 | [build-system] 28 | requires = ["hatchling"] 29 | build-backend = "hatchling.build" 30 | 31 | [tool.ruff] 32 | target-version = "py312" 33 | extend-exclude = [] 34 | 35 | [tool.pyright] 36 | include = ["."] 37 | exclude = [] 38 | ignore = [] 39 | 40 | [project.scripts] 41 | tkr = "tierkreis.cli.tkr:main" 42 | -------------------------------------------------------------------------------- /tierkreis_visualization/frontend/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 webkid GmbH 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /tierkreis_visualization/frontend/src/nodes/types.ts: -------------------------------------------------------------------------------- 1 | import { InfoProps } from "@/components/types"; 2 | import { Edge, type Node } from "@xyflow/react"; 3 | 4 | export type PyNode = { 5 | id: string | number; 6 | status: "Not started" | "Started" | "Error" | "Finished"; 7 | function_name: string; 8 | node_location: string; 9 | value?: unknown; 10 | started_time: string; 11 | finished_time: string; 12 | }; 13 | export type BackendNode = Node<{ 14 | name: string; 15 | status: "Not started" | "Started" | "Error" | "Finished"; 16 | handles: { 17 | inputs: string[]; 18 | outputs: string[]; 19 | }; 20 | hidden_handles?: { 21 | inputs: string[]; 22 | outputs: string[]; 23 | }; 24 | hidden_edges?: Edge[]; 25 | workflowId: string; 26 | node_location: string; 27 | id: string; 28 | title: string; 29 | label?: string; 30 | pinned: boolean; 31 | value: string | null; 32 | setInfo?: (info: InfoProps) => void; 33 | is_expanded: boolean; 34 | isTooltipOpen: boolean; 35 | hoveredId: string; 36 | setHoveredId: (id: string) => void; 37 | started_time: string; 38 | finished_time: string; 39 | }>; 40 | export type AppNode = BackendNode; 41 | -------------------------------------------------------------------------------- /tierkreis/tests/cli/data/sample_graph: -------------------------------------------------------------------------------- 1 | {"nodes": [{"value": 0, "inputs": {}, "type": "const", "outputs": ["value"]}, {"value": 6, "inputs": {}, "type": "const", "outputs": ["value"]}, {"value": {"nodes": [{"name": "doubler_input", "inputs": {}, "type": "input", "outputs": ["doubler_input"]}, {"name": "intercept", "inputs": {}, "type": "input", "outputs": ["intercept"]}, {"value": 2, "inputs": {}, "type": "const", "outputs": ["value"]}, {"function_name": "builtins.itimes", "inputs": {"a": [0, "doubler_input"], "b": [2, "value"]}, "type": "function", "outputs": ["value"]}, {"function_name": "builtins.iadd", "inputs": {"a": [3, "value"], "b": [1, "intercept"]}, "type": "function", "outputs": ["value"]}, {"inputs": {"doubler_output": [4, "value"]}, "type": "output", "outputs": []}], "fixed_inputs": {}, "graph_inputs": ["intercept", "doubler_input"], "graph_output_idx": 5}, "inputs": {}, "type": "const", "outputs": []}, {"graph": [2, "value"], "inputs": {"doubler_input": [1, "value"], "intercept": [0, "value"]}, "type": "eval", "outputs": ["doubler_output"]}, {"inputs": {"simple_eval_output": [3, "doubler_output"]}, "type": "output", "outputs": []}], "fixed_inputs": {}, "graph_inputs": [], "graph_output_idx": 4} -------------------------------------------------------------------------------- /tierkreis_visualization/tierkreis_visualization/app_config.py: -------------------------------------------------------------------------------- 1 | from contextlib import asynccontextmanager 2 | from enum import Enum 3 | from typing import Callable 4 | from uuid import UUID 5 | import webbrowser 6 | from fastapi import FastAPI 7 | import starlette.datastructures 8 | from tierkreis.controller.storage.filestorage import ControllerFileStorage 9 | from tierkreis.controller.storage.graphdata import GraphDataStorage 10 | from tierkreis.controller.storage.protocol import ControllerStorage 11 | 12 | 13 | @asynccontextmanager 14 | async def lifespan(app: FastAPI): 15 | if app.state.storage_type == StorageType.GRAPHDATA: 16 | webbrowser.open("http://localhost:8000/", new=0, autoraise=False) 17 | yield 18 | 19 | 20 | class StorageType(Enum): 21 | FILESTORAGE = ControllerFileStorage 22 | GRAPHDATA = GraphDataStorage 23 | 24 | 25 | class State(starlette.datastructures.State): 26 | """Typed App state information.""" 27 | 28 | get_storage_fn: Callable[[UUID], ControllerStorage] 29 | storage_type: StorageType 30 | 31 | 32 | class App(FastAPI): 33 | """FastAPI App with custom state.""" 34 | 35 | state: State # pyright: ignore[reportIncompatibleVariableOverride] 36 | -------------------------------------------------------------------------------- /tierkreis_visualization/frontend/src/nodes/hide_children.ts: -------------------------------------------------------------------------------- 1 | import { BackendNode } from "./types"; 2 | import { Edge } from "@xyflow/react"; 3 | 4 | export function hideChildren( 5 | nodeId: string, 6 | oldNodes: BackendNode[], 7 | oldEdges: Edge[] 8 | ) { 9 | let hidden_edges: Edge[] = []; 10 | oldNodes = oldNodes 11 | .map((node) => { 12 | if (node.parentId?.startsWith(nodeId)) { 13 | return undefined; 14 | } 15 | if (node.id === nodeId) { 16 | hidden_edges = node.data.hidden_edges ?? []; 17 | node.position = { x: 0, y: 0 }; 18 | node.data.handles = node.data.hidden_handles ?? { 19 | inputs: [], 20 | outputs: [], 21 | }; 22 | node.data.is_expanded = false; 23 | node.style = { 24 | width: 180, 25 | height: 124, 26 | }; 27 | } 28 | return node; 29 | }) 30 | .filter((node): node is BackendNode => node !== undefined); 31 | 32 | oldEdges.filter( 33 | (edge) => 34 | edge.target.startsWith(nodeId + ":") && 35 | edge.source.startsWith(nodeId + ":") 36 | ); 37 | return { 38 | nodes: [...oldNodes], 39 | edges: [...oldEdges, ...hidden_edges], 40 | }; 41 | } 42 | -------------------------------------------------------------------------------- /tierkreis/docs/api-docs/conf.py: -------------------------------------------------------------------------------- 1 | # Configuration file for the Sphinx documentation builder. 2 | # See https://www.sphinx-doc.org/en/master/usage/configuration.html 3 | 4 | 5 | project = "Tierkreis Python" 6 | copyright = "2024, Quantinuum" 7 | author = "Quantinuum" 8 | 9 | extensions = [ 10 | "sphinx.ext.napoleon", 11 | "sphinx.ext.autodoc", 12 | "sphinx.ext.coverage", 13 | "sphinx.ext.autosummary", 14 | "sphinx.ext.viewcode", 15 | "sphinx.ext.intersphinx", 16 | ] 17 | 18 | html_theme = "sphinx_book_theme" 19 | 20 | html_title = "Tierkreis python package API documentation." 21 | 22 | html_theme_options = { 23 | "repository_url": "https://github.com/CQCL/tierkreis", 24 | "use_repository_button": True, 25 | "navigation_with_keys": True, 26 | "logo": { 27 | "image_light": "_static/Quantinuum_logo_black.png", 28 | "image_dark": "_static/Quantinuum_logo_white.png", 29 | }, 30 | } 31 | 32 | html_static_path = ["../_static"] 33 | html_css_files = ["custom.css"] 34 | 35 | autosummary_generate = True 36 | 37 | templates_path = ["_templates"] 38 | exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"] 39 | 40 | intersphinx_mapping = { 41 | "python": ("https://docs.python.org/3/", None), 42 | } 43 | -------------------------------------------------------------------------------- /examples/example_workers/qsci_worker/qsci/active_space.py: -------------------------------------------------------------------------------- 1 | def get_frozen( 2 | mo_occ: list[int], 3 | n_cas: int, 4 | n_elecas: int, 5 | ) -> list[int]: 6 | frozen = [] 7 | n_orbs = len(mo_occ) 8 | n_core = get_n_core(mo_occ, n_elecas) 9 | if n_core + n_cas > n_orbs: 10 | raise ValueError("active space is larger than basis set") 11 | for i in range(n_orbs): 12 | # print(i, i < n_core, i >= n_core + n_cas) 13 | if i < n_core or i >= n_core + n_cas: 14 | frozen.append(i) 15 | return frozen 16 | 17 | 18 | def get_n_core( 19 | mo_occ: list[int], 20 | n_elecas: int, 21 | ) -> int: 22 | n_elec = int(sum(mo_occ)) 23 | n_core = (n_elec - n_elecas) // 2 24 | return n_core 25 | 26 | 27 | def get_n_active( 28 | mo_occ: list[int], 29 | n_cas: int, 30 | n_elecas: int, 31 | ) -> int: 32 | n_frozen = len(get_frozen(mo_occ, n_cas, n_elecas)) 33 | n_active = len(mo_occ) - n_frozen 34 | return n_active 35 | 36 | 37 | def get_n_virtual( 38 | mo_occ: list[int], 39 | n_cas: int, 40 | n_elecas: int, 41 | ) -> int: 42 | n_frozen = len(get_frozen(mo_occ, n_cas, n_elecas)) 43 | n_core = get_n_core(mo_occ, n_elecas) 44 | n_virtual = n_frozen - n_core 45 | return n_virtual 46 | -------------------------------------------------------------------------------- /examples/example_workers/chemistry_worker/chemistry/active_space.py: -------------------------------------------------------------------------------- 1 | def get_frozen( 2 | mo_occ: list[int], 3 | n_cas: int, 4 | n_elecas: int, 5 | ) -> list[int]: 6 | frozen = [] 7 | n_orbs = len(mo_occ) 8 | n_core = get_n_core(mo_occ, n_elecas) 9 | if n_core + n_cas > n_orbs: 10 | raise ValueError("active space is larger than basis set") 11 | for i in range(n_orbs): 12 | # print(i, i < n_core, i >= n_core + n_cas) 13 | if i < n_core or i >= n_core + n_cas: 14 | frozen.append(i) 15 | return frozen 16 | 17 | 18 | def get_n_core( 19 | mo_occ: list[int], 20 | n_elecas: int, 21 | ) -> int: 22 | n_elec = int(sum(mo_occ)) 23 | n_core = (n_elec - n_elecas) // 2 24 | return n_core 25 | 26 | 27 | def get_n_active( 28 | mo_occ: list[int], 29 | n_cas: int, 30 | n_elecas: int, 31 | ) -> int: 32 | n_frozen = len(get_frozen(mo_occ, n_cas, n_elecas)) 33 | n_active = len(mo_occ) - n_frozen 34 | return n_active 35 | 36 | 37 | def get_n_virtual( 38 | mo_occ: list[int], 39 | n_cas: int, 40 | n_elecas: int, 41 | ) -> int: 42 | n_frozen = len(get_frozen(mo_occ, n_cas, n_elecas)) 43 | n_core = get_n_core(mo_occ, n_elecas) 44 | n_virtual = n_frozen - n_core 45 | return n_virtual 46 | -------------------------------------------------------------------------------- /examples/nexus_polling.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | from uuid import UUID 3 | from qnexus import AerConfig 4 | from pytket.qasm.qasm import circuit_from_qasm 5 | 6 | from tierkreis.consts import PACKAGE_PATH 7 | from tierkreis.controller import run_graph 8 | from tierkreis.graphs.nexus.submit_poll import nexus_submit_and_poll 9 | from tierkreis.storage import FileStorage, read_outputs 10 | from tierkreis.executor import UvExecutor 11 | 12 | aer_config = AerConfig() 13 | circuit = circuit_from_qasm(Path(__file__).parent / "data" / "ghz_state_n23.qasm") 14 | circuits = [circuit] 15 | 16 | 17 | def main(): 18 | g = nexus_submit_and_poll() 19 | storage = FileStorage(UUID(int=107), do_cleanup=True) 20 | executor = UvExecutor(PACKAGE_PATH / ".." / "tierkreis_workers", storage.logs_path) 21 | 22 | run_graph( 23 | storage, 24 | executor, 25 | g, 26 | { 27 | "project_name": "2025-tkr-test", 28 | "job_name": "job-1", 29 | "circuits": circuits, 30 | "n_shots": [30] * len(circuits), 31 | "backend_config": aer_config, 32 | }, 33 | polling_interval_seconds=0.1, 34 | ) 35 | res = read_outputs(g, storage) 36 | print(res) 37 | 38 | 39 | if __name__ == "__main__": 40 | main() 41 | -------------------------------------------------------------------------------- /infra/slurm_local/main.py: -------------------------------------------------------------------------------- 1 | # /// script 2 | # requires-python = ">=3.12" 3 | # dependencies = ["tierkreis", "mpi4py"] 4 | # [tool.uv.sources] 5 | # tierkreis = { path = "/tierkreis", editable = true } 6 | # /// 7 | import logging 8 | import socket 9 | from sys import argv 10 | 11 | from tierkreis import Worker 12 | from mpi4py import MPI # type: ignore 13 | 14 | logger = logging.getLogger(__name__) 15 | worker = Worker("slurm_mpi_worker") 16 | 17 | comm = MPI.COMM_WORLD 18 | 19 | 20 | def _proc_info() -> dict[str, int | str]: 21 | """Returns a dictionary with the current process's details.""" 22 | comm = MPI.COMM_WORLD 23 | rank = comm.Get_rank() 24 | hostname = socket.gethostname() 25 | return {"rank": rank, "hostname": hostname} 26 | 27 | 28 | @worker.task() 29 | def mpi_rank_info() -> str | None: 30 | comm = MPI.COMM_WORLD 31 | rank = comm.Get_rank() 32 | size = comm.Get_size() 33 | info = _proc_info() 34 | all_processes_info = comm.gather(info, root=0) 35 | print(all_processes_info) 36 | if rank == 0: 37 | return "\n".join( 38 | f"Rank {info['rank']} out of {size} on {info['hostname']}." 39 | for info in all_processes_info 40 | ) 41 | return None 42 | 43 | 44 | if __name__ == "__main__": 45 | worker.app(argv) 46 | -------------------------------------------------------------------------------- /tierkreis_visualization/tierkreis_visualization/static/utils.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | /** 4 | * Creates a JSEdge from a PyEdge model. 5 | * @param {PyEdge} py_edge 6 | * @returns {JSEdge} 7 | */ 8 | function createJSEdge(py_edge) { 9 | return { 10 | id: `${py_edge.from_port}->${py_edge.to_port}:${py_edge.from_node}->${py_edge.to_node}`, 11 | from: py_edge.from_node, 12 | to: py_edge.to_node, 13 | title: `${py_edge.from_port}->${py_edge.to_port}`, 14 | label: `${py_edge.from_port}->${py_edge.to_port}`, 15 | arrows: "to", 16 | dashes: py_edge.conditional, 17 | }; 18 | } 19 | 20 | /** 21 | * Creates a JSNode from a PyNode model. 22 | * @param {PyNode} py_node 23 | * @returns {JSNode} A newly created user. 24 | */ 25 | function createJSNode(py_node) { 26 | return { 27 | id: py_node.id, 28 | title: `Function name: ${py_node.function_name}\nStatus: ${py_node.status}`, 29 | label: py_node.function_name, 30 | status: py_node.status, 31 | color: { 32 | "Not started": colors.neutral[0], 33 | Started: colors.amber.primary, 34 | Error: colors.red.primary, 35 | Finished: colors.green.primary, 36 | }[py_node.status], 37 | shape: ["input", "output"].includes(py_node.function_name) 38 | ? "ellipse" 39 | : "box", 40 | }; 41 | } 42 | -------------------------------------------------------------------------------- /examples/example_workers/chemistry_worker/stubs.py: -------------------------------------------------------------------------------- 1 | """Code generated from chemistry_worker namespace. Please do not edit.""" 2 | 3 | from typing import NamedTuple, Protocol 4 | from tierkreis.controller.data.models import TKR 5 | from tierkreis.controller.data.types import Struct 6 | 7 | 8 | class CompleteActiveSpace(Struct, Protocol): 9 | n: int # noqa: F821 # fmt: skip 10 | n_ele: int # noqa: F821 # fmt: skip 11 | 12 | 13 | class Hamiltonian(Struct, Protocol): 14 | h0: float # noqa: F821 # fmt: skip 15 | h1: list[list[float]] # noqa: F821 # fmt: skip 16 | h2: list[list[list[list[float]]]] # noqa: F821 # fmt: skip 17 | 18 | 19 | class Molecule(Struct, Protocol): 20 | basis: str # noqa: F821 # fmt: skip 21 | charge: int # noqa: F821 # fmt: skip 22 | geometry: list[tuple[str, list[float]]] # noqa: F821 # fmt: skip 23 | 24 | 25 | class make_ham(NamedTuple): 26 | molecule: TKR[Molecule] # noqa: F821 # fmt: skip 27 | mo_occ: TKR[list[int]] # noqa: F821 # fmt: skip 28 | cas: TKR[CompleteActiveSpace] # noqa: F821 # fmt: skip 29 | 30 | @staticmethod 31 | def out() -> type[TKR[Hamiltonian]]: # noqa: F821 # fmt: skip 32 | return TKR[Hamiltonian] # noqa: F821 # fmt: skip 33 | 34 | @property 35 | def namespace(self) -> str: 36 | return "chemistry_worker" 37 | -------------------------------------------------------------------------------- /tierkreis_visualization/tierkreis_visualization/templates/macros/header.html: -------------------------------------------------------------------------------- 1 | {% macro header_macro(links) -%} 2 |
5 |
6 |

9 | 10 | Tierkreis Workflow Visualizer

11 | VERSION 1 18 |
19 |
20 | {%- endmacro %} 21 | -------------------------------------------------------------------------------- /.github/actions/setup_deps/action.yml: -------------------------------------------------------------------------------- 1 | name: Set up dependencies for tasks 2 | 3 | inputs: 4 | python_version: 5 | description: "Python version to use." 6 | required: false 7 | default: "3.12" 8 | type: string 9 | include_node: 10 | description: "Install node, bun etc.." 11 | type: string 12 | required: false 13 | default: "no" 14 | include_just: 15 | description: "Install the just task runner." 16 | type: string 17 | required: false 18 | default: "yes" 19 | 20 | runs: 21 | using: "composite" 22 | steps: 23 | - name: Set up uv & venv 24 | uses: astral-sh/setup-uv@v5 25 | with: 26 | version: "0.6.6" 27 | enable-cache: true 28 | cache-suffix: ${{ inputs.python_version }} 29 | python-version: ${{ inputs.python_version }} 30 | cache-dependency-glob: uv.lock 31 | 32 | - name: Set up just 33 | if: inputs.include_just == 'yes' 34 | uses: extractions/setup-just@v3 35 | 36 | - name: Set up node 37 | uses: actions/setup-node@v5 38 | if: inputs.include_node == 'yes' 39 | with: 40 | node-version: "24.8.0" 41 | package-manager-cache: false 42 | 43 | - name: Set up bun 44 | uses: oven-sh/setup-bun@v2 45 | if: inputs.include_node == 'yes' 46 | with: 47 | bun-version: "1.2.13" 48 | no-cache: true 49 | -------------------------------------------------------------------------------- /tierkreis_visualization/frontend/src/data/logs.ts: -------------------------------------------------------------------------------- 1 | import { useQuery } from "@tanstack/react-query"; 2 | 3 | import { URL } from "./constants"; 4 | 5 | const fetchText = async ( 6 | workflowId: string, 7 | node_location: string, 8 | type: "errors" | "logs" 9 | ) => { 10 | const url = `${URL}/${workflowId}/nodes/${node_location}/${type}`; 11 | const response = await fetch(url, { 12 | method: "GET", 13 | headers: { Accept: "application/text" }, 14 | }); 15 | if (!response.ok) { 16 | throw new Error("Network response was not ok"); 17 | } 18 | return response.text(); 19 | }; 20 | 21 | export const useLogs = ( 22 | workflowId: string, 23 | node_location: string, 24 | node_title: string 25 | ) => { 26 | return useQuery({ 27 | queryKey: ["logs", workflowId, node_location], 28 | queryFn: () => fetchText(workflowId, node_location, "logs"), 29 | enabled: !!workflowId && !!node_location && node_title == "Function", 30 | }); 31 | }; 32 | 33 | export const useErrors = ( 34 | workflowId: string, 35 | node_location: string, 36 | status: "Not started" | "Started" | "Error" | "Finished" 37 | ) => { 38 | return useQuery({ 39 | queryKey: ["errors", workflowId, node_location], 40 | queryFn: () => fetchText(workflowId, node_location, "errors"), 41 | enabled: !!workflowId && !!node_location && status === "Error", 42 | }); 43 | }; 44 | -------------------------------------------------------------------------------- /examples/example_workers/openssl_worker/test/tierkreis.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Boilerplate for reading WorkerCallArgs in Bash. 4 | 5 | worker_call_args_file=$1 6 | checkpoints_directory=~/.tierkreis/checkpoints 7 | 8 | echo "START openssl_worker $checkpoints_directory/$worker_call_args_file" 9 | echo $(jq "." $checkpoints_directory/$worker_call_args_file) 10 | 11 | function_name=$checkpoints_directory/$(jq -r ".function_name" $checkpoints_directory/$worker_call_args_file) 12 | done_path=$checkpoints_directory/$(jq -r ".done_path" $checkpoints_directory/$worker_call_args_file) 13 | error_path=$checkpoints_directory/$(jq -r ".error_path" $checkpoints_directory/$worker_call_args_file) 14 | logs_path=$checkpoints_directory/$(jq -r ".logs_path" $checkpoints_directory/$worker_call_args_file) 15 | output_dir=$checkpoints_directory/$(jq -r ".output_dir" $checkpoints_directory/$worker_call_args_file) 16 | 17 | while read key ; do 18 | declare "input_${key}_file"="$checkpoints_directory/$(jq -r ".inputs.$key" $checkpoints_directory/$worker_call_args_file)" 19 | done < <(jq -r '.inputs|keys[]' $checkpoints_directory/$worker_call_args_file) 20 | 21 | while read key ; do 22 | declare "output_${key}_file"="$checkpoints_directory/$(jq -r ".outputs.$key" $checkpoints_directory/$worker_call_args_file)" 23 | done < <(jq -r '.outputs|keys[]' $checkpoints_directory/$worker_call_args_file) 24 | -------------------------------------------------------------------------------- /examples/scipy_graph.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | from typing import NamedTuple 3 | from uuid import UUID 4 | from tierkreis.builder import GraphBuilder 5 | from tierkreis.controller.data.core import EmptyModel 6 | from tierkreis.controller.data.models import TKR, OpaqueType 7 | from example_workers.scipy_worker.stubs import ( 8 | transpose, 9 | reshape, 10 | linspace, 11 | add_point, 12 | eval_point, 13 | ) 14 | from tierkreis.storage import FileStorage, read_outputs 15 | from tierkreis.executor import UvExecutor 16 | from tierkreis import run_graph 17 | 18 | NDArray = OpaqueType["numpy.ndarray"] 19 | 20 | 21 | class ScipyOutputs(NamedTuple): 22 | a: TKR[NDArray] 23 | p: TKR[float] 24 | 25 | 26 | g = GraphBuilder(EmptyModel, ScipyOutputs) 27 | onedim = g.task(linspace(g.const(0), g.const(10))) 28 | 29 | pointed = g.task(add_point(onedim, g.const(0))) 30 | scalar = g.task(eval_point(pointed)) 31 | 32 | twodim = g.task(reshape(onedim, g.const([5, 10]))) 33 | a = g.task(transpose(twodim)) 34 | g.outputs(ScipyOutputs(a, scalar)) 35 | 36 | 37 | if __name__ == "__main__": 38 | storage = FileStorage(UUID(int=207), do_cleanup=True, name="scipy_graph") 39 | executor = UvExecutor(Path(__file__).parent / "example_workers", storage.logs_path) 40 | run_graph(storage, executor, g, {}) 41 | 42 | outputs = read_outputs(g, storage) 43 | print(outputs) 44 | -------------------------------------------------------------------------------- /examples/data/ghz_state_n23.qasm: -------------------------------------------------------------------------------- 1 | OPENQASM 2.0; 2 | include "qelib1.inc"; 3 | qreg q[23]; 4 | creg c[23]; 5 | creg meas[23]; 6 | h q[0]; 7 | cx q[0],q[1]; 8 | cx q[1],q[2]; 9 | cx q[2],q[3]; 10 | cx q[3],q[4]; 11 | cx q[4],q[5]; 12 | cx q[5],q[6]; 13 | cx q[6],q[7]; 14 | cx q[7],q[8]; 15 | cx q[8],q[9]; 16 | cx q[9],q[10]; 17 | cx q[10],q[11]; 18 | cx q[11],q[12]; 19 | cx q[12],q[13]; 20 | cx q[13],q[14]; 21 | cx q[14],q[15]; 22 | cx q[15],q[16]; 23 | cx q[16],q[17]; 24 | cx q[17],q[18]; 25 | cx q[18],q[19]; 26 | cx q[19],q[20]; 27 | cx q[20],q[21]; 28 | cx q[21],q[22]; 29 | barrier q[0],q[1],q[2],q[3],q[4],q[5],q[6],q[7],q[8],q[9],q[10],q[11],q[12],q[13],q[14],q[15],q[16],q[17],q[18],q[19],q[20],q[21],q[22]; 30 | measure q[0] -> meas[0]; 31 | measure q[1] -> meas[1]; 32 | measure q[2] -> meas[2]; 33 | measure q[3] -> meas[3]; 34 | measure q[4] -> meas[4]; 35 | measure q[5] -> meas[5]; 36 | measure q[6] -> meas[6]; 37 | measure q[7] -> meas[7]; 38 | measure q[8] -> meas[8]; 39 | measure q[9] -> meas[9]; 40 | measure q[10] -> meas[10]; 41 | measure q[11] -> meas[11]; 42 | measure q[12] -> meas[12]; 43 | measure q[13] -> meas[13]; 44 | measure q[14] -> meas[14]; 45 | measure q[15] -> meas[15]; 46 | measure q[16] -> meas[16]; 47 | measure q[17] -> meas[17]; 48 | measure q[18] -> meas[18]; 49 | measure q[19] -> meas[19]; 50 | measure q[20] -> meas[20]; 51 | measure q[21] -> meas[21]; 52 | measure q[22] -> meas[22]; -------------------------------------------------------------------------------- /tierkreis/tierkreis/controller/executor/multiple.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | 3 | from tierkreis.controller.executor.protocol import ControllerExecutor 4 | from tierkreis.exceptions import TierkreisError 5 | 6 | 7 | class MultipleExecutor: 8 | """Composes multiple executors into a single object. 9 | 10 | Implements: :py:class:`tierkreis.controller.executor.protocol.ControllerExecutor` 11 | """ 12 | 13 | def __init__( 14 | self, 15 | default: ControllerExecutor, 16 | executors: dict[str, ControllerExecutor], 17 | assignments: dict[str, str], 18 | ) -> None: 19 | self.default = default 20 | self.executors = executors 21 | self.assignments = assignments 22 | 23 | def run(self, launcher_name: str, worker_call_args_path: Path) -> None: 24 | executor_name = self.assignments.get(launcher_name, None) 25 | # If there is no assignment for the worker, use the default. 26 | if executor_name is None: 27 | return self.default.run(launcher_name, worker_call_args_path) 28 | 29 | executor = self.executors.get(executor_name) 30 | if executor is None: 31 | raise TierkreisError( 32 | f"{launcher_name} is assigned to non-existent executor name: {executor_name}." 33 | ) 34 | 35 | return executor.run(launcher_name, worker_call_args_path) 36 | -------------------------------------------------------------------------------- /tierkreis/tests/controller/partial_graphdata.py: -------------------------------------------------------------------------------- 1 | from tierkreis.controller.data.graph import GraphData 2 | 3 | 4 | def ternary_add() -> GraphData: 5 | g = GraphData() 6 | a = g.input("a") 7 | b = g.input("b") 8 | c = g.input("c") 9 | 10 | step_one = g.func("builtins.iadd", {"a": a, "b": b})("value") 11 | step_two = g.func("builtins.iadd", {"a": step_one, "b": c})("value") 12 | 13 | g.output({"ternary_add_output": step_two}) 14 | return g 15 | 16 | 17 | def double_partial() -> GraphData: 18 | g = GraphData() 19 | 20 | one = g.const(1) 21 | two = g.const(2) 22 | three = g.const(3) 23 | termary_add_const = g.const(ternary_add()) 24 | 25 | add_add_1 = g.eval(termary_add_const, {"a": one})("body") 26 | add_3 = g.eval(add_add_1, {"b": two})("body") 27 | six = g.eval(add_3, {"c": three})("ternary_add_output") 28 | 29 | g.output({"double_partial_output": six}) 30 | return g 31 | 32 | 33 | def partial_intersection() -> GraphData: 34 | g = GraphData() 35 | one = g.const(1) 36 | two = g.const(2) 37 | three = g.const(3) 38 | termary_add_const = g.const(ternary_add()) 39 | 40 | add_add_1 = g.eval(termary_add_const, {"a": one})("body") 41 | add_3 = g.eval(add_add_1, {"a": one, "b": two})("body") 42 | six = g.eval(add_3, {"c": three})("ternary_add_output") 43 | 44 | g.output({"double_partial_output": six}) 45 | return g 46 | -------------------------------------------------------------------------------- /tierkreis_visualization/frontend/src/components/workflow-sidebar.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | Sidebar, 3 | SidebarContent, 4 | SidebarGroup, 5 | SidebarGroupContent, 6 | SidebarGroupLabel, 7 | SidebarMenu, 8 | SidebarMenuButton, 9 | SidebarMenuItem, 10 | } from "@/components/ui/sidebar"; 11 | import { Link } from "react-router"; 12 | import { Workflow } from "@/components/types"; 13 | 14 | export function WorkflowSidebar({ 15 | workflows, 16 | workflowId, 17 | }: { 18 | workflows: Workflow[]; 19 | workflowId: string; 20 | }) { 21 | return ( 22 | 23 | 24 | 25 | Workflows 26 | 27 | 28 | {workflows.map((workflow) => ( 29 | 30 | 34 | 35 | {workflow.name} 36 | 37 | 38 | 39 | ))} 40 | 41 | 42 | 43 | 44 | 45 | ); 46 | } 47 | -------------------------------------------------------------------------------- /tierkreis/tests/controller/test_models.py: -------------------------------------------------------------------------------- 1 | from types import NoneType 2 | from typing import NamedTuple 3 | import pytest 4 | from tierkreis.controller.data.models import PModel, dict_from_pmodel, portmapping 5 | from tierkreis.controller.data.types import PType 6 | from tests.controller.test_types import ptypes 7 | 8 | 9 | @portmapping 10 | class NamedPModel(NamedTuple): 11 | a: bool 12 | b: int 13 | c: float 14 | d: str 15 | e: NoneType 16 | f: list[str] 17 | g: bytes 18 | h: tuple[int, str] 19 | i: list[list[int] | int] 20 | 21 | 22 | @pytest.mark.parametrize("pmodel", ptypes) 23 | def test_dict_from_pmodel_unnested(pmodel: PModel): 24 | assert dict_from_pmodel(pmodel) == {"value": pmodel} 25 | 26 | 27 | named_p_model = NamedPModel( 28 | a=True, 29 | b=5, 30 | c=56.7, 31 | d="test", 32 | e=None, 33 | f=["one", "two", "three"], 34 | g=b"test bytes", 35 | h=(5, "test"), 36 | i=[[], 2], 37 | ) 38 | named_p_model_expected = { 39 | "a": True, 40 | "b": 5, 41 | "c": 56.7, 42 | "d": "test", 43 | "e": None, 44 | "f": ["one", "two", "three"], 45 | "g": b"test bytes", 46 | "h": (5, "test"), 47 | "i": [[], 2], 48 | } 49 | 50 | pmodels = [(named_p_model, named_p_model_expected)] 51 | 52 | 53 | @pytest.mark.parametrize("pmodel,expected", pmodels) 54 | def test_dict_from_pmodel_nested(pmodel: PModel, expected: dict[str, PType]): 55 | assert dict_from_pmodel(pmodel) == expected 56 | -------------------------------------------------------------------------------- /examples/example_workers/chemistry_worker/main.py: -------------------------------------------------------------------------------- 1 | # /// script 2 | # requires-python = ">=3.12" 3 | # dependencies = ["pydantic", "tierkreis", "pyscf"] 4 | # 5 | # [tool.uv.sources] 6 | # tierkreis = { path = "../../../tierkreis", editable = true } 7 | # /// 8 | import logging 9 | from sys import argv 10 | from typing import NamedTuple 11 | 12 | 13 | from chemistry.active_space import get_frozen 14 | from chemistry.molecule import extract_hamiltonian_rhf 15 | 16 | from tierkreis import Worker 17 | 18 | logger = logging.getLogger(__name__) 19 | 20 | worker = Worker("chemistry_worker") 21 | 22 | 23 | class Molecule(NamedTuple): 24 | geometry: list[tuple[str, list[float]]] 25 | basis: str 26 | charge: int 27 | 28 | 29 | class CompleteActiveSpace(NamedTuple): 30 | n: int 31 | n_ele: int 32 | 33 | 34 | class Hamiltonian(NamedTuple): 35 | h0: float 36 | h1: list[list[float]] 37 | h2: list[list[list[list[float]]]] 38 | 39 | 40 | @worker.task() 41 | def make_ham( 42 | molecule: Molecule, 43 | mo_occ: list[int], 44 | cas: CompleteActiveSpace, 45 | ) -> Hamiltonian: 46 | # Construct the frozen orbital lists. 47 | frozen = get_frozen(mo_occ, cas.n, cas.n_ele) 48 | h0, h1, h2 = extract_hamiltonian_rhf( 49 | molecule.geometry, 50 | molecule.basis, 51 | charge=molecule.charge, 52 | frozen=frozen, 53 | ) 54 | return Hamiltonian(h0=h0, h1=h1.tolist(), h2=h2.tolist()) 55 | 56 | 57 | if __name__ == "__main__": 58 | worker.app(argv) 59 | -------------------------------------------------------------------------------- /tierkreis/tierkreis/controller/data/core.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | from typing import ( 3 | Annotated, 4 | Any, 5 | Callable, 6 | Literal, 7 | NamedTuple, 8 | Protocol, 9 | SupportsIndex, 10 | get_args, 11 | get_origin, 12 | runtime_checkable, 13 | ) 14 | 15 | 16 | PortID = str 17 | NodeIndex = int 18 | ValueRef = tuple[NodeIndex, PortID] 19 | SerializationFormat = Literal["bytes", "json", "unknown"] 20 | 21 | 22 | class EmptyModel(NamedTuple): ... 23 | 24 | 25 | @runtime_checkable 26 | class RestrictedNamedTuple[T](Protocol): 27 | """A NamedTuple whose members are restricted to being of type T.""" 28 | 29 | def _asdict(self) -> dict[str, T]: ... 30 | def __getitem__(self, key: SupportsIndex, /) -> T: ... 31 | 32 | 33 | @dataclass 34 | class Serializer: 35 | serializer: Callable[[Any], Any] 36 | serialization_method: SerializationFormat = "bytes" 37 | 38 | 39 | @dataclass 40 | class Deserializer: 41 | deserializer: Callable[[Any], Any] 42 | serialization_method: SerializationFormat = "bytes" 43 | 44 | 45 | def get_t_from_args[T](t: type[T], hint: type | None) -> T | None: 46 | if hint is None or get_origin(hint) is not Annotated: 47 | return None 48 | 49 | for arg in get_args(hint): 50 | if isinstance(arg, t): 51 | return arg 52 | 53 | 54 | def get_serializer(hint: type | None): 55 | return get_t_from_args(Serializer, hint) 56 | 57 | 58 | def get_deserializer(hint: type | None): 59 | return get_t_from_args(Deserializer, hint) 60 | -------------------------------------------------------------------------------- /tierkreis_visualization/tierkreis_visualization/static/workflows.html.css: -------------------------------------------------------------------------------- 1 | .workflowlist { 2 | display: flex; 3 | flex-direction: column; 4 | gap:0.05rem; 5 | } 6 | 7 | .workflowlist__item { 8 | padding: 0.75rem; 9 | border-radius: 0.25rem; 10 | background: var(--neutral-0); 11 | border: 1px solid var(--neutral-200); 12 | text-decoration: none; 13 | } 14 | 15 | .workflowlist__item:hover { 16 | background: var(--neutral-100); 17 | transition-property: background; 18 | transition-duration: 150ms; 19 | cursor: pointer; 20 | } 21 | .workflowlist__item__title { 22 | display: flex; 23 | align-items: center; 24 | gap: 0.5rem; 25 | text-decoration: none; 26 | margin-bottom: 0.25rem; 27 | 28 | } 29 | .workflowlist__item__title::before { 30 | content: url("data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%2218%22%20height%3D%2218%22%20viewBox%3D%220%200%2024%2024%22%20fill%3D%22none%22%20stroke%3D%22%23737373%22%20stroke-width%3D%222%22%20stroke-linecap%3D%22round%22%20stroke-linejoin%3D%22round%22%20class%3D%22lucide%20lucide-workflow-icon%20lucide-workflow%22%3E%3Crect%20width%3D%228%22%20height%3D%228%22%20x%3D%223%22%20y%3D%223%22%20rx%3D%222%22%2F%3E%3Cpath%20d%3D%22M7%2011v4a2%202%200%200%200%202%202h4%22%2F%3E%3Crect%20width%3D%228%22%20height%3D%228%22%20x%3D%2213%22%20y%3D%2213%22%20rx%3D%222%22%2F%3E%3C%2Fsvg%3E"); 31 | } 32 | 33 | .workflowlist__item__id { 34 | color: var(--neutral-400); 35 | font-size: 0.675rem; 36 | margin-left: 1.5rem; 37 | text-decoration: none; 38 | } 39 | -------------------------------------------------------------------------------- /tierkreis_visualization/frontend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tierkreis-frontend", 3 | "version": "0.0.0", 4 | "type": "module", 5 | "scripts": { 6 | "dev": "vite build --watch", 7 | "build": "tsc && vite build", 8 | "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0", 9 | "preview": "vite preview" 10 | }, 11 | "dependencies": { 12 | "@dagrejs/dagre": "^1.1.4", 13 | "@radix-ui/react-dialog": "^1.1.14", 14 | "@radix-ui/react-hover-card": "^1.1.14", 15 | "@radix-ui/react-separator": "^1.1.7", 16 | "@radix-ui/react-slot": "^1.2.3", 17 | "@radix-ui/react-tooltip": "^1.2.7", 18 | "@tailwindcss/vite": "^4.1.7", 19 | "@tanstack/react-query": "^5.81.5", 20 | "@tanstack/react-query-devtools": "^5.82.0", 21 | "@xyflow/react": "^12.7.0", 22 | "class-variance-authority": "^0.7.1", 23 | "clsx": "^2.1.1", 24 | "lucide-react": "^0.511.0", 25 | "react": "^18.2.0", 26 | "react-dom": "^18.2.0", 27 | "react-router": "^7.6.2", 28 | "tailwind-merge": "^3.3.0", 29 | "tw-animate-css": "^1.3.5" 30 | }, 31 | "license": "MIT", 32 | "devDependencies": { 33 | "@types/node": "^22.15.23", 34 | "@types/react": "^18.2.53", 35 | "@types/react-dom": "^18.2.18", 36 | "@typescript-eslint/eslint-plugin": "^6.20.0", 37 | "@typescript-eslint/parser": "^6.20.0", 38 | "@vitejs/plugin-react": "^5.0.0", 39 | "eslint": "^8.56.0", 40 | "eslint-plugin-react-hooks": "^4.6.0", 41 | "eslint-plugin-react-refresh": "^0.4.5", 42 | "typescript": "^5.3.3", 43 | "vite": "^7.1.11" 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /tierkreis/CITATION.bib: -------------------------------------------------------------------------------- 1 | @INPROCEEDINGS {10025549, 2 | author = {S. Sivarajah and L. Heidemann and A. Lawrence and R. Duncan}, 3 | booktitle = {2022 IEEE/ACM Third International Workshop on Quantum Computing Software (QCS)}, 4 | title = {Tierkreis: a Dataflow Framework for Hybrid Quantum-Classical Computing}, 5 | year = {2022}, 6 | volume = {}, 7 | issn = {}, 8 | pages = {12-21}, 9 | abstract = {We present Tierkreis, a higher-order dataflow graph program representation and runtime designed for compositional, quantum-classical hybrid algorithms. The design of the system is motivated by the remote nature of quantum computers, the need for hybrid algorithms to involve cloud and distributed computing, and the long-running nature of these algorithms. The graph-based representation reflects how designers reason about and visualise algorithms, and allows automatic parallelism and asynchronicity. A strong, static type system and higher-order semantics allow for high expressivity and compositionality in the program. The flexible runtime protocol enables third-party developers to add functionality using any language or environment. With Tierkreis, quantum software developers can easily build, visualise, verify, test, and debug complex hybrid workflows, and immediately deploy them to the cloud or a custom distributed environment.}, 10 | keywords = {visualization;runtime;protocols;codes;software algorithms;buildings;semantics}, 11 | doi = {10.1109/QCS56647.2022.00007}, 12 | url = {https://doi.ieeecomputersociety.org/10.1109/QCS56647.2022.00007}, 13 | publisher = {IEEE Computer Society}, 14 | address = {Los Alamitos, CA, USA}, 15 | month = {nov} 16 | } 17 | 18 | -------------------------------------------------------------------------------- /tierkreis/docs/api-docs/_templates/autosummary/module.rst: -------------------------------------------------------------------------------- 1 | .. 2 | Custom module template to make sphinx-autosummary list the full API doc after 3 | the summary. See https://github.com/sphinx-doc/sphinx/issues/7912 4 | 5 | {{ name | escape | underline}} 6 | 7 | .. automodule:: {{ fullname }} 8 | 9 | {% block attributes %} 10 | {% if attributes %} 11 | .. rubric:: Module attributes 12 | 13 | .. autosummary:: 14 | :toctree: 15 | {% for item in attributes %} 16 | {{ item }} 17 | {%- endfor %} 18 | {% endif %} 19 | {% endblock %} 20 | 21 | {% block functions %} 22 | {% if functions %} 23 | .. rubric:: {{ _('Functions') }} 24 | 25 | .. autosummary:: 26 | :toctree: 27 | :nosignatures: 28 | {% for item in functions %} 29 | {{ item }} 30 | {%- endfor %} 31 | {% endif %} 32 | {% endblock %} 33 | 34 | {% block classes %} 35 | {% if classes %} 36 | .. rubric:: {{ _('Classes') }} 37 | 38 | .. autosummary:: 39 | :toctree: 40 | :nosignatures: 41 | {% for item in classes %} 42 | {{ item }} 43 | {%- endfor %} 44 | {% endif %} 45 | {% endblock %} 46 | 47 | {% block exceptions %} 48 | {% if exceptions %} 49 | .. rubric:: {{ _('Exceptions') }} 50 | 51 | .. autosummary:: 52 | :toctree: 53 | {% for item in exceptions %} 54 | {{ item }} 55 | {%- endfor %} 56 | {% endif %} 57 | {% endblock %} 58 | 59 | {% block modules %} 60 | {% if modules %} 61 | .. autosummary:: 62 | :toctree: 63 | :recursive: 64 | {% for item in modules %} 65 | {{ item }} 66 | {%- endfor %} 67 | {% endif %} 68 | {% endblock %} 69 | -------------------------------------------------------------------------------- /tierkreis/tierkreis/worker/storage/in_memory.py: -------------------------------------------------------------------------------- 1 | import fnmatch 2 | import json 3 | import logging 4 | from pathlib import Path 5 | 6 | from tierkreis.controller.data.location import WorkerCallArgs 7 | from tierkreis.controller.storage.in_memory import ( 8 | ControllerInMemoryStorage, 9 | InMemoryFileData, 10 | ) 11 | from tierkreis.exceptions import TierkreisError 12 | 13 | 14 | logger = logging.getLogger(__name__) 15 | 16 | 17 | class InMemoryWorkerStorage: 18 | def __init__(self, controller_storage: ControllerInMemoryStorage) -> None: 19 | self.controller_storage = controller_storage 20 | 21 | def resolve(self, path: Path | str) -> Path: 22 | return Path(path) 23 | 24 | def read_call_args(self, path: Path) -> WorkerCallArgs: 25 | bs = self.controller_storage.files[path].value 26 | return WorkerCallArgs(**json.loads(bs)) 27 | 28 | def read_input(self, path: Path) -> bytes: 29 | return self.controller_storage.files[path].value 30 | 31 | def write_output(self, path: Path, value: bytes) -> None: 32 | self.controller_storage.files[path] = InMemoryFileData(value) 33 | 34 | def glob(self, path_string: str) -> list[str]: 35 | files = [str(x) for x in self.controller_storage.files.keys()] 36 | matching = fnmatch.filter(files, path_string) 37 | return matching 38 | 39 | def mark_done(self, path: Path) -> None: 40 | self.controller_storage.touch(path) 41 | 42 | def write_error(self, path: Path, error_logs: str) -> None: 43 | logger.error(error_logs) 44 | raise TierkreisError("Error occured when running graph in-memory.") 45 | -------------------------------------------------------------------------------- /tierkreis_visualization/tierkreis_visualization/data/map.py: -------------------------------------------------------------------------------- 1 | from pydantic import BaseModel 2 | from tierkreis.controller.data.location import Loc 3 | from tierkreis.controller.storage.adjacency import outputs_iter 4 | from tierkreis.controller.storage.protocol import ControllerStorage 5 | from tierkreis.controller.data.graph import Map 6 | from tierkreis.exceptions import TierkreisError 7 | from tierkreis_visualization.data.eval import check_error 8 | from tierkreis_visualization.data.models import PyEdge, PyNode 9 | 10 | 11 | class MapNodeData(BaseModel): 12 | nodes: list[PyNode] 13 | edges: list[PyEdge] 14 | 15 | 16 | def get_map_node( 17 | storage: ControllerStorage, loc: Loc, map: Map, errored_nodes: list[Loc] 18 | ) -> MapNodeData: 19 | parent = loc.parent() 20 | if parent is None: 21 | raise TierkreisError("MAP node must have parent.") 22 | 23 | first_ref = next(x for x in map.inputs.values() if x[1] == "*") 24 | map_eles = outputs_iter(storage, parent.N(first_ref[0])) 25 | nodes: list[PyNode] = [] 26 | for i, ele in map_eles: 27 | node = PyNode( 28 | id=i, 29 | status="Started", 30 | function_name=ele, 31 | node_location=loc.M(i), 32 | started_time=storage.read_started_time(loc.M(i)) or "", 33 | finished_time=storage.read_finished_time(loc.M(i)) or "", 34 | ) 35 | if check_error(loc.M(i), errored_nodes): 36 | node.status = "Error" 37 | elif storage.is_node_finished(loc.M(i)): 38 | node.status = "Finished" 39 | nodes.append(node) 40 | 41 | return MapNodeData(nodes=nodes, edges=[]) 42 | -------------------------------------------------------------------------------- /tierkreis/tierkreis/controller/storage/adjacency.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from typing import assert_never 3 | 4 | from tierkreis.controller.data.core import PortID, ValueRef 5 | from tierkreis.controller.data.graph import ( 6 | NodeDef, 7 | ) 8 | from tierkreis.controller.data.location import Loc 9 | from tierkreis.controller.storage.protocol import ControllerStorage 10 | 11 | logger = logging.getLogger(__name__) 12 | 13 | 14 | def in_edges(node: NodeDef) -> dict[PortID, ValueRef]: 15 | parents = {k: v for k, v in node.inputs.items()} 16 | 17 | match node.type: 18 | case "eval": 19 | parents["body"] = node.graph 20 | case "loop": 21 | parents["body"] = node.body 22 | case "map": 23 | parents["body"] = node.body 24 | case "ifelse": 25 | parents["pred"] = node.pred 26 | case "eifelse": 27 | parents["pred"] = node.pred 28 | parents["body_true"] = node.if_true 29 | parents["body_false"] = node.if_false 30 | case "const" | "function" | "input" | "output": 31 | pass 32 | case _: 33 | assert_never(node) 34 | 35 | return parents 36 | 37 | 38 | def unfinished_inputs( 39 | storage: ControllerStorage, loc: Loc, node: NodeDef 40 | ) -> list[ValueRef]: 41 | ins = in_edges(node).values() 42 | ins = [x for x in ins if x[0] >= 0] # inputs at -1 already finished 43 | return [x for x in ins if not storage.is_node_finished(loc.N(x[0]))] 44 | 45 | 46 | def outputs_iter(storage: ControllerStorage, loc: Loc) -> list[tuple[int, PortID]]: 47 | eles = storage.read_output_ports(loc) 48 | return [(int(x.split("-")[-1]), x) for x in eles] 49 | -------------------------------------------------------------------------------- /tierkreis/tierkreis/worker/storage/filestorage.py: -------------------------------------------------------------------------------- 1 | import json 2 | from glob import glob 3 | import os 4 | from pathlib import Path 5 | 6 | from tierkreis.consts import TKR_DIR_KEY 7 | from tierkreis.controller.data.location import WorkerCallArgs 8 | 9 | 10 | class WorkerFileStorage: 11 | def __init__(self, tierkreis_dir: Path | None = None) -> None: 12 | if tierkreis_dir is not None: 13 | self.tierkreis_dir = tierkreis_dir 14 | elif tierkreis_dir_str := os.environ.get(TKR_DIR_KEY): 15 | self.tierkreis_dir = Path(tierkreis_dir_str).resolve() 16 | else: 17 | self.tierkreis_dir = Path.home() / ".tierkreis" / "checkpoints" 18 | 19 | def resolve(self, path: Path | str) -> Path: 20 | path = Path(path) 21 | return path if path.is_absolute() else self.tierkreis_dir / path 22 | 23 | def read_call_args(self, path: Path) -> WorkerCallArgs: 24 | with open(self.resolve(path), "r") as fh: 25 | return WorkerCallArgs(**json.loads(fh.read())) 26 | 27 | def read_input(self, path: Path) -> bytes: 28 | with open(self.resolve(path), "rb") as fh: 29 | return fh.read() 30 | 31 | def write_output(self, path: Path, value: bytes) -> None: 32 | with open(self.resolve(path), "wb+") as fh: 33 | fh.write(value) 34 | 35 | def glob(self, path_string: str) -> list[str]: 36 | return glob(str(self.resolve(path_string))) 37 | 38 | def mark_done(self, path: Path) -> None: 39 | self.resolve(path).touch() 40 | 41 | def write_error(self, path: Path, error_logs: str) -> None: 42 | with open(self.resolve(path), "w+") as f: 43 | f.write(error_logs) 44 | -------------------------------------------------------------------------------- /tierkreis_visualization/frontend/src/main.tsx: -------------------------------------------------------------------------------- 1 | import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; 2 | import React, { Suspense } from "react"; 3 | import ReactDOM from "react-dom/client"; 4 | import { 5 | createBrowserRouter, 6 | RouterProvider, 7 | type LoaderFunction, 8 | type Params, 9 | } from "react-router"; 10 | 11 | import App from "./App"; 12 | 13 | import { ReactFlowProvider } from "@xyflow/react"; 14 | import "@xyflow/react/dist/style.css"; 15 | import "./index.css"; 16 | import ErrorPage from "./error"; 17 | 18 | const queryClient = new QueryClient(); 19 | 20 | const workflowId: LoaderFunction = ({ params }: { params: Params }) => { 21 | return { params }; 22 | }; 23 | 24 | const element = ( 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | ); 33 | const router = createBrowserRouter([ 34 | { 35 | path: "/", 36 | element: element, 37 | errorElement: , 38 | }, 39 | { 40 | path: "/:workflowId", 41 | element: element, 42 | loader: workflowId, 43 | errorElement: , 44 | }, 45 | { 46 | path: "*/:workflowId", 47 | element: element, 48 | loader: workflowId, 49 | errorElement: , 50 | }, 51 | { 52 | path: "*", 53 | element: element, 54 | errorElement: , 55 | }, 56 | ]); 57 | 58 | ReactDOM.createRoot(document.getElementById("root")!).render( 59 | 60 | 61 | 62 | ); 63 | -------------------------------------------------------------------------------- /tierkreis_visualization/tierkreis_visualization/data/workflows.py: -------------------------------------------------------------------------------- 1 | import os 2 | from datetime import datetime 3 | from uuid import UUID 4 | 5 | from pydantic import BaseModel 6 | 7 | from tierkreis.controller.data.location import Loc 8 | from tierkreis_visualization.app_config import StorageType 9 | from tierkreis_visualization.config import CONFIG 10 | from tierkreis_visualization.storage import file_storage_fn 11 | 12 | 13 | class WorkflowDisplay(BaseModel): 14 | id: UUID 15 | id_int: int 16 | name: str | None 17 | start_time: str 18 | 19 | 20 | def get_workflows(storage_type: StorageType) -> list[WorkflowDisplay]: 21 | if storage_type == StorageType.GRAPHDATA: 22 | return [ 23 | WorkflowDisplay( 24 | id=UUID(int=0), 25 | id_int=0, 26 | name="tmp", 27 | start_time=datetime.now().isoformat(), 28 | ) 29 | ] 30 | return get_workflows_from_disk() 31 | 32 | 33 | def get_workflows_from_disk() -> list[WorkflowDisplay]: 34 | folders = os.listdir(CONFIG.tierkreis_path) 35 | folders.sort() 36 | workflows: list[WorkflowDisplay] = [] 37 | for folder in folders: 38 | try: 39 | id = UUID(folder) 40 | metadata = file_storage_fn(CONFIG.tierkreis_path)(id).read_metadata(Loc("")) 41 | name = metadata["name"] or "workflow" 42 | start = metadata.get("start_time", datetime.now().isoformat()) 43 | workflows.append( 44 | WorkflowDisplay(id=id, id_int=int(id), name=name, start_time=start) 45 | ) 46 | except (TypeError, ValueError): 47 | continue 48 | 49 | return workflows 50 | -------------------------------------------------------------------------------- /docs/source/conf.py: -------------------------------------------------------------------------------- 1 | # Configuration file for the Sphinx documentation builder. 2 | # 3 | # For the full list of built-in configuration values, see the documentation: 4 | # https://www.sphinx-doc.org/en/master/usage/configuration.html 5 | 6 | # -- Project information ----------------------------------------------------- 7 | # https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information 8 | 9 | project = "tierkreis" 10 | copyright = "2025, Quantinuum" 11 | author = "Quantinuum" 12 | 13 | # -- General configuration --------------------------------------------------- 14 | # https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration 15 | 16 | extensions = ["autodoc2", "myst_nb"] 17 | autodoc2_packages = [ 18 | "../../tierkreis/tierkreis", 19 | { 20 | "path": "../../tierkreis_workers/aer_worker/main.py", 21 | "module": "aer_worker", 22 | }, 23 | { 24 | "path": "../../tierkreis_workers/nexus_worker/main.py", 25 | "module": "nexus_worker", 26 | }, 27 | { 28 | "path": "../../tierkreis_workers/pytket_worker/main.py", 29 | "module": "pytket_worker", 30 | }, 31 | ] 32 | autodoc2_hidden_objects = ["private"] 33 | 34 | templates_path = ["_templates"] 35 | exclude_patterns = [] 36 | 37 | 38 | # -- Options for HTML output ------------------------------------------------- 39 | # https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output 40 | 41 | html_theme = "furo" 42 | html_static_path = ["_static"] 43 | html_favicon = "_static/quantinuum_favicon.svg" 44 | 45 | # -- Notebook options -------------------------------------------------------- 46 | 47 | nb_execution_raise_on_error = True 48 | -------------------------------------------------------------------------------- /tierkreis_visualization/tierkreis_visualization/app.py: -------------------------------------------------------------------------------- 1 | from sys import argv 2 | from tierkreis.controller.data.graph import GraphData 3 | from tierkreis_visualization.app_config import App, StorageType, lifespan 4 | from tierkreis_visualization.config import CONFIG 5 | from tierkreis_visualization.storage import ( 6 | file_storage_fn, 7 | from_graph_data_storage_fn, 8 | graph_data_storage_fn, 9 | ) 10 | from tierkreis_visualization.routers.frontend import assets 11 | from tierkreis_visualization.routers.workflows import router as workflows_router 12 | from tierkreis_visualization.routers.frontend import router as frontend_router 13 | 14 | 15 | def get_app(): 16 | app = App(lifespan=lifespan) 17 | app.include_router(workflows_router, prefix="/workflows") 18 | app.mount("/assets/", assets, name="frontend_assets") 19 | app.include_router(frontend_router) 20 | 21 | return app 22 | 23 | 24 | def get_filestorage_app(): 25 | app = get_app() 26 | app.state.get_storage_fn = file_storage_fn(CONFIG.tierkreis_path) 27 | app.state.storage_type = StorageType.FILESTORAGE 28 | return app 29 | 30 | 31 | def get_graph_data_app(): 32 | app = get_app() 33 | graph_specifier = argv[1] if len(argv) > 1 else CONFIG.graph_specifier 34 | 35 | if graph_specifier is None: 36 | return app 37 | 38 | app.state.get_storage_fn = graph_data_storage_fn(graph_specifier)[0] 39 | app.state.storage_type = StorageType.GRAPHDATA 40 | return app 41 | 42 | 43 | def app_from_graph_data(graph_data: GraphData): 44 | app = get_app() 45 | app.state.get_storage_fn = from_graph_data_storage_fn(graph_data) 46 | app.state.storage_type = StorageType.GRAPHDATA 47 | return app 48 | -------------------------------------------------------------------------------- /examples/simulate_parallel.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | import time 3 | from pytket.qasm.qasm import circuit_from_qasm 4 | from tierkreis.graphs.simulate.compile_simulate import compile_simulate 5 | from uuid import UUID 6 | 7 | from tierkreis.consts import PACKAGE_PATH 8 | from tierkreis.controller import run_graph 9 | from tierkreis.storage import FileStorage, read_outputs 10 | from tierkreis.executor import UvExecutor 11 | 12 | simulator_name = "aer" 13 | circuit = circuit_from_qasm(Path(__file__).parent / "data" / "ghz_state_n23.qasm") 14 | circuits = [circuit] * 3 15 | 16 | 17 | def main(): 18 | g = compile_simulate() 19 | storage = FileStorage(UUID(int=107), do_cleanup=True) 20 | executor = UvExecutor(PACKAGE_PATH / ".." / "tierkreis_workers", storage.logs_path) 21 | inputs = { 22 | "circuits": circuits, 23 | "n_shots": [3] * len(circuits), 24 | "simulator_name": simulator_name, 25 | "compilation_optimisation_level": 2, 26 | } 27 | 28 | print("Simulating using aer...") 29 | start = time.time() 30 | run_graph(storage, executor, g, inputs, polling_interval_seconds=0.1) 31 | print(f"time taken: {time.time() - start}") 32 | res = read_outputs(g, storage) 33 | assert isinstance(res, list) 34 | print(len(res)) 35 | 36 | inputs["simulator_name"] = "qulacs" 37 | 38 | print("Simulating using qulacs...") 39 | storage.clean_graph_files() 40 | start = time.time() 41 | run_graph(storage, executor, g, inputs, polling_interval_seconds=0.1) 42 | print(f"time taken: {time.time() - start}") 43 | res = read_outputs(g, storage) 44 | assert isinstance(res, list) 45 | print(len(res)) 46 | 47 | 48 | if __name__ == "__main__": 49 | main() 50 | -------------------------------------------------------------------------------- /tierkreis_visualization/frontend/src/components/ui/hover-card.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import * as HoverCardPrimitive from "@radix-ui/react-hover-card" 3 | 4 | import { cn } from "@/lib/utils" 5 | 6 | function HoverCard({ 7 | ...props 8 | }: React.ComponentProps) { 9 | return 10 | } 11 | 12 | function HoverCardTrigger({ 13 | ...props 14 | }: React.ComponentProps) { 15 | return ( 16 | 17 | ) 18 | } 19 | 20 | function HoverCardContent({ 21 | className, 22 | align = "center", 23 | sideOffset = 4, 24 | ...props 25 | }: React.ComponentProps) { 26 | return ( 27 | 28 | 38 | 39 | ) 40 | } 41 | 42 | export { HoverCard, HoverCardTrigger, HoverCardContent } 43 | -------------------------------------------------------------------------------- /tierkreis_visualization/frontend/src/error.tsx: -------------------------------------------------------------------------------- 1 | import { Link, useRouteError, isRouteErrorResponse } from "react-router"; 2 | import MyLogo from "./quantinuum_logo.svg"; // 3 | 4 | export default function ErrorPage() { 5 | const error = useRouteError(); 6 | let errorMessage: string; 7 | if (isRouteErrorResponse(error)) { 8 | errorMessage = error.statusText; 9 | } else if (error instanceof Error) { 10 | errorMessage = error.message; 11 | } else if (typeof error === "string") { 12 | errorMessage = error; 13 | } else { 14 | errorMessage = "An unknown error occurred"; 15 | } 16 | 17 | return ( 18 |
19 |
20 |

Oops! 😱

21 |

Sorry, an unexpected error has occurred.

22 |

23 | {errorMessage} 24 |

25 | Go back to the homepage 26 |
27 |
28 | Company Logo 29 |
30 |
31 | ); 32 | } 33 | 34 | // Basic inline styles for layout 35 | // In a real app, you'd likely use CSS classes 36 | const containerStyle: React.CSSProperties = { 37 | display: "flex", 38 | flexDirection: "column", 39 | alignItems: "center", 40 | justifyContent: "center", 41 | minHeight: "100vh", 42 | textAlign: "center", 43 | padding: "20px", 44 | boxSizing: "border-box", 45 | }; 46 | 47 | const contentStyle: React.CSSProperties = { 48 | flexGrow: 1, 49 | display: "flex", 50 | flexDirection: "column", 51 | justifyContent: "center", 52 | }; 53 | 54 | const footerStyle: React.CSSProperties = { 55 | marginTop: "auto", 56 | padding: "20px 0", 57 | }; 58 | -------------------------------------------------------------------------------- /tierkreis_workers/pytket_worker/test_main.py: -------------------------------------------------------------------------------- 1 | from pytket._tket.unit_id import Bit 2 | from pytket.backends.backendresult import BackendResult 3 | from pytket.utils.outcomearray import OutcomeArray 4 | from pytket._tket.circuit import Circuit 5 | from pytket.extensions.qiskit.backends.aer import AerBackend 6 | 7 | from .main import backend_result_from_dict, backend_result_to_dict 8 | 9 | 10 | def test_backend_result_conversion() -> None: 11 | original_result = BackendResult( 12 | shots=OutcomeArray.from_readouts([[0, 1, 0], [1, 0, 1], [1, 1, 1]]), 13 | c_bits=[Bit("a", 0), Bit("b", 0), Bit("b", 1)], 14 | ) 15 | result_dict = backend_result_to_dict(original_result) 16 | reconstructed_result = backend_result_from_dict(result_dict) 17 | assert ( 18 | original_result.get_shots().tolist() 19 | == reconstructed_result.get_shots().tolist() 20 | ) 21 | assert original_result.c_bits == reconstructed_result.c_bits 22 | assert result_dict == { 23 | "a": ["0", "1", "1"], 24 | "b": ["10", "01", "11"], 25 | } 26 | 27 | 28 | def _deterministic_circuit() -> Circuit: 29 | circuit = Circuit(3) 30 | circuit.add_c_register("a", 1) 31 | circuit.add_c_register("b", 2) 32 | circuit.X(0) 33 | circuit.X(1) 34 | circuit.Measure(0, Bit("a", 0)) 35 | circuit.Measure(1, Bit("b", 0)) 36 | circuit.Measure(2, Bit("b", 1)) 37 | return circuit 38 | 39 | 40 | def test_deterministic_circuit() -> None: 41 | circuit = _deterministic_circuit() 42 | result = AerBackend().run_circuit(circuit, 10) 43 | result_dict = backend_result_to_dict(result) 44 | assert result_dict == {"a": ["1"] * 10, "b": ["10"] * 10} 45 | assert result == backend_result_from_dict(result_dict) 46 | -------------------------------------------------------------------------------- /examples/hello_world_graph.py: -------------------------------------------------------------------------------- 1 | # /// script 2 | # requires-python = ">=3.12" 3 | # dependencies = ["tierkreis"] 4 | # 5 | # [tool.uv.sources] 6 | # tierkreis = { path = "../tierkreis", editable = true } 7 | # /// 8 | from pathlib import Path 9 | 10 | from tierkreis.builder import GraphBuilder 11 | from tierkreis.controller.data.location import Loc 12 | from tierkreis.cli.run_workflow import run_workflow 13 | from tierkreis.controller.data.models import TKR 14 | 15 | from example_workers.hello_world_worker.stubs import greet 16 | 17 | root_loc = Loc() 18 | 19 | 20 | def hello_graph() -> GraphBuilder: 21 | """A graph that greets the subject.""" 22 | 23 | # We build a graph that takes a single string as input 24 | # and produces a single string as output. 25 | g = GraphBuilder(TKR[str], TKR[str]) 26 | 27 | # We add a constant that yields the string "hello ". 28 | hello = g.const("hello ") 29 | 30 | # We import and call the "greet" task from our worker. 31 | output = g.task(greet(greeting=hello, subject=g.inputs)) 32 | 33 | # We assign the output of the greet task to the output of the whole graph. 34 | g.outputs(output) 35 | 36 | return g 37 | 38 | 39 | def main() -> None: 40 | """Configure our workflow execution and run it to completion.""" 41 | run_workflow( 42 | hello_graph().data, 43 | {"value": "world!"}, 44 | name="hello_world", 45 | run_id=100, # Assign a fixed uuid for our workflow. 46 | registry_path=Path(__file__).parent 47 | / "example_workers", # Look for workers in the `example_workers` directory. 48 | use_uv_worker=True, 49 | polling_interval_seconds=0.1, 50 | print_output=True, 51 | ) 52 | 53 | 54 | if __name__ == "__main__": 55 | main() 56 | -------------------------------------------------------------------------------- /tierkreis_visualization/tierkreis_visualization/static/_types.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @typedef {Object} PyNode 3 | * @property {number | string} id - The node ID. 4 | * @property {"Not started" | "Started" | "Error" | "Finished"} status - The node name. 5 | * @property {string} function_name - The node label.. 6 | */ 7 | 8 | /** 9 | * @typedef {Object} PyEdge 10 | * @property {number | string} from_node - The edge name. 11 | * @property {string} from_port - The edge arrow 12 | * @property {number | string} to_node - The edge name. 13 | * @property {string} to_port - The edge label. 14 | * @property {boolean} conditional - The edge doesn't always run. 15 | */ 16 | 17 | /** 18 | * @typedef {Object} JSNode 19 | * @property {number | string} id - The node ID. 20 | * @property {string} title - The node name. 21 | * @property {string} label - The node label.. 22 | * @property {string} shape - Node shape. 23 | * @property {string} color - Node colour. 24 | * @property {"Not started" | "Started" | "Error" | "Finished"} status - Node status 25 | */ 26 | 27 | /** 28 | * @typedef {Object} JSEdge 29 | * @property {string} id - The edge ID. 30 | * @property {number | string} from - The edge name. 31 | * @property {number | string} to - The edge label.. 32 | * @property {string} title - The edge name. 33 | * @property {string} label - Node shape. 34 | * @property {string} arrows - The edge arrow. 35 | * @property {boolean} dashes - If the edge should be dashed. 36 | */ 37 | 38 | /** 39 | * @typedef {Object} JSGraph 40 | * @property {JSNode[]} nodes - The edge ID. 41 | * @property {JSEdge[]} edges - The edge name. 42 | */ 43 | 44 | /** 45 | * @typedef {Object} PyGraph 46 | * @property {PyNode[]} nodes - The edge ID. 47 | * @property {PyEdge[]} edges - The edge name. 48 | */ 49 | -------------------------------------------------------------------------------- /docs/source/tutorial/auth_stubs.py: -------------------------------------------------------------------------------- 1 | """Code generated from auth_worker namespace. Please do not edit.""" 2 | 3 | from typing import NamedTuple 4 | from tierkreis.controller.data.models import TKR 5 | 6 | 7 | class EncryptionResult(NamedTuple): 8 | ciphertext: TKR[str] # noqa: F821 # fmt: skip 9 | time_taken: TKR[float] # noqa: F821 # fmt: skip 10 | 11 | 12 | class SigningResult(NamedTuple): 13 | hex_signature: TKR[str] # noqa: F821 # fmt: skip 14 | time_taken: TKR[float] # noqa: F821 # fmt: skip 15 | 16 | 17 | class encrypt(NamedTuple): 18 | plaintext: TKR[str] # noqa: F821 # fmt: skip 19 | work_factor: TKR[int] # noqa: F821 # fmt: skip 20 | 21 | @staticmethod 22 | def out() -> type[EncryptionResult]: # noqa: F821 # fmt: skip 23 | return EncryptionResult # noqa: F821 # fmt: skip 24 | 25 | @property 26 | def namespace(self) -> str: 27 | return "auth_worker" 28 | 29 | 30 | class sign(NamedTuple): 31 | private_key: TKR[bytes] # noqa: F821 # fmt: skip 32 | passphrase: TKR[bytes] # noqa: F821 # fmt: skip 33 | message: TKR[str] # noqa: F821 # fmt: skip 34 | 35 | @staticmethod 36 | def out() -> type[SigningResult]: # noqa: F821 # fmt: skip 37 | return SigningResult # noqa: F821 # fmt: skip 38 | 39 | @property 40 | def namespace(self) -> str: 41 | return "auth_worker" 42 | 43 | 44 | class verify(NamedTuple): 45 | public_key: TKR[bytes] # noqa: F821 # fmt: skip 46 | signature: TKR[str] # noqa: F821 # fmt: skip 47 | message: TKR[str] # noqa: F821 # fmt: skip 48 | 49 | @staticmethod 50 | def out() -> type[TKR[bool]]: # noqa: F821 # fmt: skip 51 | return TKR[bool] # noqa: F821 # fmt: skip 52 | 53 | @property 54 | def namespace(self) -> str: 55 | return "auth_worker" 56 | -------------------------------------------------------------------------------- /examples/example_workers/auth_worker/stubs.py: -------------------------------------------------------------------------------- 1 | """Code generated from auth_worker namespace. Please do not edit.""" 2 | 3 | from typing import NamedTuple 4 | from tierkreis.controller.data.models import TKR 5 | 6 | 7 | class EncryptionResult(NamedTuple): 8 | ciphertext: TKR[str] # noqa: F821 # fmt: skip 9 | time_taken: TKR[float] # noqa: F821 # fmt: skip 10 | 11 | 12 | class SigningResult(NamedTuple): 13 | hex_signature: TKR[str] # noqa: F821 # fmt: skip 14 | time_taken: TKR[float] # noqa: F821 # fmt: skip 15 | 16 | 17 | class encrypt(NamedTuple): 18 | plaintext: TKR[str] # noqa: F821 # fmt: skip 19 | work_factor: TKR[int] # noqa: F821 # fmt: skip 20 | 21 | @staticmethod 22 | def out() -> type[EncryptionResult]: # noqa: F821 # fmt: skip 23 | return EncryptionResult # noqa: F821 # fmt: skip 24 | 25 | @property 26 | def namespace(self) -> str: 27 | return "auth_worker" 28 | 29 | 30 | class sign(NamedTuple): 31 | private_key: TKR[bytes] # noqa: F821 # fmt: skip 32 | passphrase: TKR[bytes] # noqa: F821 # fmt: skip 33 | message: TKR[str] # noqa: F821 # fmt: skip 34 | 35 | @staticmethod 36 | def out() -> type[SigningResult]: # noqa: F821 # fmt: skip 37 | return SigningResult # noqa: F821 # fmt: skip 38 | 39 | @property 40 | def namespace(self) -> str: 41 | return "auth_worker" 42 | 43 | 44 | class verify(NamedTuple): 45 | public_key: TKR[bytes] # noqa: F821 # fmt: skip 46 | signature: TKR[str] # noqa: F821 # fmt: skip 47 | message: TKR[str] # noqa: F821 # fmt: skip 48 | 49 | @staticmethod 50 | def out() -> type[TKR[bool]]: # noqa: F821 # fmt: skip 51 | return TKR[bool] # noqa: F821 # fmt: skip 52 | 53 | @property 54 | def namespace(self) -> str: 55 | return "auth_worker" 56 | -------------------------------------------------------------------------------- /examples/error_handling_graph.py: -------------------------------------------------------------------------------- 1 | # /// script 2 | # requires-python = ">=3.12" 3 | # dependencies = ["tierkreis"] 4 | # 5 | # [tool.uv.sources] 6 | # tierkreis = { path = "../tierkreis", editable = true } 7 | # /// 8 | from pathlib import Path 9 | from uuid import UUID 10 | 11 | from tierkreis.builder import GraphBuilder 12 | from tierkreis.controller import run_graph 13 | from tierkreis.controller.data.core import EmptyModel 14 | from tierkreis.controller.data.location import Loc 15 | from tierkreis.controller.data.models import TKR 16 | from tierkreis.controller.storage.filestorage import ControllerFileStorage 17 | from tierkreis.controller.executor.uv_executor import UvExecutor 18 | 19 | from example_workers.error_worker.stubs import fail 20 | 21 | root_loc = Loc() 22 | 23 | 24 | def error_graph() -> GraphBuilder: 25 | """A graph that errors.""" 26 | 27 | g = GraphBuilder(EmptyModel, TKR[str]) 28 | output = g.task(fail()) 29 | g.outputs(output) 30 | return g 31 | 32 | 33 | def main() -> None: 34 | """Configure our workflow execution and run it to completion.""" 35 | # Assign a fixed uuid for our workflow. 36 | workflow_id = UUID(int=103) 37 | storage = ControllerFileStorage(workflow_id, name="error_handling", do_cleanup=True) 38 | 39 | # Look for workers in the `example_workers` directory. 40 | registry_path = Path(__file__).parent / "example_workers" 41 | executor = UvExecutor(registry_path=registry_path, logs_path=storage.logs_path) 42 | print("Starting workflow at location:", storage.logs_path) 43 | run_graph( 44 | storage, 45 | executor, 46 | error_graph().data, 47 | {"value": "world!"}, 48 | polling_interval_seconds=0.1, 49 | ) 50 | output = storage.read_errors(root_loc) 51 | print(output) 52 | 53 | 54 | if __name__ == "__main__": 55 | main() 56 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: Python package CI 2 | 3 | on: 4 | pull_request: 5 | branches: 6 | - main 7 | push: 8 | branches: 9 | - main 10 | env: 11 | UV_VERSION: "0.6.6" 12 | 13 | jobs: 14 | 15 | lint: 16 | name: Lint 17 | runs-on: ubuntu-24.04 18 | strategy: 19 | matrix: 20 | python-version: ['3.12'] 21 | steps: 22 | - name: Checkout code 23 | uses: actions/checkout@v4 24 | with: 25 | fetch-depth: '0' 26 | 27 | - name: Set up uv & venv 28 | uses: astral-sh/setup-uv@v5 29 | with: 30 | version: ${{ env.UV_VERSION }} 31 | enable-cache: true 32 | python-version: ${{ matrix.python-version }} 33 | cache-dependency-glob: uv.lock 34 | 35 | - name: Setup just 36 | uses: extractions/setup-just@v3 37 | 38 | - name: Lint project 39 | run: | 40 | just lint 41 | 42 | test: 43 | name: Test 44 | runs-on: ubuntu-24.04 45 | strategy: 46 | matrix: 47 | python-version: ['3.12'] 48 | 49 | steps: 50 | - name: Checkout code 51 | uses: actions/checkout@v4 52 | with: 53 | fetch-depth: '0' 54 | 55 | - name: Set up uv & venv 56 | uses: astral-sh/setup-uv@v5 57 | with: 58 | version: ${{ env.UV_VERSION }} 59 | enable-cache: true 60 | python-version: ${{ matrix.python-version }} 61 | cache-dependency-glob: uv.lock 62 | 63 | - name: Setup just 64 | uses: extractions/setup-just@v3 65 | 66 | - name: Lint project 67 | run: | 68 | just test 69 | 70 | - name: Upload pytest test results 71 | uses: actions/upload-artifact@master 72 | with: 73 | name: pytest-results-${{ matrix.python-version }} 74 | path: htmlcov/ 75 | -------------------------------------------------------------------------------- /examples/example_workers/scipy_worker/main.py: -------------------------------------------------------------------------------- 1 | import io 2 | import os 3 | import pickle 4 | from sys import argv 5 | from typing import Annotated, NamedTuple 6 | import numpy as np 7 | 8 | from tierkreis.controller.data.core import Deserializer, Serializer 9 | from tierkreis.worker.worker import Worker 10 | 11 | worker = Worker("scipy_worker") 12 | 13 | 14 | def save(a: np.ndarray) -> bytes: 15 | with io.BytesIO() as bs: 16 | np.save(bs, a) 17 | return bs.getvalue() 18 | 19 | 20 | def load(bs: bytes) -> np.ndarray: 21 | with io.BytesIO() as bi: 22 | bi.write(bs) 23 | bi.seek(0) 24 | return np.load(bi, encoding="bytes") 25 | 26 | 27 | SER_METHOD = os.environ.get("SER_METHOD") 28 | if SER_METHOD == "dumps": 29 | ser = Serializer(np.ndarray.dumps) 30 | deser = Deserializer(pickle.loads) 31 | elif SER_METHOD == "tolist": 32 | ser = Serializer(np.ndarray.tolist, "json") 33 | deser = Deserializer(np.array, "json") 34 | elif SER_METHOD == "save": 35 | ser = Serializer(save) 36 | deser = Deserializer(load) 37 | else: 38 | ser = None 39 | deser = None 40 | 41 | NDArray = Annotated[np.ndarray, ser, deser] 42 | 43 | 44 | class PointedArray(NamedTuple): 45 | a: NDArray 46 | p: int 47 | 48 | 49 | @worker.task() 50 | def add_point(a: NDArray, p: int) -> PointedArray: 51 | return PointedArray(a, p) 52 | 53 | 54 | @worker.task() 55 | def eval_point(pa: PointedArray) -> float: 56 | return pa.a.item(pa.p) 57 | 58 | 59 | @worker.task() 60 | def linspace(start: float, stop: float, num: int = 50) -> NDArray: 61 | return np.linspace(start, stop, num=num) 62 | 63 | 64 | @worker.task() 65 | def transpose(a: NDArray) -> NDArray: 66 | return a.transpose() 67 | 68 | 69 | @worker.task() 70 | def reshape(a: NDArray, shape: int | list[int]) -> NDArray: 71 | return np.reshape(a, shape) 72 | 73 | 74 | if __name__ == "__main__": 75 | worker.app(argv) 76 | -------------------------------------------------------------------------------- /tierkreis_visualization/tierkreis_visualization/storage.py: -------------------------------------------------------------------------------- 1 | from importlib.util import module_from_spec, spec_from_file_location 2 | from pathlib import Path 3 | from sys import argv 4 | import sys 5 | from typing import Callable 6 | from uuid import UUID 7 | 8 | from tierkreis.controller.data.graph import GraphData 9 | from tierkreis.controller.storage.filestorage import ControllerFileStorage 10 | from tierkreis.controller.storage.graphdata import GraphDataStorage 11 | from tierkreis.controller.storage.protocol import ControllerStorage 12 | 13 | 14 | def file_storage_fn(tkr_dir: Path) -> Callable[[UUID], ControllerStorage]: 15 | def inner(workflow_id: UUID): 16 | return ControllerFileStorage( 17 | workflow_id=workflow_id, tierkreis_directory=tkr_dir 18 | ) 19 | 20 | return inner 21 | 22 | 23 | def graph_data_storage_fn( 24 | graph_specifier: str, 25 | ) -> tuple[Callable[[UUID], ControllerStorage], Path]: 26 | graph_specifier = argv[1] 27 | mod_path, var = graph_specifier.rsplit(":", 1) 28 | spec = spec_from_file_location("tkr_tmp.graph", mod_path) 29 | 30 | if spec is None: 31 | raise ValueError(f"File is not a Python module: {mod_path}") 32 | 33 | module = module_from_spec(spec) 34 | sys.modules["tkr_tmp.graph"] = module 35 | loader = spec.loader 36 | 37 | if loader is None: 38 | raise ValueError("Could not get loader from module.") 39 | 40 | loader.exec_module(module) 41 | graph = getattr(module, var).data 42 | 43 | def inner(workflow_id: UUID) -> ControllerStorage: 44 | return GraphDataStorage(UUID(int=0), graph=graph) 45 | 46 | return inner, Path(mod_path) 47 | 48 | 49 | def from_graph_data_storage_fn( 50 | graph_data: GraphData, 51 | ) -> Callable[[UUID], ControllerStorage]: 52 | def inner(workflow_id: UUID) -> ControllerStorage: 53 | return GraphDataStorage(UUID(int=0), graph=graph_data) 54 | 55 | return inner 56 | -------------------------------------------------------------------------------- /tierkreis/tests/idl/test_idl.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | import pytest 3 | from tierkreis.exceptions import TierkreisError 4 | from tierkreis.idl.models import GenericType 5 | from tierkreis.namespace import Namespace 6 | from tierkreis.idl.type_symbols import type_symbol 7 | import tests.idl.namespace1 8 | 9 | type_symbols = [ 10 | ("uint8", GenericType(int, [])), 11 | ("string", GenericType(str, [])), 12 | ("Array", GenericType(list, [GenericType(int, [])])), 13 | ( 14 | "Record>", 15 | GenericType( 16 | dict, [GenericType(str, []), GenericType(list, [GenericType(str, [])])] 17 | ), 18 | ), 19 | ( 20 | "Record>>", 21 | GenericType( 22 | dict, 23 | [ 24 | GenericType(str, []), 25 | GenericType(list, [GenericType("C", ["T", "A"])]), 26 | ], 27 | ), 28 | ), 29 | ] 30 | type_symbols_for_failure = ["decimal", "unknown", "duration"] 31 | dir = Path(__file__).parent 32 | typespecs = [(dir / "namespace1.tsp", tests.idl.namespace1.expected_namespace)] 33 | 34 | 35 | @pytest.mark.parametrize("type_symb,expected", type_symbols) 36 | def test_type_t(type_symb: str, expected: type): 37 | assert (expected, "") == type_symbol(type_symb) 38 | 39 | 40 | @pytest.mark.parametrize("path,expected", typespecs) 41 | def test_namespace(path: Path, expected: Namespace): 42 | namespace = Namespace.from_spec_file(path) 43 | assert namespace.stubs() == expected.stubs() 44 | 45 | # Write stubs to file. 46 | # This file will be subject to linting. 47 | # Also a change in this file can indicate an unexpectedly breaking change. 48 | namespace.write_stubs(Path(__file__).parent / "stubs_output.py") 49 | 50 | 51 | @pytest.mark.parametrize("type_symb", type_symbols_for_failure) 52 | def test_parser_fail(type_symb: str): 53 | with pytest.raises(TierkreisError): 54 | type_symbol(type_symb) 55 | -------------------------------------------------------------------------------- /tierkreis_visualization/tierkreis_visualization/templates/function.jinja: -------------------------------------------------------------------------------- 1 | {% from "macros/breadcrumbs.html" import breadcrumbs_macro with context %} 2 | {% from "macros/common_head.html" import common_head_macro with context %} 3 | {% from "macros/header.html" import header_macro with context %} 4 | 5 | 6 | Tierkreis visualization: node 7 | {{ common_head_macro() }} 8 | 9 | 10 | {{ header_macro() }} 11 |
12 | {{ breadcrumbs_macro(breadcrumbs) }} 13 | 14 | 20 |

{{ definition.function_name }}

21 | 22 | {% if data.has_error %} 23 |

Error

24 |
{{ data.error_message}}
25 |
26 | {% endif %} 27 |
    28 |
  • 29 | Inputs 30 |
      31 | {% for input,path in definition.inputs.items() %} 32 |
    • 33 | {{ input }} 36 | ({{ path }}) 37 |
    • 38 | {% endfor %} 39 |
    40 |
  • 41 | 42 |
  • 43 | Outputs 44 |
      45 | {% for output,path in definition.outputs.items() %} 46 |
    • 47 | {{ output }} 50 | ({{ path }}) 51 |
    • 52 | {% endfor %} 53 |
    54 |
  • 55 |
  • logs
  • 56 |
57 |
58 | 59 | 60 | -------------------------------------------------------------------------------- /infra/slurm_local/docker-entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | if [ "$1" = "slurmdbd" ] 5 | then 6 | echo "---> Starting the MUNGE Authentication service (munged) ..." 7 | gosu munge /usr/sbin/munged 8 | 9 | echo "---> Starting the Slurm Database Daemon (slurmdbd) ..." 10 | 11 | { 12 | . /etc/slurm/slurmdbd.conf 13 | until echo "SELECT 1" | mysql -h $StorageHost -u$StorageUser -p$StoragePass 2>&1 > /dev/null 14 | do 15 | echo "-- Waiting for database to become active ..." 16 | sleep 2 17 | done 18 | } 19 | echo "-- Database is now active ..." 20 | 21 | exec gosu slurm /usr/sbin/slurmdbd -Dvvv 22 | fi 23 | 24 | if [ "$1" = "slurmctld" ] 25 | then 26 | echo "---> Starting the MUNGE Authentication service (munged) ..." 27 | gosu munge /usr/sbin/munged 28 | 29 | echo "---> Waiting for slurmdbd to become active before starting slurmctld ..." 30 | 31 | until 2>/dev/null >/dev/tcp/slurmdbd/6819 32 | do 33 | echo "-- slurmdbd is not available. Sleeping ..." 34 | sleep 2 35 | done 36 | echo "-- slurmdbd is now active ..." 37 | 38 | echo "---> Starting the Slurm Controller Daemon (slurmctld) ..." 39 | if /usr/sbin/slurmctld -V | grep -q '17.02' ; then 40 | exec gosu slurm /usr/sbin/slurmctld -Dvvv 41 | else 42 | exec gosu slurm /usr/sbin/slurmctld -i -Dvvv 43 | fi 44 | fi 45 | 46 | if [ "$1" = "slurmd" ] 47 | then 48 | echo "---> Starting the MUNGE Authentication service (munged) ..." 49 | gosu munge /usr/sbin/munged 50 | 51 | echo "---> Waiting for slurmctld to become active before starting slurmd..." 52 | 53 | until 2>/dev/null >/dev/tcp/slurmctld/6817 54 | do 55 | echo "-- slurmctld is not available. Sleeping ..." 56 | sleep 2 57 | done 58 | echo "-- slurmctld is now active ..." 59 | 60 | echo "---> Starting the Slurm Node Daemon (slurmd) ..." 61 | exec /usr/sbin/slurmd -Dvvv 62 | fi 63 | 64 | exec "$@" 65 | -------------------------------------------------------------------------------- /tierkreis_workers/qulacs_worker/main.py: -------------------------------------------------------------------------------- 1 | from sys import argv 2 | from typing import Any 3 | 4 | from tierkreis import Worker 5 | from pytket._tket.circuit import Circuit 6 | from pytket.backends.backend import Backend 7 | from pytket.backends.backendresult import BackendResult 8 | from pytket.extensions.qulacs.backends.qulacs_backend import QulacsBackend 9 | 10 | worker = Worker("qulacs_worker") 11 | 12 | 13 | def get_backend(result_type: str = "state_vector", gpu_sim: bool = False) -> Backend: 14 | if gpu_sim: 15 | from pytket.extensions.qulacs.backends.qulacs_backend import QulacsGPUBackend 16 | 17 | return QulacsGPUBackend() 18 | else: 19 | return QulacsBackend(result_type) 20 | 21 | 22 | def get_config(seed: int | None = None) -> dict[str, Any]: 23 | return {} if seed is None else {"seed": seed} 24 | 25 | 26 | @worker.task() 27 | def get_compiled_circuit( 28 | circuit: Circuit, 29 | optimisation_level: int = 2, 30 | result_type: str = "state_vector", 31 | gpu_sim: bool = False, 32 | ) -> Circuit: 33 | backend = get_backend(result_type, gpu_sim) 34 | return backend.get_compiled_circuit(circuit, optimisation_level) 35 | 36 | 37 | @worker.task() 38 | def run_circuit( 39 | circuit: Circuit, 40 | n_shots: int, 41 | result_type: str = "state_vector", 42 | gpu_sim: bool = False, 43 | seed: int | None = None, 44 | ) -> BackendResult: 45 | backend = get_backend(result_type, gpu_sim) 46 | config = get_config(seed) 47 | return backend.run_circuit(circuit, n_shots, **config) 48 | 49 | 50 | @worker.task() 51 | def run_circuits( 52 | circuits: list[Circuit], 53 | n_shots: list[int], 54 | result_type: str = "state_vector", 55 | gpu_sim: bool = False, 56 | seed: int | None = None, 57 | ) -> list[BackendResult]: 58 | backend = get_backend(result_type, gpu_sim) 59 | config = get_config(seed) 60 | return backend.run_circuits(circuits, n_shots, **config) 61 | 62 | 63 | if __name__ == "__main__": 64 | worker.app(argv) 65 | -------------------------------------------------------------------------------- /tierkreis/tests/cli/test_run_workflow.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | import json 3 | import logging 4 | from pathlib import Path 5 | from uuid import UUID 6 | from unittest import mock 7 | 8 | 9 | from tierkreis.controller.data.graph import GraphData 10 | from tierkreis.cli.run_workflow import run_workflow 11 | from tests.controller.sample_graphdata import simple_eval 12 | from tierkreis.controller.data.types import ptype_from_bytes 13 | 14 | logger = logging.getLogger(__name__) 15 | 16 | 17 | @pytest.fixture() 18 | def graph() -> GraphData: 19 | return simple_eval() 20 | 21 | 22 | def test_run_workflow(graph: GraphData) -> None: 23 | inputs = {} 24 | run_workflow(inputs=inputs, graph=graph, run_id=31415) 25 | with open( 26 | Path.home() 27 | / ".tierkreis" 28 | / "checkpoints" 29 | / str(UUID(int=31415)) 30 | / "-/outputs/simple_eval_output", 31 | "rb", 32 | ) as fh: 33 | c = ptype_from_bytes(fh.read()) 34 | 35 | assert c == 12 36 | 37 | 38 | def test_run_workflow_with_output(graph: GraphData, capfd) -> None: 39 | inputs = {} 40 | run_workflow(inputs=inputs, graph=graph, run_id=31415, print_output=True) 41 | out, _ = capfd.readouterr() 42 | assert "simple_eval_output: b'12'\n" in out 43 | 44 | 45 | @mock.patch("uuid.uuid4", return_value=UUID(int=31415)) 46 | def test_run_workflow_default_run_id(_, graph: GraphData) -> None: 47 | inputs = {} 48 | run_workflow(inputs=inputs, graph=graph) 49 | with open( 50 | Path.home() 51 | / ".tierkreis" 52 | / "checkpoints" 53 | / str(UUID(int=31415)) 54 | / "-/outputs/simple_eval_output", 55 | "rb", 56 | ) as fh: 57 | c = json.loads(fh.read()) 58 | assert c == 12 59 | 60 | 61 | def test_run_workflow_uv_executor(graph: GraphData) -> None: 62 | inputs = {} 63 | run_workflow( 64 | inputs=inputs, 65 | graph=graph, 66 | run_id=31415, 67 | use_uv_worker=True, 68 | registry_path=Path("."), 69 | ) 70 | -------------------------------------------------------------------------------- /tierkreis_visualization/frontend/README.md: -------------------------------------------------------------------------------- 1 | ![](https://github.com/xyflow/web/blob/main/assets/codesandbox-header-ts.png?raw=true) 2 | 3 | # React Flow starter (Vite + TS) 4 | 5 | We've put together this template to serve as a starting point for folks 6 | interested in React Flow. You can use this both as a base for your own React 7 | Flow applications, or for small experiments or bug reports. 8 | 9 | **TypeScript not your thing?** We also have a vanilla JavaScript starter template, 10 | just for you! 11 | 12 | ## Getting up and running 13 | 14 | You can get this template without forking/cloning the repo using `degit`: 15 | 16 | ```bash 17 | npx degit xyflow/vite-react-flow-template your-app-name 18 | ``` 19 | 20 | The template contains mostly the minimum dependencies to get up and running, but 21 | also includes eslint and some additional rules to help you write React code that 22 | is less likely to run into issues: 23 | 24 | ```bash 25 | npm install # or `pnpm install` or `yarn install` 26 | ``` 27 | 28 | Vite is a great development server and build tool that we recommend our users to 29 | use. You can start a development server with: 30 | 31 | ```bash 32 | npm run dev 33 | ``` 34 | 35 | While the development server is running, changes you make to the code will be 36 | automatically reflected in the browser! 37 | 38 | ## Things to try: 39 | 40 | - Create a new custom node inside `src/nodes/` (don't forget to export it from `src/nodes/index.ts`). 41 | - Change how things look by [overriding some of the built-in classes](https://reactflow.dev/learn/customization/theming#overriding-built-in-classes). 42 | - Add a layouting library to [position your nodes automatically](https://reactflow.dev/learn/layouting/layouting) 43 | 44 | ## Resources 45 | 46 | Links: 47 | 48 | - [React Flow - Docs](https://reactflow.dev) 49 | - [React Flow - Discord](https://discord.com/invite/Bqt6xrs) 50 | 51 | Learn: 52 | 53 | - [React Flow – Custom Nodes](https://reactflow.dev/learn/customization/custom-nodes) 54 | - [React Flow – Layouting](https://reactflow.dev/learn/layouting/layouting) 55 | -------------------------------------------------------------------------------- /tierkreis/tests/idl/stubs_output.py: -------------------------------------------------------------------------------- 1 | """Code generated from TestNamespace namespace. Please do not edit.""" 2 | 3 | from typing import NamedTuple, Protocol 4 | from tierkreis.controller.data.models import TKR 5 | from tierkreis.controller.data.types import PType, Struct 6 | 7 | 8 | class A(NamedTuple): 9 | age: TKR[int] # noqa: F821 # fmt: skip 10 | name: TKR[dict[str, str]] # noqa: F821 # fmt: skip 11 | 12 | 13 | class B(Struct, Protocol): 14 | age: int # noqa: F821 # fmt: skip 15 | name: dict[str, str] # noqa: F821 # fmt: skip 16 | 17 | 18 | class C[T: PType](Struct, Protocol): 19 | a: list[int] # noqa: F821 # fmt: skip 20 | b: "B" # noqa: F821 # fmt: skip 21 | included: "IncludedType" # noqa: F821 # fmt: skip 22 | ol: "list[ListItem]" # noqa: F821 # fmt: skip 23 | t: "T" # noqa: F821 # fmt: skip 24 | 25 | 26 | class IncludedType(Struct, Protocol): 27 | nested: "NestedType" # noqa: F821 # fmt: skip 28 | 29 | 30 | class ListItem(Struct, Protocol): 31 | i: int # noqa: F821 # fmt: skip 32 | 33 | 34 | class NestedType(Struct, Protocol): 35 | city: str # noqa: F821 # fmt: skip 36 | 37 | 38 | class foo(NamedTuple): 39 | a: TKR[int] # noqa: F821 # fmt: skip 40 | b: TKR[str] # noqa: F821 # fmt: skip 41 | 42 | @staticmethod 43 | def out() -> type[A]: # noqa: F821 # fmt: skip 44 | return A # noqa: F821 # fmt: skip 45 | 46 | @property 47 | def namespace(self) -> str: 48 | return "TestNamespace" 49 | 50 | 51 | class bar(NamedTuple): 52 | @staticmethod 53 | def out() -> type[TKR[B]]: # noqa: F821 # fmt: skip 54 | return TKR[B] # noqa: F821 # fmt: skip 55 | 56 | @property 57 | def namespace(self) -> str: 58 | return "TestNamespace" 59 | 60 | 61 | class z[T: PType](NamedTuple): 62 | c: TKR[C[T]] # noqa: F821 # fmt: skip 63 | 64 | @staticmethod 65 | def out() -> type[TKR[C[T]]]: # noqa: F821 # fmt: skip 66 | return TKR[C[T]] # noqa: F821 # fmt: skip 67 | 68 | @property 69 | def namespace(self) -> str: 70 | return "TestNamespace" 71 | -------------------------------------------------------------------------------- /tierkreis/tierkreis/controller/executor/in_memory_executor.py: -------------------------------------------------------------------------------- 1 | import json 2 | import logging 3 | import importlib.util 4 | from pathlib import Path 5 | 6 | from tierkreis.controller.data.location import WorkerCallArgs 7 | from tierkreis.controller.storage.in_memory import ControllerInMemoryStorage 8 | from tierkreis.worker.storage.in_memory import InMemoryWorkerStorage 9 | from tierkreis.exceptions import TierkreisError 10 | 11 | 12 | logger = logging.getLogger(__name__) 13 | 14 | 15 | class InMemoryExecutor: 16 | """Executes workers in the same process as the controller. 17 | 18 | Implements: :py:class:`tierkreis.controller.executor.protocol.ControllerExecutor` 19 | """ 20 | 21 | def __init__(self, registry_path: Path, storage: ControllerInMemoryStorage) -> None: 22 | self.registry_path = registry_path 23 | self.storage = storage 24 | 25 | def run( 26 | self, 27 | launcher_name: str, 28 | worker_call_args_path: Path, 29 | ) -> None: 30 | logging.basicConfig( 31 | format="%(asctime)s: %(message)s", 32 | datefmt="%Y-%m-%dT%H:%M:%S%z", 33 | filemode="a", 34 | level=logging.INFO, 35 | ) 36 | logger.info("START %s %s", launcher_name, worker_call_args_path) 37 | call_args = WorkerCallArgs( 38 | **json.loads(self.storage.read(worker_call_args_path)) 39 | ) 40 | 41 | spec = importlib.util.spec_from_file_location( 42 | "in_memory", self.registry_path / launcher_name / "main.py" 43 | ) 44 | if spec is None or spec.loader is None: 45 | raise TierkreisError( 46 | f"Couldn't load main.py in {self.registry_path / launcher_name}" 47 | ) 48 | module = importlib.util.module_from_spec(spec) 49 | spec.loader.exec_module(module) 50 | worker_storage = InMemoryWorkerStorage(self.storage) 51 | module.worker.storage = worker_storage 52 | module.worker.functions[call_args.function_name](call_args) 53 | self.storage.touch(call_args.done_path) 54 | -------------------------------------------------------------------------------- /tierkreis_visualization/tierkreis_visualization/templates/fallback.jinja: -------------------------------------------------------------------------------- 1 | {% from "macros/breadcrumbs.html" import breadcrumbs_macro with context %} 2 | {% from "macros/common_head.html" import common_head_macro with context %} 3 | {% from "macros/header.html" import header_macro with context %} 4 | 5 | 6 | Tierkreis visualization: node 7 | {{ common_head_macro() }} 8 | 9 | 10 | {{ header_macro() }} 11 |
12 | {{ breadcrumbs_macro(breadcrumbs) }} 13 | 14 | 23 |

{{ node.type }}

24 | 25 |
    26 |
  • 27 | Inputs 32 | 33 |
      34 | {% for k, (loc, port) in inputs.items() %} 35 |
    • 36 | {{ k }} 37 | ({{ loc }}, {{ port }}) 38 |
    • 39 | {% endfor %} 40 |
    41 |
  • 42 | 43 |
  • 44 | Outputs 49 |
      50 | {% for k, (loc, port) in outputs.items() %} 51 |
    • 52 | {{ k }} 53 | ({{ loc }}, {{ port }}) 54 |
    • 55 | {% endfor %} 56 |
    57 |
  • 58 |
59 |
60 | 61 | 62 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # tierkreis 2 | 3 | Quantum-classical hybrid workflow orchestration tool. 4 | This is the top level repository containing the python packages. 5 | 6 | ## Packages 7 | - **Tierkreis**: the core functionality `pip install tierkreis` 8 | - **Tierkreis Visualization**: A simple web visualization tool for debugging workflows `pip install tierkreis-visualization` 9 | - **Tierkreis Workers**: A collection of commonly used workers `pip install tkr--worker` 10 | - Qiskit Aer 11 | - Quantinuum Nexus 12 | - Quantinuum PyTKet 13 | 14 | 15 | ## CLI 16 | 17 | Tierkreis comes with a command line interface for running workflows. 18 | To see all available options use `tkr --help`. 19 | To run the hello world example from the cli 20 | ``` 21 | uv run tkr -g examples/hello_world/hello_world_graph.py:hello_graph -i data.json --uv --registry-path examples/hello_world/ -o 22 | ``` 23 | Explanation: 24 | * `-g` specifies the graph to run by specifying the location and function to run. 25 | * `-i` specifies the input for the graph function. In this case it loads a json file from the project root with the contents `{"value": "world!"}` 26 | * `--uv` enables the use of the UV executor. 27 | * `--registry-path` specifies the location of the registry to use for the UV executor. 28 | * `-o` enables output printing. 29 | 30 | 31 | 32 | ## Development 33 | Requirements 34 | - `uv` >= 0.4 35 | 36 | 37 | See the justfile (requires `just`) for common development commands. For example: 38 | 39 | ```sh 40 | just test 41 | ``` 42 | 43 | ## Documentation 44 | 45 | To build the documentation locally, run the command in the top level justfile. 46 | 47 | ```sh 48 | just docs 49 | ``` 50 | 51 | Documentation will be built in HTML format by default and will be available in the `docs/build/html` folder. 52 | 53 | For documentation we use the [MyST](https://myst-parser.readthedocs.io/en/latest/index.html) parser with markdown files. 54 | 55 | 56 | ## License 57 | 58 | This project is licensed under Apache License, Version 2.0 ([LICENSE][] or http://www.apache.org/licenses/LICENSE-2.0). 59 | 60 | 61 | [LICENSE]: https://github.com/CQCL/tierkreis/blob/main/LICENCE 62 | -------------------------------------------------------------------------------- /docs/source/worker/index.md: -------------------------------------------------------------------------------- 1 | # Workers 2 | 3 | A worker implements _atomic_ functionalities that will not be broken further by the controller. 4 | Tierkreis workers come in three flavors: 5 | 6 | - Builtin workers, provided by Tierkreis itself 7 | - User defined workers, by using the `@worker.task()` annotation or writing external ones 8 | - Prepackaged workers from the Tierkreis developers 9 | 10 | ```{toctree} 11 | :maxdepth: 2 12 | complex_types.md 13 | external_workers.md 14 | hello_world.md 15 | native_workers/index 16 | ``` 17 | 18 | ## Prepackaged workers 19 | 20 | The following outlines the functionality of the prepackaged workers. 21 | Currently the following workers are provided as separate packages on pypi: 22 | 23 | - IBMQ 24 | - Qiskit Aer 25 | - Pytket 26 | - Quantinuum Nexus 27 | - Quantinuum Backend 28 | 29 | ### Qiskit Aer 30 | 31 | Compile and run quantum circuits locally with Qiskit Aer. 32 | More detailed docs [here](worker/native_workers/aer_worker.md). 33 | 34 | ### IBMQ 35 | 36 | Compile and run quantum circuits locally with IBMQ. 37 | More detailed docs [here](worker/native_workers/ibmq_worker.md). 38 | 39 | ### Pytket 40 | 41 | The pytket compiler suite to optimize circuits. 42 | The worker only contains a subset of common operations. 43 | For a custom compilation pipeline it is advised to build your own worker. 44 | 45 | More detailed docs [here](worker/native_workers/pytket_worker.md). 46 | 47 | **Installation** 48 | 49 | ```sh 50 | pip install tkr-pytket-worker 51 | ``` 52 | 53 | will install an executable Python script `tkr_pytket_worker` into your virtual environment. 54 | 55 | **Example** 56 | 57 | See the example `hamiltonian_graph.py`. 58 | 59 | ### Quantinuum Nexus 60 | 61 | Interface to the Quantinuum Nexus platform. 62 | More detailed docs [here](worker/native_workers/nexus_worker.md). 63 | 64 | ### Quantinuum Backend 65 | 66 | Compile and run quantum circuits locally with Quantinuum backends. 67 | More detailed docs [here](worker/native_workers/quantinuum_worker.md). 68 | 69 | ### Qulacs 70 | 71 | Compile and run quantum circuits locally with Qulacs. 72 | More detailed docs [here](worker/native_workers/qulacs_worker.md). 73 | -------------------------------------------------------------------------------- /tierkreis/tests/executor/test_hpc_executor.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | from uuid import UUID 3 | import pytest 4 | from tierkreis.builder import GraphBuilder 5 | from tierkreis.controller import run_graph 6 | from tierkreis.controller.data.graph import GraphData 7 | from tierkreis.controller.data.models import TKR 8 | from tierkreis.controller.executor.hpc.job_spec import ( 9 | JobSpec, 10 | MpiSpec, 11 | ResourceSpec, 12 | ) 13 | from tierkreis.controller.executor.hpc.slurm import SLURMExecutor 14 | from tierkreis.controller.storage.filestorage import ControllerFileStorage 15 | 16 | from tests.executor.stubs import mpi_rank_info 17 | from tierkreis.storage import read_outputs 18 | 19 | 20 | def mpi_graph() -> GraphData: 21 | builder = GraphBuilder(outputs_type=TKR[str | None]) 22 | mpi_result = builder.task(mpi_rank_info()) 23 | builder.outputs(mpi_result) 24 | return builder.data 25 | 26 | 27 | def job_spec() -> JobSpec: 28 | return JobSpec( 29 | job_name="test_job", 30 | account="test_usr", 31 | command="--allow-run-as-root /root/.local/bin/uv run /slurm_mpi_worker/main.py ", 32 | resource=ResourceSpec(nodes=2, memory_gb=None), 33 | walltime="00:15:00", 34 | mpi=MpiSpec(max_proc_per_node=1), 35 | extra_scheduler_args={"--open-mode=append": None}, 36 | output_path=Path("./logs.log"), 37 | error_path=Path("./errors.log"), 38 | ) 39 | 40 | 41 | @pytest.mark.skip(reason="Needs SLURM setup.") 42 | def test_slurm_with_mpi() -> None: 43 | g = mpi_graph() 44 | storage = ControllerFileStorage( 45 | UUID(int=22), 46 | name="mpi_graph", 47 | do_cleanup=True, 48 | ) 49 | sbatch = str( 50 | Path(__file__).parent.parent.parent.parent / "infra/slurm_local/sbatch" 51 | ) 52 | executor = SLURMExecutor( 53 | spec=job_spec(), 54 | registry_path=None, 55 | logs_path=storage.logs_path, 56 | command=sbatch, 57 | ) 58 | run_graph(storage, executor, g, {}) 59 | 60 | output = read_outputs(g, storage) 61 | 62 | assert output is not None 63 | assert output == "Rank 0 out of 2 on c1.\nRank 1 out of 2 on c2." 64 | -------------------------------------------------------------------------------- /tierkreis_visualization/frontend/src/components/ui/tooltip.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as TooltipPrimitive from "@radix-ui/react-tooltip" 5 | 6 | import { cn } from "@/lib/utils" 7 | 8 | function TooltipProvider({ 9 | delayDuration = 0, 10 | ...props 11 | }: React.ComponentProps) { 12 | return ( 13 | 18 | ) 19 | } 20 | 21 | function Tooltip({ 22 | ...props 23 | }: React.ComponentProps) { 24 | return ( 25 | 26 | 27 | 28 | ) 29 | } 30 | 31 | function TooltipTrigger({ 32 | ...props 33 | }: React.ComponentProps) { 34 | return 35 | } 36 | 37 | function TooltipContent({ 38 | className, 39 | sideOffset = 0, 40 | children, 41 | ...props 42 | }: React.ComponentProps) { 43 | return ( 44 | 45 | 54 | {children} 55 | 56 | 57 | 58 | ) 59 | } 60 | 61 | export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider } 62 | -------------------------------------------------------------------------------- /tierkreis/tierkreis/cli/run_workflow.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | import uuid 3 | import logging 4 | 5 | from tierkreis.controller import run_graph 6 | from tierkreis.controller.data.graph import GraphData 7 | from tierkreis.controller.data.location import Loc 8 | from tierkreis.controller.data.types import PType 9 | from tierkreis.controller.storage.filestorage import ControllerFileStorage 10 | from tierkreis.controller.executor.shell_executor import ShellExecutor 11 | from tierkreis.controller.executor.uv_executor import UvExecutor 12 | 13 | logger = logging.getLogger(__name__) 14 | 15 | 16 | def run_workflow( 17 | graph: GraphData, 18 | inputs: dict[str, PType], 19 | name: str | None = None, 20 | run_id: int | None = None, 21 | log_level: int | str = logging.INFO, 22 | registry_path: Path | None = None, 23 | print_output: bool = False, 24 | use_uv_worker: bool = False, 25 | n_iterations: int = 10000, 26 | polling_interval_seconds: float = 0.1, 27 | ) -> None: 28 | """Run a workflow.""" 29 | logging.basicConfig( 30 | format="%(asctime)s: %(message)s", 31 | datefmt="%Y-%m-%dT%H:%M:%S%z", 32 | level=log_level, 33 | ) 34 | 35 | if run_id is None: 36 | workflow_id = uuid.uuid4() 37 | else: 38 | workflow_id = uuid.UUID(int=run_id) 39 | logging.info("Workflow ID is %s", workflow_id) 40 | storage = ControllerFileStorage(workflow_id, name=name, do_cleanup=True) 41 | if registry_path is None: 42 | registry_path = Path(__file__).parent 43 | if use_uv_worker: 44 | executor = UvExecutor(registry_path=registry_path, logs_path=storage.logs_path) 45 | else: 46 | executor = ShellExecutor(registry_path, storage.workflow_dir) 47 | 48 | logging.info("Starting workflow at location: %s", storage.logs_path) 49 | 50 | run_graph( 51 | storage, 52 | executor, 53 | graph, 54 | inputs, 55 | n_iterations, 56 | polling_interval_seconds, 57 | ) 58 | if print_output: 59 | all_outputs = graph.nodes[graph.output_idx()].inputs 60 | for output in all_outputs: 61 | print(f"{output}: {storage.read_output(Loc(), output)}") 62 | -------------------------------------------------------------------------------- /tierkreis/tierkreis/controller/storage/in_memory.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | from uuid import UUID 3 | from time import time 4 | 5 | 6 | from tierkreis.controller.storage.protocol import ( 7 | StorageEntryMetadata, 8 | ControllerStorage, 9 | ) 10 | 11 | 12 | class InMemoryFileData: 13 | value: bytes 14 | stats: StorageEntryMetadata 15 | 16 | def __init__(self, value: bytes) -> None: 17 | self.value = value 18 | self.stats = StorageEntryMetadata(time()) 19 | 20 | 21 | class ControllerInMemoryStorage(ControllerStorage): 22 | def __init__( 23 | self, 24 | workflow_id: UUID, 25 | name: str | None = None, 26 | tierkreis_directory: Path = Path(), 27 | ) -> None: 28 | self.tkr_dir = tierkreis_directory 29 | self.workflow_id = workflow_id 30 | self.name = name 31 | 32 | self.files: dict[Path, InMemoryFileData] = {} 33 | 34 | def delete(self, path: Path) -> None: 35 | self.files = {} 36 | 37 | def exists(self, path: Path) -> bool: 38 | return path in list(self.files.keys()) 39 | 40 | def list_subpaths(self, path: Path) -> list[Path]: 41 | if path == self.workflow_dir: 42 | nodes = set( 43 | [ 44 | Path("/".join(str(x).split("/")[:2])) 45 | for x in self.files.keys() 46 | if str(x).startswith(str(path) + "/") 47 | ] 48 | ) 49 | return list(nodes) 50 | return [x for x in self.files.keys() if str(x).startswith(str(path) + "/")] 51 | 52 | def link(self, src: Path, dst: Path) -> None: 53 | self.files[dst] = self.files[src] 54 | 55 | def mkdir(self, path: Path) -> None: 56 | return 57 | 58 | def read(self, path: Path) -> bytes: 59 | return self.files[path].value 60 | 61 | def touch(self, path: Path, is_dir: bool = False) -> None: 62 | self.files[path] = InMemoryFileData(b"") 63 | 64 | def stat(self, path: Path) -> StorageEntryMetadata: 65 | return self.files[path].stats 66 | 67 | def write(self, path: Path, value: bytes) -> None: 68 | self.files[path] = InMemoryFileData(value) 69 | -------------------------------------------------------------------------------- /tierkreis/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | 4 | ## [0.5.0] (2024-02-15) 5 | 6 | 7 | ### Features 8 | 9 | * New `OpaqueModel` which derives from pydantic `BaseModel` to defer Tierkreis 10 | serialization to pydantic. 11 | * New `TierkreisType.register_alternative` method to register 12 | Tierkreis-compatible alternatives to types to use for 13 | serialization/deserialization for arbitrary types. 14 | * `UnpackRow` base dataclass for defining rows that should be unpacked in Tierkreis 15 | (e.g. multiple outputs in a worker function). 16 | 17 | ### Breaking changes 18 | 19 | * Graph, Signature and Worker protos updated to v1alpha1 to add `FunctionNode` 20 | message to graph proto, with optional `retry_secs` field. Graphs serialized 21 | with v1alpha would deserialize as containing functions with empty names. 22 | * `ServerRuntime` simplified to only offer `run_graph` for execution, all "task" 23 | related methods removed. `runtime.proto` updated to v1alpha1 to match. 24 | * `TierkreisValue.from_python` uses python type to guide conversion - takes an 25 | optional annotation or uses `type(value)` as default. 26 | Subclasses of supported types will no longer converted as the supported super type. 27 | 28 | 29 | ## [0.4.1] (2024-02-07) 30 | 31 | 32 | ### Bug Fixes 33 | * Fix bug in `val_known_tk_type` (assumed dataclass for all structures). 34 | * Fix protobuf versioning 35 | * Fix shebang in pytket worker 36 | 37 | 38 | ## [0.4.0] (2024-02-01) 39 | 40 | 41 | ### Features 42 | 43 | * Support Pydantic `BaseModel` automated conversion to 44 | `StructType`/`StructValue`. 45 | * `UnpackRow` base dataclass for defining rows that should be unpacked in Tierkreis 46 | (e.g. multiple outputs in a worker function). 47 | 48 | ### Breaking changes 49 | 50 | * `TierkreisStruct` and `register_struct_convertible` are removed. Now automated 51 | conversion will be attempted on any dataclass or Pydantic `BaseModel`. 52 | * `ServerRuntime` simplified to only offer `run_graph` for execution, all "task" 53 | related methods removed. `runtime.proto` updated to v1alpha1 to match. 54 | 55 | ### Minor changes 56 | 57 | * `CircStruct` simplified to just contain the JSON string of a Pytket Circuit 58 | (essentially acts as an opaque newtype). -------------------------------------------------------------------------------- /tierkreis/tests/controller/test_read_loop_trace.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | from typing import Any, Type 3 | from uuid import UUID 4 | 5 | import pytest 6 | from tests.controller.loop_graphdata import loop_multiple_acc, loop_multiple_acc_untyped 7 | from tierkreis.controller import run_graph 8 | from tierkreis.controller.executor.in_memory_executor import InMemoryExecutor 9 | from tierkreis.controller.executor.shell_executor import ShellExecutor 10 | from tierkreis.controller.storage.filestorage import ControllerFileStorage 11 | from tierkreis.controller.storage.in_memory import ControllerInMemoryStorage 12 | from tierkreis.controller.data.graph import GraphData 13 | from tierkreis.storage import read_loop_trace 14 | 15 | 16 | return_value = [ 17 | {"acc1": x, "acc2": y, "acc3": z} 18 | for x, y, z in zip(range(1, 7), range(2, 13, 2), range(3, 19, 3)) 19 | ] 20 | 21 | params: list[tuple[GraphData, Any, str, int]] = [ 22 | ( 23 | loop_multiple_acc_untyped(), 24 | return_value, 25 | "multi_acc", 26 | 9, 27 | ), 28 | ( 29 | loop_multiple_acc().get_data(), 30 | return_value, 31 | "multi_acc", 32 | 9, 33 | ), 34 | ] 35 | ids = [ 36 | "loop_multiple_acc_untyped", 37 | "loop_multiple_acc", 38 | ] 39 | 40 | storage_classes = [ControllerFileStorage, ControllerInMemoryStorage] 41 | storage_ids = ["FileStorage", "In-memory"] 42 | 43 | 44 | @pytest.mark.parametrize("storage_class", storage_classes, ids=storage_ids) 45 | @pytest.mark.parametrize("graph,output,name,id", params, ids=ids) 46 | def test_read_loop_trace( 47 | storage_class: Type[ControllerFileStorage | ControllerInMemoryStorage], 48 | graph: GraphData, 49 | output: Any, 50 | name: str, 51 | id: int, 52 | ): 53 | g = graph 54 | storage = storage_class(UUID(int=id), name=name) 55 | executor = ShellExecutor(Path("./python/examples/launchers"), Path("")) 56 | if isinstance(storage, ControllerInMemoryStorage): 57 | executor = InMemoryExecutor(Path("./tierkreis/tierkreis"), storage=storage) 58 | storage.clean_graph_files() 59 | run_graph(storage, executor, g, {}) 60 | 61 | actual_output = read_loop_trace(g, storage, "my_loop") 62 | assert actual_output == output 63 | -------------------------------------------------------------------------------- /tierkreis/tierkreis/controller/storage/filestorage.py: -------------------------------------------------------------------------------- 1 | import os 2 | import shutil 3 | from pathlib import Path 4 | from time import time_ns 5 | from uuid import UUID 6 | 7 | from tierkreis.controller.storage.protocol import ( 8 | StorageEntryMetadata, 9 | ControllerStorage, 10 | ) 11 | 12 | 13 | class ControllerFileStorage(ControllerStorage): 14 | def __init__( 15 | self, 16 | workflow_id: UUID, 17 | name: str | None = None, 18 | tierkreis_directory: Path = Path.home() / ".tierkreis" / "checkpoints", 19 | do_cleanup: bool = False, 20 | ) -> None: 21 | self.tkr_dir = tierkreis_directory 22 | self.workflow_id = workflow_id 23 | self.name = name 24 | if do_cleanup: 25 | self.delete(self.workflow_dir) 26 | 27 | def delete(self, path: Path) -> None: 28 | uid = os.getuid() 29 | tmp_dir = Path(f"/tmp/{uid}/tierkreis/archive/{self.workflow_id}/{time_ns()}") 30 | tmp_dir.mkdir(parents=True, exist_ok=True) 31 | if self.exists(path): 32 | shutil.move(path, tmp_dir) 33 | 34 | def exists(self, path: Path) -> bool: 35 | return path.exists() 36 | 37 | def list_subpaths(self, path: Path) -> list[Path]: 38 | return [sub_path for sub_path in path.iterdir()] 39 | 40 | def link(self, src: Path, dst: Path) -> None: 41 | dst.parent.mkdir(parents=True, exist_ok=True) 42 | os.link(src, dst) 43 | 44 | def mkdir(self, path: Path) -> None: 45 | return path.mkdir(parents=True, exist_ok=True) 46 | 47 | def read(self, path: Path) -> bytes: 48 | with open(path, "rb") as fh: 49 | return fh.read() 50 | 51 | def touch(self, path: Path, is_dir: bool = False) -> None: 52 | if is_dir: 53 | path.mkdir(parents=True, exist_ok=True) 54 | return 55 | 56 | path.parent.mkdir(parents=True, exist_ok=True) 57 | path.touch() 58 | 59 | def stat(self, path: Path) -> StorageEntryMetadata: 60 | return StorageEntryMetadata(path.stat().st_mtime) 61 | 62 | def write(self, path: Path, value: bytes) -> None: 63 | path.parent.mkdir(parents=True, exist_ok=True) 64 | with open(path, "wb+") as fh: 65 | fh.write(value) 66 | -------------------------------------------------------------------------------- /tierkreis/tierkreis/storage.py: -------------------------------------------------------------------------------- 1 | from tierkreis.builder import GraphBuilder 2 | from tierkreis.controller.data.graph import GraphData 3 | from tierkreis.controller.data.location import Loc 4 | from tierkreis.controller.data.types import PType, ptype_from_bytes 5 | from tierkreis.controller.storage.protocol import ControllerStorage 6 | from tierkreis.controller.storage.filestorage import ( 7 | ControllerFileStorage as FileStorage, 8 | ) 9 | from tierkreis.controller.storage.in_memory import ( 10 | ControllerInMemoryStorage as InMemoryStorage, 11 | ) 12 | from tierkreis.exceptions import TierkreisError 13 | 14 | __all__ = ["FileStorage", "InMemoryStorage"] 15 | 16 | 17 | def read_outputs( 18 | g: GraphData | GraphBuilder, storage: ControllerStorage 19 | ) -> dict[str, PType] | PType: 20 | if isinstance(g, GraphBuilder): 21 | g = g.get_data() 22 | 23 | out_ports = list(g.nodes[g.output_idx()].inputs.keys()) 24 | if len(out_ports) == 1 and "value" in out_ports: 25 | return ptype_from_bytes(storage.read_output(Loc(), "value")) 26 | return {k: ptype_from_bytes(storage.read_output(Loc(), k)) for k in out_ports} 27 | 28 | 29 | def read_loop_trace( 30 | g: GraphData | GraphBuilder, 31 | storage: ControllerStorage, 32 | node_name: str, 33 | output_name: str | None = None, 34 | ) -> list[PType | dict[str, list[PType]]]: 35 | """Reads the trace of a loop from storage.""" 36 | if isinstance(g, GraphBuilder): 37 | g = g.get_data() 38 | loc = storage.loc_from_node_name(node_name) 39 | if loc is None: 40 | raise TierkreisError(f"Loop name {node_name} not found in debug data.") 41 | output_names = storage.read_output_ports(loc) 42 | if output_name is None: 43 | traces = { 44 | name: [ptype_from_bytes(r) for r in storage.read_loop_trace(loc, name)] 45 | for name in output_names 46 | if name != "should_continue" 47 | } 48 | return [dict(zip(traces.keys(), vals)) for vals in zip(*traces.values())] 49 | 50 | if output_name not in output_names: 51 | raise TierkreisError(f"Output name {output_name} not found in loop node output") 52 | results = storage.read_loop_trace(loc, output_name) 53 | return [ptype_from_bytes(r) for r in results] 54 | -------------------------------------------------------------------------------- /tierkreis_visualization/tierkreis_visualization/templates/eval.jinja: -------------------------------------------------------------------------------- 1 | 2 | 3 | {% from "macros/breadcrumbs.html" import breadcrumbs_macro with context %} 4 | {% from "macros/common_head.html" import common_head_macro with context %} 5 | {% from "macros/header.html" import header_macro with context %} 6 | {% from "macros/json_editor.html" import json_editor_macro with context %} 7 | 8 | 9 | Tierkreis visualization: EVAL node 10 | {{ common_head_macro() }} 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | {{ header_macro() }} 19 |
20 | {{ breadcrumbs_macro(breadcrumbs, true) }} 21 |
22 |
23 | 26 | 27 | {{ json_editor_macro(nodes|tojson, edges|tojson) }} 28 |
29 |
30 | 66 | 67 | 68 | 69 | 70 | -------------------------------------------------------------------------------- /examples/pytket_graph.py: -------------------------------------------------------------------------------- 1 | # /// script 2 | # requires-python = ">=3.12" 3 | # dependencies = ["tierkreis", "pytket"] 4 | # 5 | # [tool.uv.sources] 6 | # tierkreis = { path = "../tierkreis", editable = true } 7 | # /// 8 | from uuid import UUID 9 | from pathlib import Path 10 | from typing import NamedTuple 11 | 12 | from pytket._tket.circuit import Circuit 13 | from pytket.backends.backendresult import BackendResult 14 | 15 | from tierkreis.builder import GraphBuilder 16 | 17 | from tierkreis.controller.data.models import TKR, OpaqueType 18 | from tierkreis.controller import run_graph 19 | from tierkreis.storage import read_outputs, InMemoryStorage 20 | from tierkreis.controller.executor.in_memory_executor import InMemoryExecutor 21 | from tierkreis.ibmq_worker import compile_circuit_ibmq 22 | from tierkreis.aer_worker import submit_single 23 | 24 | 25 | def ghz() -> Circuit: 26 | circ1 = Circuit(2) 27 | circ1.H(0) 28 | circ1.CX(0, 1) 29 | circ1.measure_all() 30 | return circ1 31 | 32 | 33 | class IBMInput(NamedTuple): 34 | circuit: TKR[OpaqueType["pytket._tket.circuit.Circuit"]] # noqa: F821 35 | n_shots: TKR[int] 36 | backend: TKR[str] 37 | 38 | 39 | def compile_run_single(): 40 | g = GraphBuilder( 41 | IBMInput, TKR[OpaqueType["pytket.backends.backendresult.BackendResult"]] 42 | ) 43 | 44 | compiled_circuit = g.task( 45 | compile_circuit_ibmq( 46 | circuit=g.inputs.circuit, 47 | device_name=g.inputs.backend, 48 | optimisation_level=g.const(2), 49 | ) 50 | ) 51 | res = g.task(submit_single(compiled_circuit, g.inputs.n_shots)) 52 | g.outputs(res) 53 | return g 54 | 55 | 56 | def main(): 57 | g = compile_run_single() 58 | storage = InMemoryStorage(UUID(int=109)) 59 | executor = InMemoryExecutor( 60 | Path(__file__).parent.parent / "tierkreis_workers", storage 61 | ) 62 | n_shots = 30 63 | run_graph( 64 | storage, 65 | executor, 66 | g, 67 | { 68 | "circuit": ghz(), 69 | "n_shots": n_shots, 70 | }, 71 | polling_interval_seconds=0.1, 72 | ) 73 | res = read_outputs(g, storage) 74 | assert isinstance(res, BackendResult) 75 | assert len(res.get_shots()) == n_shots 76 | print(res) 77 | 78 | 79 | if __name__ == "__main__": 80 | main() 81 | -------------------------------------------------------------------------------- /tierkreis_visualization/tierkreis_visualization/data/loop.py: -------------------------------------------------------------------------------- 1 | import json 2 | from pydantic import BaseModel 3 | from tierkreis.controller.data.location import Loc 4 | from tierkreis.controller.storage.protocol import ControllerStorage 5 | 6 | 7 | from tierkreis_visualization.data.eval import check_error 8 | from tierkreis_visualization.data.models import PyNode, PyEdge 9 | 10 | 11 | class LoopNodeData(BaseModel): 12 | nodes: list[PyNode] 13 | edges: list[PyEdge] 14 | 15 | 16 | def get_loop_node( 17 | storage: ControllerStorage, node_location: Loc, errored_nodes: list[Loc] 18 | ) -> LoopNodeData: 19 | i = 0 20 | while storage.is_node_started(node_location.L(i + 1)): 21 | i += 1 22 | new_location = node_location.L(i) 23 | 24 | nodes = [ 25 | PyNode( 26 | id=n, 27 | status="Finished", 28 | function_name=f"L{n}", 29 | node_location=node_location.L(n), 30 | started_time=storage.read_started_time(node_location.L(n)) or "", 31 | finished_time=storage.read_finished_time(node_location.L(n)) or "", 32 | ) 33 | for n in range(i) 34 | ] 35 | 36 | if check_error(node_location, errored_nodes): 37 | last_status = "Error" 38 | elif storage.is_node_finished(new_location): 39 | last_status = "Finished" 40 | else: 41 | last_status = "Started" 42 | nodes.append( 43 | PyNode( 44 | id=i, 45 | status=last_status, 46 | function_name=f"L{i}", 47 | node_location=new_location, 48 | started_time=storage.read_started_time(new_location) or "", 49 | finished_time=storage.read_finished_time(new_location) or "", 50 | ) 51 | ) 52 | edges = [] 53 | for port_name in storage.read_node_def(node_location.L(0)).outputs: 54 | edges.extend( 55 | [ 56 | PyEdge( 57 | from_node=n, 58 | from_port=port_name, 59 | to_node=n + 1, 60 | to_port=port_name, 61 | value=json.loads( 62 | storage.read_output(node_location.L(n), port_name) 63 | ), 64 | ) 65 | for n in range(i) 66 | ] 67 | ) 68 | return LoopNodeData(nodes=nodes, edges=edges) 69 | -------------------------------------------------------------------------------- /docs/source/tutorial/eval.md: -------------------------------------------------------------------------------- 1 | --- 2 | file_format: mystnb 3 | kernelspec: 4 | name: python3 5 | --- 6 | 7 | # Nested graphs using Eval 8 | 9 | To create this graph we need only to install the `tierkreis` package: 10 | 11 | ``` 12 | pip install tierkreis 13 | ``` 14 | 15 | ## Graph 16 | 17 | We can run graphs from within other graphs by using `GraphBuilder.eval`. 18 | Recall the `fib_step` graph that we wrote last time: 19 | 20 | ```{code-cell} ipython3 21 | from typing import NamedTuple 22 | 23 | from tierkreis.builder import GraphBuilder 24 | from tierkreis.builtins.stubs import iadd 25 | from tierkreis.models import TKR 26 | 27 | 28 | class FibData(NamedTuple): 29 | a: TKR[int] 30 | b: TKR[int] 31 | 32 | 33 | fib_step = GraphBuilder(FibData, FibData) 34 | sum = fib_step.task(iadd(fib_step.inputs.a, fib_step.inputs.b)) 35 | fib_step.outputs(FibData(fib_step.inputs.b, sum)) 36 | ``` 37 | 38 | We create a graph `fib4` that calls `fib_step` three times. 39 | The graph will have no inputs and gives a single integer as output: 40 | 41 | ```{code-cell} ipython3 42 | from tierkreis.models import EmptyModel 43 | 44 | fib4 = GraphBuilder(EmptyModel, TKR[int]) 45 | ``` 46 | 47 | The `GraphBuilder.eval` method takes a `GraphBuilder` object as its first argument 48 | and the appropriately typed input data as the second object. 49 | 50 | ```{code-cell} ipython3 51 | second = fib4.eval(fib_step, FibData(a=fib4.const(0), b=fib4.const(1))) 52 | ``` 53 | 54 | We can iterate manually as follows: 55 | 56 | ```{code-cell} ipython3 57 | third = fib4.eval(fib_step, second) 58 | fourth = fib4.eval(fib_step, third) 59 | fib4.outputs(fourth.b) 60 | ``` 61 | 62 | In the [next tutorial](./loop.md) we will see how to iterate programmatically. 63 | 64 | # Execution 65 | 66 | Since we still only use built-in functions, we execute the graph in the same way as before. 67 | 68 | ```{code-cell} ipython3 69 | from uuid import UUID 70 | from pathlib import Path 71 | 72 | from tierkreis import run_graph 73 | from tierkreis.storage import FileStorage, read_outputs 74 | from tierkreis.executor import ShellExecutor 75 | 76 | storage = FileStorage(UUID(int=99), name="Nested graphs using Eval") 77 | executor = ShellExecutor(Path("."), workflow_dir=storage.workflow_dir) 78 | 79 | storage.clean_graph_files() 80 | run_graph(storage, executor, fib4.get_data(), {}) 81 | print(read_outputs(fib4, storage)) 82 | ``` 83 | --------------------------------------------------------------------------------