qevedo-compiler 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (29) hide show
  1. qevedo_compiler-0.1.0/PKG-INFO +68 -0
  2. qevedo_compiler-0.1.0/README.md +54 -0
  3. qevedo_compiler-0.1.0/pyproject.toml +38 -0
  4. qevedo_compiler-0.1.0/qevedo/compiler/__init__.py +19 -0
  5. qevedo_compiler-0.1.0/qevedo/compiler/cli.py +79 -0
  6. qevedo_compiler-0.1.0/qevedo/compiler/compiler.py +51 -0
  7. qevedo_compiler-0.1.0/qevedo/compiler/device.py +84 -0
  8. qevedo_compiler-0.1.0/qevedo/compiler/emit/__init__.py +0 -0
  9. qevedo_compiler-0.1.0/qevedo/compiler/emit/qasm.py +83 -0
  10. qevedo_compiler-0.1.0/qevedo/compiler/frontend/__init__.py +0 -0
  11. qevedo_compiler-0.1.0/qevedo/compiler/frontend/qasm.py +346 -0
  12. qevedo_compiler-0.1.0/qevedo/compiler/ir.py +107 -0
  13. qevedo_compiler-0.1.0/qevedo/compiler/passes/__init__.py +12 -0
  14. qevedo_compiler-0.1.0/qevedo/compiler/passes/base.py +40 -0
  15. qevedo_compiler-0.1.0/qevedo/compiler/passes/decompose.py +82 -0
  16. qevedo_compiler-0.1.0/qevedo/compiler/passes/optimize.py +52 -0
  17. qevedo_compiler-0.1.0/qevedo/compiler/profiles/all_to_all.yaml +11 -0
  18. qevedo_compiler-0.1.0/qevedo/compiler/profiles/grid_2x4.yaml +20 -0
  19. qevedo_compiler-0.1.0/qevedo/compiler/profiles/linear_chain.yaml +25 -0
  20. qevedo_compiler-0.1.0/qevedo_compiler.egg-info/PKG-INFO +68 -0
  21. qevedo_compiler-0.1.0/qevedo_compiler.egg-info/SOURCES.txt +27 -0
  22. qevedo_compiler-0.1.0/qevedo_compiler.egg-info/dependency_links.txt +1 -0
  23. qevedo_compiler-0.1.0/qevedo_compiler.egg-info/entry_points.txt +2 -0
  24. qevedo_compiler-0.1.0/qevedo_compiler.egg-info/requires.txt +5 -0
  25. qevedo_compiler-0.1.0/qevedo_compiler.egg-info/top_level.txt +1 -0
  26. qevedo_compiler-0.1.0/setup.cfg +4 -0
  27. qevedo_compiler-0.1.0/tests/test_compile.py +48 -0
  28. qevedo_compiler-0.1.0/tests/test_frontend.py +171 -0
  29. qevedo_compiler-0.1.0/tests/test_integration.py +24 -0
@@ -0,0 +1,68 @@
1
+ Metadata-Version: 2.4
2
+ Name: qevedo-compiler
3
+ Version: 0.1.0
4
+ Summary: QCC, the Quantum Compiler Collection: a technology-agnostic quantum circuit compiler (qevedo.compiler)
5
+ License-Expression: Apache-2.0
6
+ Project-URL: Homepage, https://github.com/qevedo/qevedo-compiler
7
+ Project-URL: Repository, https://github.com/qevedo/qevedo-compiler
8
+ Requires-Python: >=3.10
9
+ Description-Content-Type: text/markdown
10
+ Requires-Dist: PyYAML>=6.0
11
+ Requires-Dist: openqasm<4,>=3
12
+ Provides-Extra: dev
13
+ Requires-Dist: pytest>=7.0; extra == "dev"
14
+
15
+ # QCC: the Quantum Compiler Collection
16
+
17
+ A technology-agnostic quantum circuit compiler, part of the Qevedo packages:
18
+ it installs as `qevedo-compiler` and imports as `qevedo.compiler`.
19
+
20
+ ## Install
21
+
22
+ ```bash
23
+ pip install qevedo-compiler
24
+ ```
25
+
26
+ For development, from a checkout:
27
+
28
+ ```bash
29
+ cd qcc/python
30
+ pip install -e ".[dev]"
31
+ ```
32
+
33
+ `openqasm` 3.x is installed from PyPI. To develop both together, also install
34
+ the sibling checkout: `pip install -e ../../openqasm`.
35
+
36
+ ## Usage
37
+
38
+ ```bash
39
+ qcc compile examples/bell.qasm -o /tmp/out.qasm --device qevedo/compiler/profiles/all_to_all.yaml
40
+ qcc compile program.qasm -o /tmp/out.qasm --qasm-version 3 -I lib/
41
+ ```
42
+
43
+ From Python:
44
+
45
+ ```python
46
+ from qevedo.compiler import compile_source, emit_qasm
47
+
48
+ circuit = compile_source(open("examples/bell.qasm").read())
49
+ print(emit_qasm(circuit))
50
+ ```
51
+
52
+ The reference device profiles ship with the package, in
53
+ `qevedo/compiler/profiles/`.
54
+
55
+ Input can be OpenQASM 2 or 3. Programs are checked by `openqasm.analyze`
56
+ before lowering, so errors are reported with their location. The frontend
57
+ supports qubit and bit registers, gate calls with constant parameters
58
+ (broadcast over registers), measurements, resets, barriers, `let` aliases of
59
+ qubits and `const` values. Control flow, gate modifiers, classical variables,
60
+ timing and hardware qubits are rejected with an error until the IR supports
61
+ them. Gate definitions are not inlined: gates stay named instructions for the
62
+ decomposition pass.
63
+
64
+ ## Tests
65
+
66
+ ```bash
67
+ pytest
68
+ ```
@@ -0,0 +1,54 @@
1
+ # QCC: the Quantum Compiler Collection
2
+
3
+ A technology-agnostic quantum circuit compiler, part of the Qevedo packages:
4
+ it installs as `qevedo-compiler` and imports as `qevedo.compiler`.
5
+
6
+ ## Install
7
+
8
+ ```bash
9
+ pip install qevedo-compiler
10
+ ```
11
+
12
+ For development, from a checkout:
13
+
14
+ ```bash
15
+ cd qcc/python
16
+ pip install -e ".[dev]"
17
+ ```
18
+
19
+ `openqasm` 3.x is installed from PyPI. To develop both together, also install
20
+ the sibling checkout: `pip install -e ../../openqasm`.
21
+
22
+ ## Usage
23
+
24
+ ```bash
25
+ qcc compile examples/bell.qasm -o /tmp/out.qasm --device qevedo/compiler/profiles/all_to_all.yaml
26
+ qcc compile program.qasm -o /tmp/out.qasm --qasm-version 3 -I lib/
27
+ ```
28
+
29
+ From Python:
30
+
31
+ ```python
32
+ from qevedo.compiler import compile_source, emit_qasm
33
+
34
+ circuit = compile_source(open("examples/bell.qasm").read())
35
+ print(emit_qasm(circuit))
36
+ ```
37
+
38
+ The reference device profiles ship with the package, in
39
+ `qevedo/compiler/profiles/`.
40
+
41
+ Input can be OpenQASM 2 or 3. Programs are checked by `openqasm.analyze`
42
+ before lowering, so errors are reported with their location. The frontend
43
+ supports qubit and bit registers, gate calls with constant parameters
44
+ (broadcast over registers), measurements, resets, barriers, `let` aliases of
45
+ qubits and `const` values. Control flow, gate modifiers, classical variables,
46
+ timing and hardware qubits are rejected with an error until the IR supports
47
+ them. Gate definitions are not inlined: gates stay named instructions for the
48
+ decomposition pass.
49
+
50
+ ## Tests
51
+
52
+ ```bash
53
+ pytest
54
+ ```
@@ -0,0 +1,38 @@
1
+ [build-system]
2
+ requires = ["setuptools>=77"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "qevedo-compiler"
7
+ version = "0.1.0"
8
+ description = "QCC, the Quantum Compiler Collection: a technology-agnostic quantum circuit compiler (qevedo.compiler)"
9
+ readme = "README.md"
10
+ license = "Apache-2.0"
11
+ requires-python = ">=3.10"
12
+ dependencies = [
13
+ "PyYAML>=6.0",
14
+ "openqasm>=3,<4",
15
+ ]
16
+
17
+ [project.optional-dependencies]
18
+ dev = ["pytest>=7.0"]
19
+
20
+ [project.scripts]
21
+ qcc = "qevedo.compiler.cli:main"
22
+
23
+ [project.urls]
24
+ Homepage = "https://github.com/qevedo/qevedo-compiler"
25
+ Repository = "https://github.com/qevedo/qevedo-compiler"
26
+
27
+ # `qevedo` is a namespace package (no __init__.py) shared with the other
28
+ # Qevedo packages, such as qevedo-simulator.
29
+ [tool.setuptools.packages.find]
30
+ where = ["."]
31
+ include = ["qevedo.compiler*"]
32
+ namespaces = true
33
+
34
+ [tool.setuptools.package-data]
35
+ "qevedo.compiler" = ["profiles/*.yaml"]
36
+
37
+ [tool.pytest.ini_options]
38
+ testpaths = ["tests"]
@@ -0,0 +1,19 @@
1
+ """QCC, the Quantum Compiler Collection (`qevedo.compiler`)."""
2
+
3
+ from qevedo.compiler.compiler import CompileOptions, compile_circuit, compile_file, compile_source
4
+ from qevedo.compiler.emit.qasm import emit_qasm
5
+ from qevedo.compiler.frontend.qasm import QasmFrontendError, parse_qasm
6
+ from qevedo.compiler.ir import Circuit, Instruction
7
+
8
+ __all__ = [
9
+ "Circuit",
10
+ "Instruction",
11
+ "CompileOptions",
12
+ "QasmFrontendError",
13
+ "compile_circuit",
14
+ "compile_file",
15
+ "compile_source",
16
+ "emit_qasm",
17
+ "parse_qasm",
18
+ ]
19
+ __version__ = "0.1.0"
@@ -0,0 +1,79 @@
1
+ """QCC command-line interface."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ import sys
7
+ from pathlib import Path
8
+
9
+ from qevedo.compiler.compiler import CompileOptions, compile_file
10
+ from qevedo.compiler.device import DeviceSpec, default_device
11
+ from qevedo.compiler.emit.qasm import emit_qasm
12
+ from qevedo.compiler.frontend.qasm import QasmFrontendError
13
+
14
+
15
+ def _profiles_dir() -> Path:
16
+ return Path(__file__).resolve().parent / "profiles"
17
+
18
+
19
+ def main(argv: list[str] | None = None) -> int:
20
+ parser = argparse.ArgumentParser(prog="qcc", description="Quantum Compiler Collection")
21
+ sub = parser.add_subparsers(dest="command", required=True)
22
+
23
+ compile_cmd = sub.add_parser("compile", help="Compile an OpenQASM 2 or 3 circuit")
24
+ compile_cmd.add_argument("input", type=Path, help="Input .qasm file")
25
+ compile_cmd.add_argument("-o", "--output", type=Path, required=True, help="Output .qasm file")
26
+ compile_cmd.add_argument(
27
+ "--device",
28
+ type=Path,
29
+ default=None,
30
+ help="Device profile YAML (default: built-in all-to-all)",
31
+ )
32
+ compile_cmd.add_argument(
33
+ "--qasm-version",
34
+ choices=["2", "3"],
35
+ default="2",
36
+ help="OpenQASM version of the output (default: 2)",
37
+ )
38
+ compile_cmd.add_argument(
39
+ "-I",
40
+ "--include",
41
+ action="append",
42
+ default=[],
43
+ type=Path,
44
+ metavar="DIR",
45
+ help="Directory searched by include statements (repeatable)",
46
+ )
47
+ compile_cmd.add_argument(
48
+ "--stats",
49
+ action="store_true",
50
+ help="Print gate counts and depth to stderr",
51
+ )
52
+
53
+ args = parser.parse_args(argv)
54
+ if args.command == "compile":
55
+ return _run_compile(args)
56
+ return 1
57
+
58
+
59
+ def _run_compile(args: argparse.Namespace) -> int:
60
+ device = DeviceSpec.from_yaml(args.device) if args.device else default_device()
61
+ try:
62
+ options = CompileOptions(device=device, include_paths=args.include)
63
+ compiled = compile_file(args.input, options)
64
+ except QasmFrontendError as error:
65
+ print(f"qcc: {error}", file=sys.stderr)
66
+ return 1
67
+ output = emit_qasm(compiled, version=f"{args.qasm_version}.0")
68
+ args.output.write_text(output + "\n", encoding="utf-8")
69
+ if args.stats:
70
+ counts = compiled.gate_counts()
71
+ print(f"device: {device.name}", file=sys.stderr)
72
+ print(f"depth: {compiled.depth()}", file=sys.stderr)
73
+ print(f"2q_gates: {compiled.two_qubit_gate_count()}", file=sys.stderr)
74
+ print(f"gates: {counts}", file=sys.stderr)
75
+ return 0
76
+
77
+
78
+ if __name__ == "__main__":
79
+ raise SystemExit(main())
@@ -0,0 +1,51 @@
1
+ """End-to-end compilation driver."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Sequence
6
+ from dataclasses import dataclass, field
7
+ from pathlib import Path
8
+
9
+ from qevedo.compiler.device import DeviceSpec, default_device
10
+ from qevedo.compiler.emit.qasm import emit_qasm
11
+ from qevedo.compiler.frontend.qasm import parse_qasm
12
+ from qevedo.compiler.ir import Circuit
13
+ from qevedo.compiler.passes.base import PassManager
14
+
15
+
16
+ @dataclass
17
+ class CompileOptions:
18
+ device: DeviceSpec | None = None
19
+ pass_manager: PassManager | None = None
20
+ #: Directories searched by ``include`` statements, after the input's directory.
21
+ include_paths: Sequence[str | Path] = field(default_factory=tuple)
22
+
23
+
24
+ def compile_circuit(circuit: Circuit, options: CompileOptions | None = None) -> Circuit:
25
+ opts = options or CompileOptions()
26
+ device = opts.device or default_device()
27
+ manager = opts.pass_manager or PassManager.default(device)
28
+ return manager.run(circuit, device)
29
+
30
+
31
+ def compile_file(input_path: str | Path, options: CompileOptions | None = None) -> Circuit:
32
+ """Compile an OpenQASM 2 or 3 file."""
33
+ opts = options or CompileOptions()
34
+ circuit = parse_qasm(path=input_path, include_paths=opts.include_paths)
35
+ return compile_circuit(circuit, opts)
36
+
37
+
38
+ def compile_source(source: str, options: CompileOptions | None = None) -> Circuit:
39
+ """Compile OpenQASM 2 or 3 source text."""
40
+ opts = options or CompileOptions()
41
+ circuit = parse_qasm(source=source, include_paths=opts.include_paths)
42
+ return compile_circuit(circuit, opts)
43
+
44
+
45
+ def compile_to_qasm(
46
+ input_path: str | Path,
47
+ options: CompileOptions | None = None,
48
+ version: str = "2.0",
49
+ ) -> str:
50
+ """Compile a file and return the result as OpenQASM ``version`` source."""
51
+ return emit_qasm(compile_file(input_path, options), version=version)
@@ -0,0 +1,84 @@
1
+ """Technology-agnostic backend description."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass, field
6
+ from pathlib import Path
7
+ from typing import Dict, Iterable, List, Optional, Set, Tuple
8
+
9
+ import yaml
10
+
11
+
12
+ @dataclass
13
+ class DeviceSpec:
14
+ """Describes qubit connectivity and native gates for a target."""
15
+
16
+ name: str
17
+ topology: str
18
+ qubits: int
19
+ edges: List[Tuple[int, int]] = field(default_factory=list)
20
+ native_gates: Set[str] = field(default_factory=set)
21
+ directed_edges: bool = False
22
+ max_parallel_2q: int = 1
23
+ gate_rules: Dict[str, dict] = field(default_factory=dict)
24
+
25
+ @classmethod
26
+ def from_dict(cls, data: dict) -> DeviceSpec:
27
+ edges = [tuple(edge) for edge in data.get("edges", [])]
28
+ topology = data.get("topology", "coupling_graph")
29
+ qubits = int(data["qubits"])
30
+ if topology == "all_to_all" and not edges:
31
+ edges = [(i, j) for i in range(qubits) for j in range(i + 1, qubits)]
32
+ native = {gate.lower() for gate in data.get("native_gates", [])}
33
+ return cls(
34
+ name=data["name"],
35
+ topology=topology,
36
+ qubits=qubits,
37
+ edges=edges,
38
+ native_gates=native,
39
+ directed_edges=bool(data.get("directed_edges", False)),
40
+ max_parallel_2q=int(data.get("max_parallel_2q", 1)),
41
+ gate_rules=data.get("gate_rules", {}),
42
+ )
43
+
44
+ @classmethod
45
+ def from_yaml(cls, path: str | Path) -> DeviceSpec:
46
+ with open(path, encoding="utf-8") as handle:
47
+ data = yaml.safe_load(handle)
48
+ return cls.from_dict(data)
49
+
50
+ def adjacency(self) -> Dict[int, Set[int]]:
51
+ graph: Dict[int, Set[int]] = {q: set() for q in range(self.qubits)}
52
+ for a, b in self.edges:
53
+ graph.setdefault(a, set()).add(b)
54
+ if not self.directed_edges:
55
+ graph.setdefault(b, set()).add(a)
56
+ return graph
57
+
58
+ def supports_gate(self, gate: str, qubits: Iterable[int]) -> bool:
59
+ gate = gate.lower()
60
+ if gate not in self.native_gates:
61
+ return False
62
+ if gate not in self.gate_rules:
63
+ return True
64
+ rule = self.gate_rules[gate]
65
+ placement = rule.get("placement") or rule.get("on")
66
+ if placement == "edge" and len(list(qubits)) == 2:
67
+ pair = tuple(qubits)
68
+ if self.directed_edges:
69
+ return pair in self.edges
70
+ return pair in self.edges or (pair[1], pair[0]) in self.edges
71
+ return True
72
+
73
+
74
+ def default_device() -> DeviceSpec:
75
+ """All-to-all superconducting-style device with a common IBM-like gateset."""
76
+ qubits = 32
77
+ edges = [(i, j) for i in range(qubits) for j in range(i + 1, qubits)]
78
+ return DeviceSpec(
79
+ name="all_to_all_32",
80
+ topology="all_to_all",
81
+ qubits=qubits,
82
+ edges=edges,
83
+ native_gates={"rz", "sx", "x", "cx"},
84
+ )
File without changes
@@ -0,0 +1,83 @@
1
+ """QCC IR → OpenQASM 2 or 3, printed by the ``openqasm`` package (3.x)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import math
6
+ from fractions import Fraction
7
+
8
+ import openqasm
9
+ from openqasm import ast
10
+
11
+ from qevedo.compiler.ir import Circuit, Clbit, Instruction, Qubit
12
+
13
+ __all__ = ["emit_qasm"]
14
+
15
+ # IR gate names that are spelled differently in OpenQASM 3's stdgates.inc.
16
+ _OPENQASM3_NAMES = {"u": "U"}
17
+
18
+
19
+ def emit_qasm(circuit: Circuit, version: str = "2.0", include_barriers: bool = True) -> str:
20
+ """Serialize ``circuit`` as an OpenQASM ``version`` ("2.0" or "3.0") program.
21
+
22
+ Gates keep their IR names and come from ``qelib1.inc`` (OpenQASM 2) or
23
+ ``stdgates.inc`` (OpenQASM 3).
24
+ """
25
+ if version not in ("2.0", "3.0"):
26
+ raise ValueError(f"unsupported OpenQASM version {version!r}")
27
+ openqasm3 = version == "3.0"
28
+ statements: list[ast.Statement] = [ast.Include("stdgates.inc" if openqasm3 else "qelib1.inc")]
29
+ for name, size in circuit.qreg_sizes.items():
30
+ statements.append(ast.QubitDeclaration(ast.Identifier(name), ast.IntegerLiteral(size)))
31
+ for name, size in circuit.creg_sizes.items():
32
+ statements.append(
33
+ ast.ClassicalDeclaration(ast.BitType(ast.IntegerLiteral(size)), ast.Identifier(name))
34
+ )
35
+ for inst in circuit.instructions:
36
+ statement = _statement(inst, openqasm3, include_barriers)
37
+ if statement is not None:
38
+ statements.append(statement)
39
+ # The printer uses OpenQASM 2 syntax (qreg, creg, measure ->) for version 2.0.
40
+ return openqasm.dumps(ast.Program(statements, version=version)).rstrip("\n")
41
+
42
+
43
+ def _statement(inst: Instruction, openqasm3: bool, include_barriers: bool) -> ast.Statement | None:
44
+ name = inst.name.lower()
45
+ if name == "barrier":
46
+ return ast.Barrier([_bit(q) for q in inst.qubits]) if include_barriers else None
47
+ if name == "measure":
48
+ measure = ast.MeasureExpression(_bit(inst.qubits[0]))
49
+ return ast.MeasureStatement(measure, _bit(inst.clbits[0]))
50
+ if name == "reset":
51
+ return ast.Reset(_bit(inst.qubits[0]))
52
+ name = _OPENQASM3_NAMES.get(name, inst.name) if openqasm3 else inst.name
53
+ return ast.GateCall(
54
+ [], ast.Identifier(name), [_angle(p) for p in inst.params], [_bit(q) for q in inst.qubits]
55
+ )
56
+
57
+
58
+ def _bit(bit: Qubit | Clbit) -> ast.IndexedIdentifier:
59
+ return ast.IndexedIdentifier(ast.Identifier(bit.register), [[ast.IntegerLiteral(bit.index)]])
60
+
61
+
62
+ def _angle(value: float) -> ast.Expression:
63
+ """``value`` as a readable expression: a small rational multiple of pi when it is one."""
64
+ ratio = Fraction(value / math.pi).limit_denominator(16)
65
+ if ratio and abs(float(ratio) * math.pi - value) < 1e-12:
66
+ # Build "-3 * pi / 4" rather than "-(3 * pi / 4)".
67
+ expression: ast.Expression = ast.Identifier("pi")
68
+ numerator = abs(ratio.numerator)
69
+ if numerator != 1:
70
+ factor: ast.Expression = ast.IntegerLiteral(numerator)
71
+ if ratio < 0:
72
+ factor = ast.UnaryExpression(ast.UnaryOperator.NEGATE, factor)
73
+ expression = ast.BinaryExpression(ast.BinaryOperator.MULTIPLY, factor, expression)
74
+ elif ratio < 0:
75
+ expression = ast.UnaryExpression(ast.UnaryOperator.NEGATE, expression)
76
+ if ratio.denominator != 1:
77
+ expression = ast.BinaryExpression(
78
+ ast.BinaryOperator.DIVIDE, expression, ast.IntegerLiteral(ratio.denominator)
79
+ )
80
+ return expression
81
+ if value < 0:
82
+ return ast.UnaryExpression(ast.UnaryOperator.NEGATE, ast.FloatLiteral(-value))
83
+ return ast.FloatLiteral(value)