From 057e39bd78e19d78150e612324303a59acc8aeb5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Konrad=20Ja=C5=82owiecki?= Date: Mon, 25 Mar 2024 16:02:20 +0100 Subject: [PATCH] Add basic routine visualization and update README (#4) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Implement rendering to graphviz DAG * Minor style fixes * Add script for rendering serialized diagram * Rename: hqar.experimental.visualization -> hqar.experimental.rendering * Add flake8-copyright to pre-commit hooks * Move loading of valid test programs to fixture * Fix typo * Add tests for rendering * Add graphviz to dependencies * Add GH action step with graphviz setup * Make mypy ignore graphviz errors * Add good-enough initial README * Expand README * Explain hierarchy and DAG in README * Remove unnecessary comment * List visualization tool as one of features * Reword tricky -> hacky --------- Co-authored-by: Konrad Jałowiecki --- .flake8 | 5 +- .github/workflows/quality_checks.yaml | 2 + .pre-commit-config.yaml | 2 +- README.md | 143 +++++++++++++- example_routine.svg | 107 +++++++++++ example_routine.yaml | 60 ++++++ poetry.lock | 18 +- pyproject.toml | 11 ++ src/hqar/__init__.py | 1 + src/hqar/_schema_v1.py | 1 + src/hqar/experimental/__init__.py | 0 src/hqar/experimental/rendering.py | 177 ++++++++++++++++++ tests/conftest.py | 28 +++ tests/hqar/data/valid_program_examples.yaml | 107 ----------- tests/hqar/data/valid_programs/example_0.yaml | 5 + tests/hqar/data/valid_programs/example_1.yaml | 14 ++ tests/hqar/data/valid_programs/example_2.yaml | 50 +++++ tests/hqar/data/valid_programs/example_3.yaml | 37 ++++ tests/hqar/experimental/test_rendering.py | 33 ++++ tests/hqar/test_schema_validation.py | 18 +- 20 files changed, 694 insertions(+), 125 deletions(-) create mode 100644 example_routine.svg create mode 100644 example_routine.yaml create mode 100644 src/hqar/experimental/__init__.py create mode 100644 src/hqar/experimental/rendering.py create mode 100644 tests/conftest.py delete mode 100644 tests/hqar/data/valid_program_examples.yaml create mode 100644 tests/hqar/data/valid_programs/example_0.yaml create mode 100644 tests/hqar/data/valid_programs/example_1.yaml create mode 100644 tests/hqar/data/valid_programs/example_2.yaml create mode 100644 tests/hqar/data/valid_programs/example_3.yaml create mode 100644 tests/hqar/experimental/test_rendering.py diff --git a/.flake8 b/.flake8 index 8f3439f..371c258 100644 --- a/.flake8 +++ b/.flake8 @@ -25,4 +25,7 @@ ignore = per-file-ignores = tests/**/test_*.py:D103,D102,D101 __init__.py:F401 - +copyright-check = True +copyright-regexp = Copyright\s+(©\s+)?\d{4}([-,]\d{4})*\s+%(author)s +copyright-author = PsiQuantum Corp. +copyright-min-file-size = 1 diff --git a/.github/workflows/quality_checks.yaml b/.github/workflows/quality_checks.yaml index ce91d51..32152fa 100644 --- a/.github/workflows/quality_checks.yaml +++ b/.github/workflows/quality_checks.yaml @@ -13,6 +13,8 @@ jobs: uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} + - name: Setup Graphviz + uses: ts-graphviz/setup-graphviz@v2 - name: Install dependencies and the package run: | python -m pip install --upgrade pip poetry diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index a401729..bc9350a 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -20,4 +20,4 @@ repos: rev: 6.0.0 hooks: - id: flake8 - + additional_dependencies: [flake8-copyright] diff --git a/README.md b/README.md index e717819..7a73422 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,141 @@ -# hqar -Hierarchical Quantum Algorithms Representation +# HQAR +Hierarchical Quantum Algorithms Representation is an open format for representing +quantum algorithms, optimized for usage in quantum resource estimation (QRE). + +HQAR comprises: + +- Definition of data format, formalized as a JSON schema. +- A Python library for validation of quantum programs written in HQAR format using [Pydantic](https://docs.pydantic.dev/). +- Rudimentary visualization tool `hqar-render`. + +## Installation + +Using HQAR data format does not require installation - you can easily write quantum +programs in YAML or JSON. + +To install HQAR Python package, clone this repository and install it as usual with `pip`: + +```bash +# Clone HQAR repo (you can use HTTP link as well) +git clone git@github.com:PsiQ/hqar.git +cd hqar +pip install . +``` + +## HQAR format + +HQAR format represents quantum programs as a hierarchical directed acyclic graphs (DAGs). +That's a mouthful, so let us unpack what it means: + +- *hierarchical*: each node can contain subgraphs, i.e. routines can be nested inside + larger routines. +- *directed*: information flow is unidirectional, and the direction is unambiguous. +- *acyclic*: meaning there are no loops. + +Consider the following hierarchical DAG of a hypothetical quantum program: + +![program example](example_routine.svg) + +It can be succinctly written in HQAR format as: + + +```yaml +version: v1 +program: + name: my_algorithm + ports: + - direction: input + name: in_0 + size: 2 + - direction: input + name: in_1 + size: 2 + - direction: output + name: out_0 + size: 4 + children: + - name: subroutine_1 + ports: + - direction: input + name: in_0 + size: 2 + - direction: output + name: out_0 + size: 3 + - name: subroutine_2 + ports: + - direction: input + name: in_0 + size: 2 + - direction: output + name: out_0 + size: 1 + - direction: output + name: out_1 + size: 1 + - name: merge + ports: + - direction: input + name: in_0 + size: 1 + - direction: input + name: in_1 + size: 1 + - direction: input + name: in_2 + size: 2 + - direction: output + name: out_0 + size: 4 + connections: + - source: in_0 + target: subroutine_1.in_0 + - source: in_1 + target: subroutine_2.in_0 + - source: subroutine_1.out_0 + target: merge.in_2 + - source: subroutine_2.out_0 + target: merge.in_0 + - source: subroutine_2.out_1 + target: merge.in_1 + - source: merge.out_0 + target: out_0 +``` + + +For full description of HQAR format, check our [docs](https://example.com). + +## Using HQAR package + +### Using JSON schema for validating data in HQAR format + +JSON schema for HQAR format can be obtained by calling `generate_program_schema` function. +Such schema can be then used for validating user's input, e.g. using `jsonschema` package: + +```python +from jsonschema import validate +from hqar import generate_program_schema + +# Hypothetical function loading your data as native Python dictionary. +data = load_some_program() +schema = generate_program_schema() + +# This will raise if there are some validation errors. +validate(schema, data) +``` + +### Validation using Pydantic models + +If you are familiar with Pydantic, you might find it easier to work with HQAR Pydantic +models instead of interacting with JSON schema directly. In the example below, we create +an instance of `SchemaV1` model from validated data stored in HQAR format: + +```python +from hqar import SchemaV1 + +data = load_some_program() + +# This will raise if data is not valid +program = SchemaV1.model_validate(data) +``` + diff --git a/example_routine.svg b/example_routine.svg new file mode 100644 index 0000000..baf8372 --- /dev/null +++ b/example_routine.svg @@ -0,0 +1,107 @@ + + + + + + + + +cluster_.my_algorithm + +my_algorithm + + + +.my_algorithm.in_0 + +in_0 + + + +.my_algorithm.subroutine_1 + +in_0 + +subroutine_1 + +out_0 + + + +.my_algorithm.in_0->.my_algorithm.subroutine_1:in_0 + + + + + +.my_algorithm.in_1 + +in_1 + + + +.my_algorithm.subroutine_2 + +in_0 + +subroutine_2 + +out_0 + +out_1 + + + +.my_algorithm.in_1->.my_algorithm.subroutine_2:in_0 + + + + + +.my_algorithm.out_0 + +out_0 + + + +.my_algorithm.merge + +in_0 + +in_1 + +in_2 + +merge + +out_0 + + + +.my_algorithm.subroutine_1:out_0->.my_algorithm.merge:in_2 + + + + + +.my_algorithm.subroutine_2:out_0->.my_algorithm.merge:in_0 + + + + + +.my_algorithm.subroutine_2:out_1->.my_algorithm.merge:in_1 + + + + + +.my_algorithm.merge:out_0->.my_algorithm.out_0 + + + + + diff --git a/example_routine.yaml b/example_routine.yaml new file mode 100644 index 0000000..d4b13f6 --- /dev/null +++ b/example_routine.yaml @@ -0,0 +1,60 @@ +version: v1 +program: + name: my_algorithm + ports: + - direction: input + name: in_0 + size: 2 + - direction: input + name: in_1 + size: 2 + - direction: output + name: out_0 + size: 4 + children: + - name: subroutine_1 + ports: + - direction: input + name: in_0 + size: 2 + - direction: output + name: out_0 + size: 3 + - name: subroutine_2 + ports: + - direction: input + name: in_0 + size: 2 + - direction: output + name: out_0 + size: 1 + - direction: output + name: out_1 + size: 1 + - name: merge + ports: + - direction: input + name: in_0 + size: 1 + - direction: input + name: in_1 + size: 1 + - direction: input + name: in_2 + size: 2 + - direction: output + name: out_0 + size: 4 + connections: + - source: in_0 + target: subroutine_1.in_0 + - source: in_1 + target: subroutine_2.in_0 + - source: subroutine_1.out_0 + target: merge.in_2 + - source: subroutine_2.out_0 + target: merge.in_0 + - source: subroutine_2.out_1 + target: merge.in_1 + - source: merge.out_0 + target: out_0 diff --git a/poetry.lock b/poetry.lock index f51f6d3..f88e8a0 100644 --- a/poetry.lock +++ b/poetry.lock @@ -131,6 +131,22 @@ mccabe = ">=0.7.0,<0.8.0" pycodestyle = ">=2.11.0,<2.12.0" pyflakes = ">=3.2.0,<3.3.0" +[[package]] +name = "graphviz" +version = "0.20.3" +description = "Simple Python interface for Graphviz" +optional = false +python-versions = ">=3.8" +files = [ + {file = "graphviz-0.20.3-py3-none-any.whl", hash = "sha256:81f848f2904515d8cd359cc611faba817598d2feaac4027b266aa3eda7b3dde5"}, + {file = "graphviz-0.20.3.zip", hash = "sha256:09d6bc81e6a9fa392e7ba52135a9d49f1ed62526f96499325930e87ca1b5925d"}, +] + +[package.extras] +dev = ["flake8", "pep8-naming", "tox (>=3)", "twine", "wheel"] +docs = ["sphinx (>=5,<7)", "sphinx-autodoc-typehints", "sphinx-rtd-theme"] +test = ["coverage", "pytest (>=7,<8.1)", "pytest-cov", "pytest-mock (>=3)"] + [[package]] name = "iniconfig" version = "2.0.0" @@ -660,4 +676,4 @@ files = [ [metadata] lock-version = "2.0" python-versions = "^3.9" -content-hash = "5540d2e53358da65644b671965749e98e704f2aec836dd6305d6afc11c88f810" +content-hash = "a92eed0b5238eff7943419c128dcb3d5c65d4fdd475cf6dfec61279c307bda16" diff --git a/pyproject.toml b/pyproject.toml index 8ec3542..6ccb055 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -9,6 +9,7 @@ readme = "README.md" [tool.poetry.dependencies] python = "^3.9" pydantic = "^2.0" +graphviz = "^0.20.3" [tool.poetry.group.dev.dependencies] @@ -19,6 +20,11 @@ black = "^24.2.0" pyyaml = "^6" jsonschema = "^4" + +[tool.poetry.scripts] +hqar-render = "hqar.experimental.rendering:render_entry_point" + + [build-system] requires = ["poetry-core"] build-backend = "poetry.core.masonry.api" @@ -27,3 +33,8 @@ build-backend = "poetry.core.masonry.api" [tool.black] line-length = 100 target-version = ['py39'] + + +[[tool.mypy.overrides]] +module = "graphviz.*" +ignore_missing_imports = true diff --git a/src/hqar/__init__.py b/src/hqar/__init__.py index 7cb4332..697d196 100644 --- a/src/hqar/__init__.py +++ b/src/hqar/__init__.py @@ -9,6 +9,7 @@ Public API of HQAR. """ + from typing import Any from ._schema_v1 import SchemaV1, generate_schema_v1 diff --git a/src/hqar/_schema_v1.py b/src/hqar/_schema_v1.py index 1f27001..ccac3b1 100644 --- a/src/hqar/_schema_v1.py +++ b/src/hqar/_schema_v1.py @@ -9,6 +9,7 @@ Pydantic models used for defining V1 schema of Routine. """ + from __future__ import annotations from typing import Annotated, Any, Literal, Optional, Union diff --git a/src/hqar/experimental/__init__.py b/src/hqar/experimental/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/hqar/experimental/rendering.py b/src/hqar/experimental/rendering.py new file mode 100644 index 0000000..d6b98af --- /dev/null +++ b/src/hqar/experimental/rendering.py @@ -0,0 +1,177 @@ +""" +.. Copyright © 2023-2024 PsiQuantum Corp. All rights reserved. + PSIQUANTUM CORP. CONFIDENTIAL + This file includes unpublished proprietary source code of PsiQuantum Corp. + The copyright notice above does not evidence any actual or intended publication + of such source code. Disclosure of this source code or any related proprietary + information is strictly prohibited without the express written permission of + PsiQuantum Corp. + +Experimental visualization capabilities for HQAR. + +Currently, the visualizations are done with graphviz, which does not suport +hierarchical structures. Therefore, we have to use a somewhat hacky representation +of our routines: + +- The leaf nodes are drawn as a single graphviz node (this is not surprising) with + Mrecord shape. This allows to visually separate routine name from its ports. +- The non-leaf nodes are represented as clusters. + - Clusters can't use Mrecord shape, and so the ports are drawn as separate nodes. + - Input and output ports are grouped into subgraphs with the same rank, which + forces all inputs / all outputs to be placed in a single column. + +Because of the above dichotomy, care has to be taken when constructing edges. +- If addressing port of a leaf, it must be specified as graphviz port, e.g: + "root.child:in_0" +- If addressing port of a non-leaf, you use normal node reference, e.g: + "root.child.in_0" +""" + +from argparse import ArgumentParser +from pathlib import Path +from typing import Union + +import graphviz +import yaml + +from .. import SchemaV1 + +# Dictionary of default graph attributes, used for non-leaf nodes +GRAPH_ATTRS = { + "rankdir": "LR", # Draw left to right (default is top to bottom) + "fontname": "Helvetica", +} + +# Keyword args passed to dag.node for drawing leaf nodes +LEAF_NODE_KWARGS = { + "shape": "Mrecord", # Allow drawing leafs split into labels/ports + "style": "bold", # Bold border + "color": "#0288f5", # Nice blue color + "fontsize": "12", # Larger font size then e.g. ports of non-leafs +} + +# Keyword args passed to dag.node for drawing ports of non-leaf nodes +PORT_NODE_KWARGS = { + "style": "bold", # Bold border + "color": "#ffa44a", # Orange border color + "fontsize": "10", # Smaller font size than the rest of the graph + "shape": "circle", +} + +# Additional attributes of subgraphs of ports (ports of the same direction +# are grouped into such subgraphs) +PORT_GROUP_ATTRS = {"rank": "same"} # Place all ports in the group in the same column + +# Additional kwargs passed to dag.subgraph for defining non-leaf clusters +CLUSTER_KWARGS = {"style": "rounded"} # Make cluster edges rounded + + +def _format_node_name(node_name, parent): + """Given a node name and a parent container, format it accordingly. + + Read the module-level docstring for explanation why different port formats + of ports are being used. + """ + if "." not in node_name: # Case 1: parent port (=> port is a graphviz node) + return node_name + + # Resolve the child, assume the graph is correct and thus the child exists. + child_name, port_name = node_name.split(".") + child = next(iter((child for child in parent.children if child.name == child_name))) + if child.children: # Case 2: port of non-leaf child (=> port is a graphviz node) + return node_name + else: # Case 3: port of leaf child (=> port is an actual port of Mrecord, use ":") + return f"{child_name}:{port_name}" + + +def _add_nonleaf_ports(ports, parent_cluster, parent_path: str, group_name): + with parent_cluster.subgraph( + name=f"{parent_path}:{group_name}", graph_attr=PORT_GROUP_ATTRS + ) as subgraph: + for port in ports: + subgraph.node(name=f"{parent_path}.{port.name}", label=port.name, **PORT_NODE_KWARGS) + + +def _split_ports(ports): + input_ports = [] + output_ports = [] + for port in ports: + if port.direction == "input": + input_ports.append(port) + elif port.direction == "output": + output_ports.append(port) + else: + raise ValueError("Bi-directional ports are not yet supported for rendering") + return input_ports, output_ports + + +def _add_nonleaf(routine, dag: graphviz.Digraph, parent_path: str) -> None: + input_ports, output_ports = _split_ports(routine.ports) + full_path = f"{parent_path}.{routine.name}" + + with dag.subgraph( + name=f"cluster_{full_path}", graph_attr={"label": routine.name, **CLUSTER_KWARGS} + ) as cluster: + _add_nonleaf_ports(input_ports, cluster, full_path, "inputs") + _add_nonleaf_ports(output_ports, cluster, full_path, "outputs") + + for child in routine.children: + _add_routine(child, cluster, f"{parent_path}.{routine.name}") + + for connection in routine.connections: + cluster.edge( + full_path + "." + _format_node_name(connection.source, routine), + full_path + "." + _format_node_name(connection.target, routine), + ) + + +def _ports_row(ports) -> str: + return "{" + "|".join(f"<{port.name}> {port.name}" for port in ports) + "}" + + +def _add_leaf(routine, dag: graphviz.Digraph, parent_path: str) -> None: + input_ports, output_ports = _split_ports(routine.ports) + label = f"{{ {_ports_row(input_ports)} | {routine.name } | {_ports_row(output_ports)} }}" + dag.node(".".join((parent_path, routine.name)), label=label, **LEAF_NODE_KWARGS) + + +def _add_routine(routine, dag: graphviz.Digraph, parent_path: str = "") -> None: + if routine.children: + _add_nonleaf(routine, dag, parent_path) + else: + _add_leaf(routine, dag, parent_path) + + +def _ensure_schema_v1(data: Union[dict, SchemaV1]) -> SchemaV1: + return data if isinstance(data, SchemaV1) else SchemaV1(**data) + + +def to_graphviz(data: Union[dict, SchemaV1]) -> graphviz.Digraph: + """Convert routine encoded with v1 schema to a graphviz DAG.""" + data = _ensure_schema_v1(data) + dag = graphviz.Digraph(graph_attr=GRAPH_ATTRS) + _add_routine(data.program, dag) + return dag + + +def render_entry_point(): + parser = ArgumentParser() + parser.add_argument( + "input", help="Path to the YAML or JSON file with Routine in V1 schema", type=Path + ) + parser.add_argument( + "output", + help=( + "Path to the output file. File format is determined based on the extension, " + "which should be either .svg or .pdf" + ), + type=Path, + ) + + args = parser.parse_args() + + with open(args.input) as f: + routine = SchemaV1.model_validate(yaml.safe_load(f)) + + dag = to_graphviz(routine) + dag.render(args.output.with_suffix(""), format=args.output.suffix.strip(".")) diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..84b10a0 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,28 @@ +""" +.. Copyright © 2023-2024 PsiQuantum Corp. All rights reserved. + PSIQUANTUM CORP. CONFIDENTIAL + This file includes unpublished proprietary source code of PsiQuantum Corp. + The copyright notice above does not evidence any actual or intended publication + of such source code. Disclosure of this source code or any related proprietary + information is strictly prohibited without the express written permission of + PsiQuantum Corp. +""" + +from pathlib import Path + +import pytest +import yaml + +VALID_PROGRAMS_ROOT_PATH = Path(__file__).parent / "hqar/data/valid_programs" + + +def _load_valid_examples(): + for path in VALID_PROGRAMS_ROOT_PATH.iterdir(): + with open(path) as f: + data = yaml.safe_load(f) + yield pytest.param(data["input"], id=data["description"]) + + +@pytest.fixture(params=_load_valid_examples()) +def valid_program(request): + return request.param diff --git a/tests/hqar/data/valid_program_examples.yaml b/tests/hqar/data/valid_program_examples.yaml deleted file mode 100644 index 99b83d0..0000000 --- a/tests/hqar/data/valid_program_examples.yaml +++ /dev/null @@ -1,107 +0,0 @@ -- input: - version: v1 - program: - name: "root" - description: "Empty program" -- input: - version: v1 - program: - name: root - input_params: - - "N" - ports: - - name: in_0 - size: N - direction: input - - name: out_0 - size: 2 - direction: output - description: "Program with ports and input params" -- input: - version: v1 - program: - name: root - input_params: - - "N" - ports: - - name: in_0 - size: N - direction: input - - name: out_0 - size: N - direction: output - - name: out_1 - size: 3 - direction: output - children: - - name: foo - input_params: - - M - ports: - - name: in_0 - direction: input - size: M - - name: out_0 - direction: output - size: 3 - - name: bar - input_params: - - N - ports: - - name: in_0 - direction: input - size: N - - name: out_0 - direction: output - size: N - linked_params: - - source: N - targets: - - foo.M - - bar.N - connections: - - source: in_0 - target: foo.in_0 - - source: foo.out_0 - target: out_0 - - source: bar.out_0 - target: out_1 - description: Fully-featured program -- input: - version: v1 - program: - name: root - ports: - - name: in_0 - size: 1 - direction: input - - name: in_1 - size: 2 - direction: input - - name: out_0 - size: 1 - direction: output - - name: out_1 - size: 2 - direction: output - children: - - name: foo - ports: - - name: in_0 - size: 2 - direction: input - - name: out_0 - size: 2 - direction: output - connections: - - source: in_0 - target: out_0 - connections: - - source: in_0 - target: out_0 - - source: in_1 - target: foo.in_0 - - source: foo.out_0 - target: out_1 - description: Program with passthroughs - diff --git a/tests/hqar/data/valid_programs/example_0.yaml b/tests/hqar/data/valid_programs/example_0.yaml new file mode 100644 index 0000000..a4e7be1 --- /dev/null +++ b/tests/hqar/data/valid_programs/example_0.yaml @@ -0,0 +1,5 @@ +description: Empty program +input: + program: + name: root + version: v1 diff --git a/tests/hqar/data/valid_programs/example_1.yaml b/tests/hqar/data/valid_programs/example_1.yaml new file mode 100644 index 0000000..d15a40c --- /dev/null +++ b/tests/hqar/data/valid_programs/example_1.yaml @@ -0,0 +1,14 @@ +description: Program with ports and input params +input: + program: + input_params: + - N + name: root + ports: + - direction: input + name: in_0 + size: N + - direction: output + name: out_0 + size: 2 + version: v1 diff --git a/tests/hqar/data/valid_programs/example_2.yaml b/tests/hqar/data/valid_programs/example_2.yaml new file mode 100644 index 0000000..7137a4a --- /dev/null +++ b/tests/hqar/data/valid_programs/example_2.yaml @@ -0,0 +1,50 @@ +description: Fully-featured program +input: + program: + children: + - input_params: + - M + name: foo + ports: + - direction: input + name: in_0 + size: M + - direction: output + name: out_0 + size: 3 + - input_params: + - N + name: bar + ports: + - direction: input + name: in_0 + size: N + - direction: output + name: out_0 + size: N + connections: + - source: in_0 + target: foo.in_0 + - source: foo.out_0 + target: out_0 + - source: bar.out_0 + target: out_1 + input_params: + - N + linked_params: + - source: N + targets: + - foo.M + - bar.N + name: root + ports: + - direction: input + name: in_0 + size: N + - direction: output + name: out_0 + size: N + - direction: output + name: out_1 + size: 3 + version: v1 diff --git a/tests/hqar/data/valid_programs/example_3.yaml b/tests/hqar/data/valid_programs/example_3.yaml new file mode 100644 index 0000000..8807a9f --- /dev/null +++ b/tests/hqar/data/valid_programs/example_3.yaml @@ -0,0 +1,37 @@ +description: Program with passthroughs +input: + program: + children: + - connections: + - source: in_0 + target: out_0 + name: foo + ports: + - direction: input + name: in_0 + size: 2 + - direction: output + name: out_0 + size: 2 + connections: + - source: in_0 + target: out_0 + - source: in_1 + target: foo.in_0 + - source: foo.out_0 + target: out_1 + name: root + ports: + - direction: input + name: in_0 + size: 1 + - direction: input + name: in_1 + size: 2 + - direction: output + name: out_0 + size: 1 + - direction: output + name: out_1 + size: 2 + version: v1 diff --git a/tests/hqar/experimental/test_rendering.py b/tests/hqar/experimental/test_rendering.py new file mode 100644 index 0000000..11a7a5a --- /dev/null +++ b/tests/hqar/experimental/test_rendering.py @@ -0,0 +1,33 @@ +""" +.. Copyright © 2023-2024 PsiQuantum Corp. All rights reserved. + PSIQUANTUM CORP. CONFIDENTIAL + This file includes unpublished proprietary source code of PsiQuantum Corp. + The copyright notice above does not evidence any actual or intended publication + of such source code. Disclosure of this source code or any related proprietary + information is strictly prohibited without the express written permission of + PsiQuantum Corp. + +Sanity checks for hqar rendering capabilities. +""" + +import json +from subprocess import Popen + +from hqar.experimental.rendering import to_graphviz + + +def test_example_valid_programs_can_converted_to_graphviz(valid_program): + to_graphviz(valid_program) + + +def test_example_valid_programs_can_be_rendered_from_cli(valid_program, tmp_path): + input_path = tmp_path / "input.json" + output_path = tmp_path / "output.svg" + + with open(input_path, "wt") as f: + json.dump(valid_program, f) + + process = Popen(["hqar-render", input_path, output_path]) + process.wait() + + assert process.returncode == 0 diff --git a/tests/hqar/test_schema_validation.py b/tests/hqar/test_schema_validation.py index d1df996..3aaa945 100644 --- a/tests/hqar/test_schema_validation.py +++ b/tests/hqar/test_schema_validation.py @@ -9,6 +9,7 @@ Test cases checking that schema matches data that we expect it to match. """ + from pathlib import Path import pydantic @@ -38,13 +39,6 @@ def load_invalid_examples(): ] -def load_valid_examples(): - with open(Path(__file__).parent / "data/valid_program_examples.yaml") as f: - data = yaml.safe_load(f) - - return [pytest.param(example["input"], id=example["description"]) for example in data] - - @pytest.mark.parametrize("input, error_path, error_message", load_invalid_examples()) def test_invalid_program_fails_to_validate_with_schema_v1(input, error_path, error_message): with pytest.raises(ValidationError) as err_info: @@ -54,9 +48,8 @@ def test_invalid_program_fails_to_validate_with_schema_v1(input, error_path, err assert err_info.value.message == error_message -@pytest.mark.parametrize("input", load_valid_examples()) -def test_valid_program_fails_to_validate_with_schema_v1(input): - validate_with_v1(input) +def test_valid_program_successfully_validates_with_schema_v1(valid_program): + validate_with_v1(valid_program) @pytest.mark.parametrize("input", [input for input, *_ in load_invalid_examples()]) @@ -65,6 +58,5 @@ def test_invalid_program_fails_to_validate_with_pydantic_model_v1(input): SchemaV1.model_validate(input) -@pytest.mark.parametrize("input", load_valid_examples()) -def test_valid_program_succesfully_validate_with_pydantic_model_v1(input): - SchemaV1.model_validate(input) +def test_valid_program_succesfully_validate_with_pydantic_model_v1(valid_program): + SchemaV1.model_validate(valid_program)