diff --git a/.flake8 b/.flake8 index 5f6e78d..c793ca9 100644 --- a/.flake8 +++ b/.flake8 @@ -1,5 +1,5 @@ [flake8] -max-line-length: 100 +max-line-length: 120 docstring-convention = google ignore = # Allow continuation line under-indented for visual indent. diff --git a/docs/library/reference/qref.md b/docs/library/reference/qref.md index 2eda782..50671bd 100644 --- a/docs/library/reference/qref.md +++ b/docs/library/reference/qref.md @@ -1,6 +1,5 @@ ::: qref handler: python options: - members: - - generate_program_schema - - SchemaV1 + filters: + - "!__all__" \ No newline at end of file diff --git a/docs/library/reference/qref.schema_v1.md b/docs/library/reference/qref.schema_v1.md new file mode 100644 index 0000000..63ae471 --- /dev/null +++ b/docs/library/reference/qref.schema_v1.md @@ -0,0 +1,6 @@ +::: qref.schema_v1 + handler: python + options: + filters: + - "!^_[^_]" + - "!SchemaV1" diff --git a/mkdocs.yml b/mkdocs.yml index c75a8e2..5f5bcbf 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -11,6 +11,7 @@ nav: - library/userguide.md - API Reference: - qref: library/reference/qref.md + - qref.schema_v1: library/reference/qref.schema_v1.md - qref.experimental.rendering: library/reference/qref.experimental.rendering.md - development.md theme: diff --git a/pyproject.toml b/pyproject.toml index 8789283..c00d85b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -44,7 +44,7 @@ build-backend = "poetry_dynamic_versioning.backend" [tool.black] -line-length = 100 +line-length = 120 target-version = ['py39'] diff --git a/src/qref/__init__.py b/src/qref/__init__.py index 58509a2..3a7a3a3 100644 --- a/src/qref/__init__.py +++ b/src/qref/__init__.py @@ -16,7 +16,8 @@ from typing import Any -from ._schema_v1 import SchemaV1, generate_schema_v1 +from .schema_v1 import SchemaV1, generate_schema_v1 +from .verification import verify_topology SCHEMA_GENERATORS = {"v1": generate_schema_v1} MODELS = {"v1": SchemaV1} @@ -41,4 +42,4 @@ def generate_program_schema(version: str = LATEST_SCHEMA_VERSION) -> dict[str, A raise ValueError(f"Unknown schema version {version}") -__all__ = ["generate_program_schema", "SchemaV1"] +__all__ = ["generate_program_schema", "SchemaV1", "verify_topology"] diff --git a/src/qref/experimental/rendering.py b/src/qref/experimental/rendering.py index d5d25e7..aa89aa6 100644 --- a/src/qref/experimental/rendering.py +++ b/src/qref/experimental/rendering.py @@ -90,9 +90,7 @@ def _format_node_name(node_name, parent): 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: + 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) @@ -114,9 +112,7 @@ 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: + 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") @@ -161,9 +157,7 @@ def to_graphviz(data: Union[dict, SchemaV1]) -> graphviz.Digraph: 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("input", help="Path to the YAML or JSON file with Routine in V1 schema", type=Path) parser.add_argument( "output", help=( diff --git a/src/qref/_schema_v1.py b/src/qref/schema_v1.py similarity index 65% rename from src/qref/_schema_v1.py rename to src/qref/schema_v1.py index 64866c7..10cc7ce 100644 --- a/src/qref/_schema_v1.py +++ b/src/qref/schema_v1.py @@ -29,52 +29,64 @@ from pydantic.json_schema import GenerateJsonSchema NAME_PATTERN = "[A-Za-z_][A-Za-z0-9_]*" -NAMESPACED_NAME_PATTERN = rf"{NAME_PATTERN}\.{NAME_PATTERN}" - -Name = Annotated[str, StringConstraints(pattern=rf"^{NAME_PATTERN}$")] -NamespacedName = Annotated[str, StringConstraints(pattern=rf"^{NAMESPACED_NAME_PATTERN}")] -OptionallyNamespacedName = Annotated[ - str, StringConstraints(pattern=rf"^(({NAME_PATTERN})|({NAMESPACED_NAME_PATTERN}))$") +OPTIONALLY_NAMESPACED_NAME_PATTERN = rf"^({NAME_PATTERN}\.)?{NAME_PATTERN}$" +MULTINAMESPACED_NAME_PATTERN = rf"^({NAME_PATTERN}\.)+{NAME_PATTERN}$" +OPTIONALLY_MULTINAMESPACED_NAME_PATTERN = rf"^({NAME_PATTERN}\.)*{NAME_PATTERN}$" + +_Name = Annotated[str, StringConstraints(pattern=rf"^{NAME_PATTERN}$")] +_OptionallyNamespacedName = Annotated[str, StringConstraints(pattern=rf"{OPTIONALLY_NAMESPACED_NAME_PATTERN}")] +_MultiNamespacedName = Annotated[str, StringConstraints(pattern=rf"{MULTINAMESPACED_NAME_PATTERN}")] +_OptionallyMultiNamespacedName = Annotated[ + str, StringConstraints(pattern=rf"{OPTIONALLY_MULTINAMESPACED_NAME_PATTERN}") ] + _Value = Union[int, float, str] -def sorter(key): +def _sorter(key): def _inner(v): return sorted(v, key=key) return _inner -name_sorter = AfterValidator(sorter(lambda p: p.name)) -source_sorter = AfterValidator(sorter(lambda c: c.source)) +_name_sorter = AfterValidator(_sorter(lambda p: p.name)) +_source_sorter = AfterValidator(_sorter(lambda c: c.source)) + +class PortV1(BaseModel): + """Description of Port in V1 schema""" -class _PortV1(BaseModel): - name: Name + name: _Name direction: Literal["input", "output", "through"] size: Optional[_Value] model_config = ConfigDict(title="Port") -class _ConnectionV1(BaseModel): - source: OptionallyNamespacedName - target: OptionallyNamespacedName +class ConnectionV1(BaseModel): + """Description of Connection in V1 schema""" + + source: _OptionallyNamespacedName + target: _OptionallyNamespacedName model_config = ConfigDict(title="Connection", use_enum_values=True) -class _ResourceV1(BaseModel): - name: Name +class ResourceV1(BaseModel): + """Description of Resource in V1 schema""" + + name: _Name type: Literal["additive", "multiplicative", "qubits", "other"] value: Union[int, float, str, None] model_config = ConfigDict(title="Resource") -class _ParamLinkV1(BaseModel): - source: Name - targets: list[NamespacedName] +class ParamLinkV1(BaseModel): + """Description of Parameter link in V1 schema""" + + source: _OptionallyNamespacedName + targets: list[_MultiNamespacedName] model_config = ConfigDict(title="ParamLink") @@ -87,15 +99,15 @@ class RoutineV1(BaseModel): SchemaV1. """ - name: Name - children: Annotated[list[RoutineV1], name_sorter] = Field(default_factory=list) + name: _Name + children: Annotated[list[RoutineV1], _name_sorter] = Field(default_factory=list) type: Optional[str] = None - ports: Annotated[list[_PortV1], name_sorter] = Field(default_factory=list) - resources: Annotated[list[_ResourceV1], name_sorter] = Field(default_factory=list) - connections: Annotated[list[_ConnectionV1], source_sorter] = Field(default_factory=list) - input_params: list[Name] = Field(default_factory=list) + ports: Annotated[list[PortV1], _name_sorter] = Field(default_factory=list) + resources: Annotated[list[ResourceV1], _name_sorter] = Field(default_factory=list) + connections: Annotated[list[ConnectionV1], _source_sorter] = Field(default_factory=list) + input_params: list[_OptionallyMultiNamespacedName] = Field(default_factory=list) local_variables: list[str] = Field(default_factory=list) - linked_params: Annotated[list[_ParamLinkV1], source_sorter] = Field(default_factory=list) + linked_params: Annotated[list[ParamLinkV1], _source_sorter] = Field(default_factory=list) meta: dict[str, Any] = Field(default_factory=dict) model_config = ConfigDict(title="Routine") @@ -105,11 +117,9 @@ def __init__(self, **data: Any): @field_validator("connections", mode="after") @classmethod - def _validate_connections(cls, v, values) -> list[_ConnectionV1]: + def _validate_connections(cls, v, values) -> list[ConnectionV1]: children_port_names = [ - f"{child.name}.{port.name}" - for child in values.data.get("children") - for port in child.ports + f"{child.name}.{port.name}" for child in values.data.get("children") for port in child.ports ] parent_port_names = [port.name for port in values.data["ports"]] available_port_names = set(children_port_names + parent_port_names) diff --git a/src/qref/verification.py b/src/qref/verification.py index 1d899a1..701846d 100644 --- a/src/qref/verification.py +++ b/src/qref/verification.py @@ -16,7 +16,7 @@ from dataclasses import dataclass from typing import Optional, Union -from ._schema_v1 import RoutineV1, SchemaV1 +from .schema_v1 import RoutineV1, SchemaV1 @dataclass @@ -36,6 +36,8 @@ def __bool__(self) -> bool: def verify_topology(routine: Union[SchemaV1, RoutineV1]) -> TopologyVerificationOutput: """Checks whether program has correct topology. + Correct topology cannot include cycles or disconnected ports. + Args: routine: Routine or program to be verified. """ @@ -58,9 +60,7 @@ def _verify_routine_topology(routine: RoutineV1) -> list[str]: return problems -def _get_adjacency_list_from_routine( - routine: RoutineV1, path: Optional[str] -) -> dict[str, list[str]]: +def _get_adjacency_list_from_routine(routine: RoutineV1, path: Optional[str]) -> dict[str, list[str]]: """This function creates a flat graph representing one hierarchy level of a routine. Nodes represent ports and edges represent connections (they're directed). @@ -135,17 +135,13 @@ def _find_disconnected_ports(routine: RoutineV1): for port in child.ports: pname = f"{routine.name}.{child.name}.{port.name}" if port.direction == "input": - matches_in = [ - c for c in routine.connections if c.target == f"{child.name}.{port.name}" - ] + matches_in = [c for c in routine.connections if c.target == f"{child.name}.{port.name}"] if len(matches_in) == 0: problems.append(f"No incoming connections to {pname}.") elif len(matches_in) > 1: problems.append(f"Too many incoming connections to {pname}.") elif port.direction == "output": - matches_out = [ - c for c in routine.connections if c.source == f"{child.name}.{port.name}" - ] + matches_out = [c for c in routine.connections if c.source == f"{child.name}.{port.name}"] if len(matches_out) == 0: problems.append(f"No outgoing connections from {pname}.") elif len(matches_out) > 1: diff --git a/tests/qref/data/invalid_yaml_programs.yaml b/tests/qref/data/invalid_yaml_programs.yaml index a700d2c..0062dc5 100644 --- a/tests/qref/data/invalid_yaml_programs.yaml +++ b/tests/qref/data/invalid_yaml_programs.yaml @@ -194,7 +194,7 @@ target: bar.in_0 description: "Connections have more than one namespace" error_path: "$.program.connections[0].source" - error_message: "'foo.foo.out_0' does not match '^(([A-Za-z_][A-Za-z0-9_]*)|([A-Za-z_][A-Za-z0-9_]*\\\\.[A-Za-z_][A-Za-z0-9_]*))$'" + error_message: "'foo.foo.out_0' does not match '^([A-Za-z_][A-Za-z0-9_]*\\\\.)?[A-Za-z_][A-Za-z0-9_]*$'" - input: version: v1 program: @@ -204,7 +204,7 @@ - "my-input-param" description: "Input param has invalid name" error_path: "$.program.input_params[1]" - error_message: "'my-input-param' does not match '^[A-Za-z_][A-Za-z0-9_]*$'" + error_message: "'my-input-param' does not match '^([A-Za-z_][A-Za-z0-9_]*\\\\.)*[A-Za-z_][A-Za-z0-9_]*$'" - input: version: v1 program: @@ -212,22 +212,6 @@ description: "Program has an empty name" error_path: "$.program.name" error_message: "'' does not match '^[A-Za-z_][A-Za-z0-9_]*$'" -- input: - version: v1 - program: - name: "root" - input_params: - - N - linked_params: - - source: foo.N - targets: [foo.N] - children: - - name: foo - input_params: - - N - description: Source of a paramater link is namespaced - error_path: "$.program.linked_params[0].source" - error_message: "'foo.N' does not match '^[A-Za-z_][A-Za-z0-9_]*$'" - input: version: v1 program: @@ -243,5 +227,5 @@ - N description: "Target of a paramater link is not namespaced" error_path: "$.program.linked_params[0].targets[0]" - error_message: "'N' does not match '^[A-Za-z_][A-Za-z0-9_]*\\\\.[A-Za-z_][A-Za-z0-9_]*'" + error_message: "'N' does not match '^([A-Za-z_][A-Za-z0-9_]*\\\\.)+[A-Za-z_][A-Za-z0-9_]*$'" \ No newline at end of file