openqml 0.3.0a8__py3-none-any.whl
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.
Potentially problematic release.
This version of openqml might be problematic. Click here for more details.
- openqml/__init__.py +161 -0
- openqml/__main__.py +3 -0
- openqml/_native_verification.py +260 -0
- openqml/_qasm.py +176 -0
- openqml/_scheduling.py +261 -0
- openqml/_serialization.py +124 -0
- openqml/_validation.py +124 -0
- openqml/adapters/__init__.py +1 -0
- openqml/adapters/_cirq_control.py +217 -0
- openqml/adapters/_common.py +106 -0
- openqml/adapters/cirq.py +406 -0
- openqml/adapters/openqasm.py +552 -0
- openqml/adapters/pennylane.py +242 -0
- openqml/adapters/qiskit.py +519 -0
- openqml/adjoint.py +484 -0
- openqml/aer.py +290 -0
- openqml/backends.py +266 -0
- openqml/batch.py +388 -0
- openqml/cli.py +169 -0
- openqml/compiler.py +433 -0
- openqml/conversion.py +692 -0
- openqml/density.py +503 -0
- openqml/errors.py +46 -0
- openqml/execution.py +765 -0
- openqml/frontends.py +102 -0
- openqml/gradients.py +226 -0
- openqml/interfaces/__init__.py +1 -0
- openqml/interfaces/_jax_ad.py +248 -0
- openqml/interfaces/_spec.py +411 -0
- openqml/interfaces/_torch_ad.py +140 -0
- openqml/interfaces/flax.py +154 -0
- openqml/interfaces/jax.py +308 -0
- openqml/interfaces/torch.py +403 -0
- openqml/ir.py +697 -0
- openqml/layers.py +516 -0
- openqml/measurement.py +255 -0
- openqml/observables.py +35 -0
- openqml/optimization.py +237 -0
- openqml/parameters.py +362 -0
- openqml/pauli.py +510 -0
- openqml/planning.py +232 -0
- openqml/program.py +225 -0
- openqml/providers.py +291 -0
- openqml/py.typed +0 -0
- openqml/readout.py +160 -0
- openqml/simulator.py +787 -0
- openqml/templates.py +56 -0
- openqml/verification.py +382 -0
- openqml-0.3.0a8.dist-info/METADATA +116 -0
- openqml-0.3.0a8.dist-info/RECORD +54 -0
- openqml-0.3.0a8.dist-info/WHEEL +5 -0
- openqml-0.3.0a8.dist-info/entry_points.txt +2 -0
- openqml-0.3.0a8.dist-info/licenses/LICENSE +202 -0
- openqml-0.3.0a8.dist-info/top_level.txt +1 -0
openqml/__init__.py
ADDED
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
"""Hybrid quantum learning and explicit circuit interoperability.
|
|
2
|
+
|
|
3
|
+
Import once as ``import openqml as oq``. Optional integrations are available
|
|
4
|
+
through ``oq.torch``, ``oq.jax`` and ``oq.flax`` and load only when accessed.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from importlib import import_module as _import_module
|
|
8
|
+
|
|
9
|
+
from ._scheduling import ExecutionBatch, ReadoutRoute
|
|
10
|
+
from .adjoint import AdjointPlan, adjoint_jvp, adjoint_vjp
|
|
11
|
+
from .aer import AerBackend
|
|
12
|
+
from .backends import (
|
|
13
|
+
BackendCapabilities,
|
|
14
|
+
DensityMatrixBackend,
|
|
15
|
+
ExecutionOptions,
|
|
16
|
+
ExpectationBackend,
|
|
17
|
+
SamplingBackend,
|
|
18
|
+
StatevectorBackend,
|
|
19
|
+
get_backend,
|
|
20
|
+
list_backends,
|
|
21
|
+
register_backend,
|
|
22
|
+
unregister_backend,
|
|
23
|
+
)
|
|
24
|
+
from .batch import execute_batch, expectations_batch
|
|
25
|
+
from .compiler import CircuitStats, circuit_stats, decompose, inverse, simplify
|
|
26
|
+
from .conversion import ConversionPlan, ConversionResult, convert, diagnose, plan_conversion
|
|
27
|
+
from .density import density_matrix
|
|
28
|
+
from .errors import (
|
|
29
|
+
BackendError,
|
|
30
|
+
ConversionError,
|
|
31
|
+
Diagnostic,
|
|
32
|
+
MissingDependencyError,
|
|
33
|
+
OpenQMLError,
|
|
34
|
+
ResourceError,
|
|
35
|
+
ValidationError,
|
|
36
|
+
)
|
|
37
|
+
from .execution import ExecutionGroup, ExpectationCheck, ExpectationPlan
|
|
38
|
+
from .frontends import FrameworkAdapter, list_formats, register_adapter, unregister_adapter
|
|
39
|
+
from .gradients import (
|
|
40
|
+
parameter_shift,
|
|
41
|
+
parameter_shift_jacobian,
|
|
42
|
+
parameter_shift_jvp,
|
|
43
|
+
parameter_shift_vjp,
|
|
44
|
+
)
|
|
45
|
+
from .ir import Circuit, ClassicalCondition, Operation
|
|
46
|
+
from .layers import LayerCheckpoint, LayerSpec
|
|
47
|
+
from .measurement import EstimateResult, MeasurementPlan, ShotBackend, estimate, measurement_plan
|
|
48
|
+
from .observables import Hamiltonian, PauliString, z_observables
|
|
49
|
+
from .optimization import OptimizationResult, optimize
|
|
50
|
+
from .parameters import Expression, Parameter
|
|
51
|
+
from .pauli import PauliPlan
|
|
52
|
+
from .planning import DensityPlan, plan_density
|
|
53
|
+
from .program import QuantumProgram, as_circuit
|
|
54
|
+
from .providers import CirqDensityBackend, QiskitEstimatorBackend, QiskitSamplerBackend
|
|
55
|
+
from .readout import ReadoutPlan, plan_readout
|
|
56
|
+
from .simulator import ExecutionResult, execute, expectation, expectations, sample, statevector
|
|
57
|
+
from .templates import data_reuploading, hardware_efficient
|
|
58
|
+
from .verification import ConversionCheck
|
|
59
|
+
|
|
60
|
+
_INTERFACE_NAMES = frozenset({"torch", "jax", "flax"})
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def __getattr__(name):
|
|
64
|
+
if name in _INTERFACE_NAMES:
|
|
65
|
+
module = _import_module(f".interfaces.{name}", __name__)
|
|
66
|
+
globals()[name] = module
|
|
67
|
+
return module
|
|
68
|
+
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def __dir__():
|
|
72
|
+
return sorted(set(globals()) | _INTERFACE_NAMES)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
__version__ = "0.3.0a8"
|
|
76
|
+
|
|
77
|
+
__all__ = [
|
|
78
|
+
"ReadoutRoute",
|
|
79
|
+
"ConversionCheck",
|
|
80
|
+
"ExecutionBatch",
|
|
81
|
+
"ExpectationCheck",
|
|
82
|
+
"ExpectationPlan",
|
|
83
|
+
"ExecutionGroup",
|
|
84
|
+
"PauliPlan",
|
|
85
|
+
"ReadoutPlan",
|
|
86
|
+
"plan_readout",
|
|
87
|
+
"AdjointPlan",
|
|
88
|
+
"adjoint_vjp",
|
|
89
|
+
"adjoint_jvp",
|
|
90
|
+
"ConversionPlan",
|
|
91
|
+
"plan_conversion",
|
|
92
|
+
"data_reuploading",
|
|
93
|
+
"LayerCheckpoint",
|
|
94
|
+
"OptimizationResult",
|
|
95
|
+
"optimize",
|
|
96
|
+
"DensityPlan",
|
|
97
|
+
"plan_density",
|
|
98
|
+
"Circuit",
|
|
99
|
+
"ClassicalCondition",
|
|
100
|
+
"density_matrix",
|
|
101
|
+
"DensityMatrixBackend",
|
|
102
|
+
"Operation",
|
|
103
|
+
"Parameter",
|
|
104
|
+
"Expression",
|
|
105
|
+
"PauliString",
|
|
106
|
+
"Hamiltonian",
|
|
107
|
+
"statevector",
|
|
108
|
+
"expectation",
|
|
109
|
+
"expectations",
|
|
110
|
+
"execute",
|
|
111
|
+
"sample",
|
|
112
|
+
"parameter_shift",
|
|
113
|
+
"convert",
|
|
114
|
+
"diagnose",
|
|
115
|
+
"hardware_efficient",
|
|
116
|
+
"z_observables",
|
|
117
|
+
"LayerSpec",
|
|
118
|
+
"ConversionResult",
|
|
119
|
+
"ExecutionResult",
|
|
120
|
+
"Diagnostic",
|
|
121
|
+
"OpenQMLError",
|
|
122
|
+
"ValidationError",
|
|
123
|
+
"ConversionError",
|
|
124
|
+
"ResourceError",
|
|
125
|
+
"MissingDependencyError",
|
|
126
|
+
"BackendError",
|
|
127
|
+
"BackendCapabilities",
|
|
128
|
+
"ExecutionOptions",
|
|
129
|
+
"StatevectorBackend",
|
|
130
|
+
"ExpectationBackend",
|
|
131
|
+
"SamplingBackend",
|
|
132
|
+
"register_backend",
|
|
133
|
+
"unregister_backend",
|
|
134
|
+
"list_backends",
|
|
135
|
+
"get_backend",
|
|
136
|
+
"CircuitStats",
|
|
137
|
+
"circuit_stats",
|
|
138
|
+
"decompose",
|
|
139
|
+
"inverse",
|
|
140
|
+
"simplify",
|
|
141
|
+
"QuantumProgram",
|
|
142
|
+
"as_circuit",
|
|
143
|
+
"execute_batch",
|
|
144
|
+
"expectations_batch",
|
|
145
|
+
"parameter_shift_jacobian",
|
|
146
|
+
"parameter_shift_vjp",
|
|
147
|
+
"parameter_shift_jvp",
|
|
148
|
+
"estimate",
|
|
149
|
+
"measurement_plan",
|
|
150
|
+
"MeasurementPlan",
|
|
151
|
+
"EstimateResult",
|
|
152
|
+
"ShotBackend",
|
|
153
|
+
"AerBackend",
|
|
154
|
+
"CirqDensityBackend",
|
|
155
|
+
"QiskitEstimatorBackend",
|
|
156
|
+
"QiskitSamplerBackend",
|
|
157
|
+
"FrameworkAdapter",
|
|
158
|
+
"list_formats",
|
|
159
|
+
"register_adapter",
|
|
160
|
+
"unregister_adapter",
|
|
161
|
+
]
|
openqml/__main__.py
ADDED
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
"""Independent execution of exported artifacts on complete spanning inputs.
|
|
2
|
+
|
|
3
|
+
Native SDK state evolution is deliberately separate from IR reimport. Channels
|
|
4
|
+
use physical pure-state probes spanning the operator space, so SDKs need not
|
|
5
|
+
accept nonphysical matrix units. No sampling or phase alignment is used.
|
|
6
|
+
|
|
7
|
+
SDK entry points:
|
|
8
|
+
https://quantum.cloud.ibm.com/docs/en/api/qiskit/qiskit.quantum_info.DensityMatrix
|
|
9
|
+
https://quantumai.google/reference/python/cirq/DensityMatrixSimulator
|
|
10
|
+
https://docs.pennylane.ai/en/stable/code/api/pennylane.StatePrep.html
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from itertools import islice
|
|
14
|
+
|
|
15
|
+
import numpy as np
|
|
16
|
+
|
|
17
|
+
from .errors import (
|
|
18
|
+
ConversionError,
|
|
19
|
+
MissingDependencyError,
|
|
20
|
+
ResourceError,
|
|
21
|
+
ValidationError,
|
|
22
|
+
conversion_error,
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _unsupported(message):
|
|
27
|
+
return conversion_error(
|
|
28
|
+
"NATIVE_VERIFICATION_UNSUPPORTED",
|
|
29
|
+
message,
|
|
30
|
+
hint="Use roundtrip verification for retained measurement records and feedback.",
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _native_scope(circuit, target, source_options):
|
|
35
|
+
if source_options:
|
|
36
|
+
raise ValidationError("source_options apply to roundtrip verification only.")
|
|
37
|
+
if target not in {"qiskit", "cirq", "pennylane", "openqasm2", "openqasm3"}:
|
|
38
|
+
raise _unsupported("Native verification requires a supported SDK or OpenQASM target.")
|
|
39
|
+
if any(op.name in {"measure", "if_else"} for _, op in circuit.walk_operations()):
|
|
40
|
+
raise _unsupported(
|
|
41
|
+
"Native complete-input checks do not support recorded measurement or feedback."
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _probes(width, channel):
|
|
46
|
+
dimension = 1 << width
|
|
47
|
+
for index in range(dimension):
|
|
48
|
+
vector = np.zeros(dimension, dtype=complex)
|
|
49
|
+
vector[index] = 1
|
|
50
|
+
yield vector
|
|
51
|
+
if channel:
|
|
52
|
+
for first in range(dimension):
|
|
53
|
+
for second in range(first + 1, dimension):
|
|
54
|
+
for phase in (1, 1j):
|
|
55
|
+
vector = np.zeros(dimension, dtype=complex)
|
|
56
|
+
vector[first], vector[second] = 1 / np.sqrt(2), phase / np.sqrt(2)
|
|
57
|
+
yield vector
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _profile(arities, width, instrument, max_operations):
|
|
61
|
+
work = 1
|
|
62
|
+
for count, arity in enumerate(arities, 1):
|
|
63
|
+
if count > max_operations:
|
|
64
|
+
raise ResourceError("Native verification exceeds max_operations.")
|
|
65
|
+
if not 0 <= arity <= width:
|
|
66
|
+
raise ValidationError("Native artifact uses an unexpected quantum register.")
|
|
67
|
+
work += 16 * (1 << (2 * arity)) if instrument else 1 << arity
|
|
68
|
+
return work
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _qiskit(value, circuit, instrument, max_operations):
|
|
72
|
+
from qiskit.circuit import Gate
|
|
73
|
+
from qiskit.quantum_info import DensityMatrix, Statevector
|
|
74
|
+
|
|
75
|
+
if (value.num_qubits, value.num_clbits) != (circuit.num_qubits, circuit.num_clbits):
|
|
76
|
+
raise ValidationError("Native artifact changed quantum or classical register width.")
|
|
77
|
+
if value.parameters:
|
|
78
|
+
raise ValidationError("Native artifact contains unbound parameters.")
|
|
79
|
+
|
|
80
|
+
def arities():
|
|
81
|
+
for item in value.data:
|
|
82
|
+
if item.operation.name == "measure" or getattr(item.operation, "blocks", ()):
|
|
83
|
+
raise _unsupported("Native artifact contains recorded measurement or control flow.")
|
|
84
|
+
if item.clbits:
|
|
85
|
+
raise _unsupported("Native artifact contains a classical instruction.")
|
|
86
|
+
if (
|
|
87
|
+
not instrument
|
|
88
|
+
and not isinstance(item.operation, Gate)
|
|
89
|
+
and item.operation.name != "barrier"
|
|
90
|
+
):
|
|
91
|
+
raise _unsupported("Native artifact is not unitary.")
|
|
92
|
+
yield len(item.qubits)
|
|
93
|
+
|
|
94
|
+
work = _profile(arities(), circuit.num_qubits, instrument, max_operations)
|
|
95
|
+
width = circuit.num_qubits
|
|
96
|
+
permutation = (
|
|
97
|
+
np.arange(1 << width)
|
|
98
|
+
.reshape((2,) * width)
|
|
99
|
+
.transpose(tuple(reversed(range(width))))
|
|
100
|
+
.reshape(-1)
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
def run(probes):
|
|
104
|
+
outputs = []
|
|
105
|
+
for vector in probes:
|
|
106
|
+
state = (DensityMatrix if instrument else Statevector)(vector[permutation]).evolve(
|
|
107
|
+
value
|
|
108
|
+
)
|
|
109
|
+
output = np.asarray(state.data)
|
|
110
|
+
outputs.append(
|
|
111
|
+
output[np.ix_(permutation, permutation)].reshape(-1)
|
|
112
|
+
if instrument
|
|
113
|
+
else output[permutation]
|
|
114
|
+
)
|
|
115
|
+
return np.stack(outputs)
|
|
116
|
+
|
|
117
|
+
return run, work
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def _cirq(value, circuit, instrument, max_operations):
|
|
121
|
+
import cirq
|
|
122
|
+
|
|
123
|
+
order = tuple(cirq.LineQubit.range(circuit.num_qubits))
|
|
124
|
+
if value.all_qubits() != frozenset(order):
|
|
125
|
+
raise ValidationError("Native artifact changed canonical quantum wires.")
|
|
126
|
+
|
|
127
|
+
def arities():
|
|
128
|
+
for op in value.all_operations():
|
|
129
|
+
if (
|
|
130
|
+
isinstance(op, cirq.CircuitOperation)
|
|
131
|
+
or cirq.is_measurement(op)
|
|
132
|
+
or op.classical_controls
|
|
133
|
+
):
|
|
134
|
+
raise _unsupported("Native artifact contains measurement or nested control flow.")
|
|
135
|
+
if cirq.is_parameterized(op):
|
|
136
|
+
raise ValidationError("Native artifact contains unbound parameters.")
|
|
137
|
+
if not instrument and not cirq.has_unitary(op):
|
|
138
|
+
raise _unsupported(
|
|
139
|
+
"Native artifact is not unitary; stochastic verification is refused."
|
|
140
|
+
)
|
|
141
|
+
yield len(op.qubits)
|
|
142
|
+
|
|
143
|
+
work = _profile(arities(), circuit.num_qubits, instrument, max_operations)
|
|
144
|
+
simulator = (
|
|
145
|
+
cirq.DensityMatrixSimulator(dtype=np.complex128, split_untangled_states=False)
|
|
146
|
+
if instrument
|
|
147
|
+
else cirq.Simulator(dtype=np.complex128)
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
def run(probes):
|
|
151
|
+
outputs = []
|
|
152
|
+
for vector in probes:
|
|
153
|
+
result = simulator.simulate(value, initial_state=vector, qubit_order=order)
|
|
154
|
+
output = result.final_density_matrix if instrument else result.final_state_vector
|
|
155
|
+
outputs.append(np.asarray(output).reshape(-1))
|
|
156
|
+
return np.stack(outputs)
|
|
157
|
+
|
|
158
|
+
return run, work
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def _pennylane(value, circuit, instrument, max_operations):
|
|
162
|
+
import pennylane as qml
|
|
163
|
+
|
|
164
|
+
if value.measurements:
|
|
165
|
+
raise _unsupported("Native PennyLane verification requires an operation-only tape.")
|
|
166
|
+
order = tuple(range(circuit.num_qubits))
|
|
167
|
+
|
|
168
|
+
def arities():
|
|
169
|
+
for op in value.operations:
|
|
170
|
+
if not set(op.wires).issubset(order):
|
|
171
|
+
raise ValidationError("Native artifact changed canonical quantum wires.")
|
|
172
|
+
if not op.has_matrix and not isinstance(op, qml.operation.Channel):
|
|
173
|
+
raise _unsupported("Native tape contains an unsupported dynamic operation.")
|
|
174
|
+
if not instrument and isinstance(op, qml.operation.Channel):
|
|
175
|
+
raise _unsupported("Native artifact is not unitary.")
|
|
176
|
+
yield len(op.wires)
|
|
177
|
+
|
|
178
|
+
work = _profile(arities(), circuit.num_qubits, instrument, max_operations)
|
|
179
|
+
device = qml.device("default.mixed" if instrument else "default.qubit", wires=order)
|
|
180
|
+
|
|
181
|
+
@qml.qnode(device, interface=None, diff_method=None)
|
|
182
|
+
def program(vector):
|
|
183
|
+
qml.StatePrep(vector, wires=order)
|
|
184
|
+
for op in value.operations:
|
|
185
|
+
qml.apply(op)
|
|
186
|
+
return qml.density_matrix(wires=order) if instrument else qml.state()
|
|
187
|
+
|
|
188
|
+
def run(probes):
|
|
189
|
+
return np.stack([np.asarray(program(vector)).reshape(-1) for vector in probes])
|
|
190
|
+
|
|
191
|
+
return run, work
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def _verify_native(
|
|
195
|
+
plan, value, instrument, records, peak, size, work, max_work, budget, chunk, atol, rtol
|
|
196
|
+
):
|
|
197
|
+
from .verification import _basis_capacity, _Comparison, _evolve_basis
|
|
198
|
+
|
|
199
|
+
circuit, target = plan.circuit, plan.target_format
|
|
200
|
+
backend = "qiskit" if target.startswith("openqasm") else target
|
|
201
|
+
try:
|
|
202
|
+
if target.startswith("openqasm"):
|
|
203
|
+
from .program import QuantumProgram
|
|
204
|
+
|
|
205
|
+
value = QuantumProgram.from_qasm(
|
|
206
|
+
value,
|
|
207
|
+
version=int(target[-1]),
|
|
208
|
+
max_operations=plan.max_operations,
|
|
209
|
+
max_depth=plan.max_depth,
|
|
210
|
+
max_register_bits=circuit.num_qubits + circuit.num_clbits,
|
|
211
|
+
max_matrix_qubits=circuit.num_qubits,
|
|
212
|
+
).native
|
|
213
|
+
runner, native_work = {"qiskit": _qiskit, "cirq": _cirq, "pennylane": _pennylane}[backend](
|
|
214
|
+
value, circuit, instrument, plan.max_operations
|
|
215
|
+
)
|
|
216
|
+
except ImportError as exc:
|
|
217
|
+
extra = "qasm" if target.startswith("openqasm") else backend
|
|
218
|
+
raise MissingDependencyError(f"Install openqml[{extra}] for native verification.") from exc
|
|
219
|
+
except (ConversionError, ResourceError, ValidationError):
|
|
220
|
+
raise
|
|
221
|
+
except Exception as exc:
|
|
222
|
+
raise conversion_error(
|
|
223
|
+
"NATIVE_VERIFICATION_PREPARATION", f"{backend} artifact preparation failed: {exc}"
|
|
224
|
+
) from exc
|
|
225
|
+
if work + size * size * native_work > max_work:
|
|
226
|
+
raise ResourceError("Native verification exceeds max_work.")
|
|
227
|
+
capacity = _basis_capacity((circuit,), (peak, 1), size, budget)
|
|
228
|
+
chunk, probes, comparison = (
|
|
229
|
+
min(chunk, capacity, size),
|
|
230
|
+
_probes(circuit.num_qubits, instrument),
|
|
231
|
+
_Comparison(),
|
|
232
|
+
)
|
|
233
|
+
for start in range(0, size, chunk):
|
|
234
|
+
vectors = np.stack(tuple(islice(probes, min(chunk, size - start))))
|
|
235
|
+
basis = (
|
|
236
|
+
(vectors[..., :, None] * vectors[..., None, :].conj()).reshape(len(vectors), -1)
|
|
237
|
+
if instrument
|
|
238
|
+
else vectors
|
|
239
|
+
)
|
|
240
|
+
expected = _evolve_basis(circuit, basis, instrument, records)
|
|
241
|
+
try:
|
|
242
|
+
actual = {frozenset(): runner(vectors)}
|
|
243
|
+
except (ConversionError, ResourceError, ValidationError, MissingDependencyError):
|
|
244
|
+
raise
|
|
245
|
+
except Exception as exc:
|
|
246
|
+
raise conversion_error(
|
|
247
|
+
"NATIVE_VERIFICATION_EXECUTION", f"{backend} execution failed: {exc}"
|
|
248
|
+
) from exc
|
|
249
|
+
comparison.add(expected, actual, start, atol, rtol)
|
|
250
|
+
del vectors, basis, expected, actual
|
|
251
|
+
return comparison.result(
|
|
252
|
+
circuit,
|
|
253
|
+
target,
|
|
254
|
+
"quantum_channel" if instrument else "unitary_including_global_phase",
|
|
255
|
+
size,
|
|
256
|
+
atol,
|
|
257
|
+
rtol,
|
|
258
|
+
"native",
|
|
259
|
+
backend,
|
|
260
|
+
)
|
openqml/_qasm.py
ADDED
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
"""Frontend-independent source limits and comment handling."""
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
|
|
5
|
+
from .errors import conversion_error
|
|
6
|
+
from .ir import integer
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def clean_source(text, max_source_bytes=2_000_000):
|
|
10
|
+
integer(max_source_bytes, "max_source_bytes", 1)
|
|
11
|
+
if (
|
|
12
|
+
not isinstance(text, str)
|
|
13
|
+
or len(text) > max_source_bytes
|
|
14
|
+
or len(text.encode("utf-8")) > max_source_bytes
|
|
15
|
+
):
|
|
16
|
+
raise conversion_error("SOURCE_LIMIT", "OpenQASM source exceeds the configured byte limit.")
|
|
17
|
+
# Remove comments without changing strings, line numbers or character offsets.
|
|
18
|
+
output, index = [], 0
|
|
19
|
+
while index < len(text):
|
|
20
|
+
if text[index] == '"':
|
|
21
|
+
start = index
|
|
22
|
+
index += 1
|
|
23
|
+
while index < len(text):
|
|
24
|
+
if text[index] == "\\":
|
|
25
|
+
index += 2
|
|
26
|
+
elif text[index] == '"':
|
|
27
|
+
index += 1
|
|
28
|
+
break
|
|
29
|
+
else:
|
|
30
|
+
index += 1
|
|
31
|
+
else:
|
|
32
|
+
raise conversion_error("SYNTAX", "Unterminated source string.")
|
|
33
|
+
output.append(text[start:index])
|
|
34
|
+
elif text.startswith("//", index):
|
|
35
|
+
end = text.find("\n", index)
|
|
36
|
+
end = len(text) if end < 0 else end
|
|
37
|
+
output.append(" " * (end - index))
|
|
38
|
+
index = end
|
|
39
|
+
elif text.startswith("/*", index):
|
|
40
|
+
end = text.find("*/", index + 2)
|
|
41
|
+
if end < 0:
|
|
42
|
+
raise conversion_error("SYNTAX", "Unterminated block comment.")
|
|
43
|
+
end += 2
|
|
44
|
+
output.append("".join("\n" if c == "\n" else " " for c in text[index:end]))
|
|
45
|
+
index = end
|
|
46
|
+
else:
|
|
47
|
+
output.append(text[index])
|
|
48
|
+
index += 1
|
|
49
|
+
return "".join(output)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def validate_header(text, version=None):
|
|
53
|
+
header = re.match(r"\s*OPENQASM\s+([23])(?:\.0)?\s*;", text)
|
|
54
|
+
if not header:
|
|
55
|
+
raise conversion_error("QASM_VERSION", "Expected an OpenQASM 2.0 or 3.0 header.")
|
|
56
|
+
actual = int(header.group(1))
|
|
57
|
+
if version is not None and (type(version) is not int or version != actual):
|
|
58
|
+
raise conversion_error(
|
|
59
|
+
"QASM_VERSION", "Source header differs from requested source format."
|
|
60
|
+
)
|
|
61
|
+
return actual
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def check_literal_registers(text, max_register_bits):
|
|
65
|
+
"""Reject oversized literal declarations before an SDK constructs their storage.
|
|
66
|
+
|
|
67
|
+
SDK-resolved constant/expression sizes also receive a post-parse width check.
|
|
68
|
+
This preflight does not claim a process-level SDK parsing memory limit.
|
|
69
|
+
"""
|
|
70
|
+
integer(max_register_bits, "max_register_bits", 1)
|
|
71
|
+
code = re.sub(r'"(?:\\.|[^"\\])*"', '""', text)
|
|
72
|
+
declarations = re.finditer(
|
|
73
|
+
r"\b(?:qreg|creg)\s+[A-Za-z_]\w*\s*\[\s*(\d+)\s*\]"
|
|
74
|
+
r"|\b(?:qubit|bit)\s*(?:\[\s*(\d+)\s*\])?\s+[A-Za-z_]\w*",
|
|
75
|
+
code,
|
|
76
|
+
)
|
|
77
|
+
total = 0
|
|
78
|
+
for declaration in declarations:
|
|
79
|
+
total += int(declaration.group(1) or declaration.group(2) or "1")
|
|
80
|
+
if total > max_register_bits:
|
|
81
|
+
raise conversion_error("REGISTER_LIMIT", "Register size exceeds the configured limit.")
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def expand_includes(text, include_paths, version, max_source_bytes, max_include_depth):
|
|
85
|
+
"""Inline custom files from explicit roots with cumulative read/expansion limits."""
|
|
86
|
+
from pathlib import Path
|
|
87
|
+
|
|
88
|
+
integer(max_include_depth, "max_include_depth")
|
|
89
|
+
if isinstance(include_paths, (str, bytes)):
|
|
90
|
+
raise conversion_error("INCLUDE_PATH", "include_paths must be a sequence of directories.")
|
|
91
|
+
try:
|
|
92
|
+
roots = tuple(Path(p).resolve(strict=True) for p in include_paths)
|
|
93
|
+
except (TypeError, OSError, ValueError, RuntimeError) as exc:
|
|
94
|
+
raise conversion_error(
|
|
95
|
+
"INCLUDE_PATH", "Include roots must be existing directories."
|
|
96
|
+
) from exc
|
|
97
|
+
if any(not p.is_dir() for p in roots):
|
|
98
|
+
raise conversion_error("INCLUDE_PATH", "Include roots must be directories.")
|
|
99
|
+
remaining = max_source_bytes - len(text.encode("utf-8"))
|
|
100
|
+
directive = re.compile(r'"(?:\\.|[^"\\])*"|\binclude\b(?:\s*"([^"\n]+)"\s*;)?')
|
|
101
|
+
standard = "qelib1.inc" if version == 2 else "stdgates.inc"
|
|
102
|
+
|
|
103
|
+
def expand(code, parent, stack):
|
|
104
|
+
nonlocal remaining
|
|
105
|
+
pieces, end = [], 0
|
|
106
|
+
for match in directive.finditer(code):
|
|
107
|
+
filename = match.group(1)
|
|
108
|
+
if filename is None:
|
|
109
|
+
if not match.group().startswith('"'):
|
|
110
|
+
raise conversion_error("SYNTAX", "Malformed include directive.")
|
|
111
|
+
continue
|
|
112
|
+
if filename == standard:
|
|
113
|
+
continue
|
|
114
|
+
if not roots:
|
|
115
|
+
raise conversion_error(
|
|
116
|
+
"INCLUDE_PATH", "Custom includes require explicit include_paths."
|
|
117
|
+
)
|
|
118
|
+
if len(stack) >= max_include_depth:
|
|
119
|
+
raise conversion_error(
|
|
120
|
+
"INCLUDE_DEPTH_LIMIT", "Custom include nesting exceeds max_include_depth."
|
|
121
|
+
)
|
|
122
|
+
target = Path(filename)
|
|
123
|
+
searches = ((parent,) if parent is not None else ()) + roots
|
|
124
|
+
candidates = (target,) if target.is_absolute() else tuple(p / target for p in searches)
|
|
125
|
+
resolved = None
|
|
126
|
+
for candidate in candidates:
|
|
127
|
+
try:
|
|
128
|
+
candidate = candidate.resolve()
|
|
129
|
+
except (OSError, ValueError, RuntimeError):
|
|
130
|
+
continue
|
|
131
|
+
if any(candidate.is_relative_to(root) for root in roots) and candidate.is_file():
|
|
132
|
+
resolved = candidate
|
|
133
|
+
break
|
|
134
|
+
if resolved is None:
|
|
135
|
+
raise conversion_error(
|
|
136
|
+
"INCLUDE_PATH", "Include file is missing or outside the explicit roots."
|
|
137
|
+
)
|
|
138
|
+
if resolved in stack:
|
|
139
|
+
raise conversion_error("INCLUDE_CYCLE", "Custom include files contain a cycle.")
|
|
140
|
+
try:
|
|
141
|
+
with resolved.open("rb") as stream:
|
|
142
|
+
payload = stream.read(remaining + 1)
|
|
143
|
+
except OSError as exc:
|
|
144
|
+
raise conversion_error(
|
|
145
|
+
"INCLUDE_PATH", "Cannot read the requested include file."
|
|
146
|
+
) from exc
|
|
147
|
+
if len(payload) > remaining:
|
|
148
|
+
raise conversion_error(
|
|
149
|
+
"SOURCE_LIMIT", "Source and included files exceed max_source_bytes."
|
|
150
|
+
)
|
|
151
|
+
remaining -= len(payload)
|
|
152
|
+
try:
|
|
153
|
+
child = clean_source(payload.decode("utf-8"), max_source_bytes)
|
|
154
|
+
except UnicodeError as exc:
|
|
155
|
+
raise conversion_error("INCLUDE_ENCODING", "Include files must use UTF-8.") from exc
|
|
156
|
+
pieces.extend(
|
|
157
|
+
(
|
|
158
|
+
code[end : match.start()],
|
|
159
|
+
"\n",
|
|
160
|
+
expand(child, resolved.parent, stack + (resolved,)),
|
|
161
|
+
"\n",
|
|
162
|
+
)
|
|
163
|
+
)
|
|
164
|
+
end = match.end()
|
|
165
|
+
pieces.append(code[end:])
|
|
166
|
+
return "".join(pieces)
|
|
167
|
+
|
|
168
|
+
try:
|
|
169
|
+
output = expand(text, None, ())
|
|
170
|
+
except RecursionError as exc:
|
|
171
|
+
raise conversion_error(
|
|
172
|
+
"INCLUDE_DEPTH_LIMIT", "Include nesting exceeds the Python recursion limit."
|
|
173
|
+
) from exc
|
|
174
|
+
if len(output.encode("utf-8")) > max_source_bytes:
|
|
175
|
+
raise conversion_error("SOURCE_LIMIT", "Expanded source exceeds max_source_bytes.")
|
|
176
|
+
return output
|