1616import warnings
1717from collections import defaultdict
1818from collections .abc import Mapping , Sequence
19+ from dataclasses import dataclass
1920
21+ from braket .default_simulator .openqasm .parser .openqasm_ast import (
22+ Identifier ,
23+ IntegerLiteral ,
24+ QASMNode ,
25+ QuantumMeasurementStatement ,
26+ QubitDeclaration ,
27+ )
28+ from braket .default_simulator .openqasm .parser .openqasm_parser import parse
2029from braket .ir .openqasm import Program
2130
2231from braket .circuits import Circuit , Gate , Observable
2938from braket .registers import QubitSet
3039
3140
41+ @dataclass
42+ class _AngleInjectionPlan :
43+ declarations_offset : int
44+ measure_offset : int
45+ qubit_format : str
46+ qubits : QubitSet
47+
48+
49+ def _span_offset (node : QASMNode , line_offsets : Sequence [int ]) -> int :
50+ return line_offsets [node .span .start_line - 1 ] + node .span .start_column
51+
52+
53+ def _plan_injection (source : str ) -> _AngleInjectionPlan :
54+ program = parse (source )
55+
56+ line_offsets = [0 ]
57+ offset = 0
58+ for line in source .split ("\n " )[:- 1 ]:
59+ offset += len (line ) + 1
60+ line_offsets .append (offset )
61+
62+ declarations_offset = (
63+ _span_offset (program .statements [0 ], line_offsets ) if program .statements else len (source )
64+ )
65+
66+ measure_offset = len (source )
67+ register_name = None
68+ register_size = 0
69+ for stmt in program .statements :
70+ if isinstance (stmt , QubitDeclaration ) and register_name is None :
71+ # TODO: support multiple qubit registers
72+ register_name = stmt .qubit .name
73+ # stmt.size is None for a single unindexed qubit
74+ register_size = stmt .size .value if isinstance (stmt .size , IntegerLiteral ) else 1
75+
76+ measure_offset = (
77+ min (measure_offset , _span_offset (stmt , line_offsets ))
78+ if isinstance (stmt , QuantumMeasurementStatement )
79+ else len (source )
80+ )
81+
82+ qubit_format , qubits = (
83+ (f"{ register_name } [{{}}]" , QubitSet (range (register_size )))
84+ if register_name
85+ else (
86+ "${}" ,
87+ QubitSet (
88+ int (node .name [1 :])
89+ for node in _walk (program )
90+ if isinstance (node , Identifier ) and node .name .startswith ("$" )
91+ ),
92+ )
93+ )
94+ return _AngleInjectionPlan (
95+ declarations_offset = declarations_offset ,
96+ measure_offset = measure_offset ,
97+ qubit_format = qubit_format ,
98+ qubits = qubits ,
99+ )
100+
101+
102+ def _walk (node : QASMNode ):
103+ yield node
104+ for value in vars (node ).values ():
105+ for child in value if isinstance (value , list ) else [value ]:
106+ if isinstance (child , QASMNode ):
107+ yield from _walk (child )
108+
109+
32110class CircuitBinding :
33111 def __init__ (
34112 self ,
35- circuit : Circuit ,
113+ circuit : Circuit | str ,
36114 input_sets : ParameterSetsLike | None = None ,
37115 observables : Sequence [Observable | PauliString | str ] | Sum | None = None ,
38116 ):
@@ -51,7 +129,8 @@ def __init__(
51129 Note: Circuits cannot have result types attached.
52130
53131 Args:
54- circuit (Circuit): The parametrized circuit
132+ circuit (Circuit | str): The parametrized circuit, either as a Circuit object or as
133+ an OpenQASM string.
55134 input_sets (ParameterSetsLike | None): The inputs to the circuit, if specified.
56135 observables (Sequence[Observable | PauliString | str] | Sum | None): The observables
57136 or Hamiltonian to measure, if specified.
@@ -70,11 +149,14 @@ def __init__(
70149 and any (isinstance (obs , Sum ) for obs in observables )
71150 ):
72151 raise TypeError ("Cannot have Sum Hamiltonian in list of observables" )
73- if circuit .result_types :
152+ if isinstance ( circuit , Circuit ) and circuit .result_types :
74153 raise ValueError ("Circuit cannot have result types" )
75154 self ._circuit = circuit
76155 self ._input_sets = ParameterSets (input_sets ) if input_sets else None
77156 self ._observables = CircuitBinding ._to_observables (observables )
157+ self ._injection_plan = (
158+ _plan_injection (circuit ) if isinstance (circuit , str ) and self ._observables else None
159+ )
78160
79161 @staticmethod
80162 def _to_observables (
@@ -96,9 +178,9 @@ def _to_observables(
96178 return obs
97179
98180 @property
99- def circuit (self ) -> Circuit :
181+ def circuit (self ) -> Circuit | str :
100182 """
101- Circuit: The parametrized circuit
183+ Circuit | str : The parametrized circuit, either as a Circuit object or an OpenQASM string.
102184 """
103185 return self ._circuit
104186
@@ -135,23 +217,60 @@ def to_ir(
135217 """
136218 if not self ._observables :
137219 return Program (
138- source = self ._circuit .to_ir (
139- IRType .OPENQASM , gate_definitions = gate_definitions
140- ).source ,
220+ source = self ._circuit_openqasm (gate_definitions ),
141221 inputs = self ._input_sets .as_dict () if self ._input_sets else None ,
142222 )
143- # with_euler_angles validates that the observable has valid Euler angle gates
144- circuit_with_euler_angles = self ._circuit .with_euler_angles (self ._observables )
145223 euler_angles = self ._get_euler_angles ()
224+ if isinstance (self ._circuit , Circuit ):
225+ source = (
226+ self ._circuit
227+ .with_euler_angles (self ._observables )
228+ .to_ir (IRType .OPENQASM , gate_definitions = gate_definitions )
229+ .source
230+ )
231+ else :
232+ source = _inject_euler_angles (
233+ self ._circuit ,
234+ self ._injection_plan ,
235+ self ._euler_rotation_targets (),
236+ euler_angles .keys (),
237+ )
146238 return Program (
147- source = circuit_with_euler_angles .to_ir (
148- IRType .OPENQASM , gate_definitions = gate_definitions
149- ).source ,
239+ source = source ,
150240 inputs = (
151241 self ._input_sets * euler_angles if self ._input_sets else ParameterSets (euler_angles )
152242 ).as_dict (),
153243 )
154244
245+ def _circuit_openqasm (
246+ self ,
247+ gate_definitions : Mapping [tuple [Gate , QubitSet ], PulseSequence ] | None ,
248+ ) -> str :
249+ if isinstance (self ._circuit , Circuit ):
250+ return self ._circuit .to_ir (IRType .OPENQASM , gate_definitions = gate_definitions ).source
251+ return self ._circuit
252+
253+ def _circuit_qubits (self ) -> QubitSet :
254+ if isinstance (self ._circuit , Circuit ):
255+ return self ._circuit .qubits
256+ return self ._injection_plan .qubits
257+
258+ def _euler_rotation_targets (self ) -> QubitSet :
259+ observables = self ._observables
260+ circuit_qubits = self ._circuit_qubits ()
261+ if isinstance (observables , Sum ):
262+ if observables .targets :
263+ # Sum.targets is a per-summand list of QubitSets
264+ return QubitSet ().union (* observables .targets )
265+ return circuit_qubits
266+ targets = QubitSet ()
267+ for obs in observables :
268+ if obs .targets :
269+ targets |= obs .targets
270+ else :
271+ targets |= circuit_qubits
272+ return targets
273+
155274 def _get_euler_angles (self ) -> dict [str , float ] | None :
156275 observables = self ._observables
157276 return (
@@ -164,7 +283,7 @@ def _get_euler_angles_sum(self, observables: Sum) -> dict[str, float]:
164283 euler_angles = defaultdict (list )
165284 summands = observables .summands
166285 if not observables .targets :
167- targets = self ._circuit . qubits
286+ targets = self ._circuit_qubits ()
168287 for obs in summands :
169288 for param , angle in obs .get_euler_angles (targets ).items ():
170289 euler_angles [param ].append (angle )
@@ -179,7 +298,7 @@ def _get_euler_angles_sum(self, observables: Sum) -> dict[str, float]:
179298
180299 def _get_euler_angles_list (self , observables : Sequence [Observable ]) -> dict [str , float ]:
181300 euler_angles = defaultdict (list )
182- circuit_qubits = self ._circuit . qubits
301+ circuit_qubits = self ._circuit_qubits ()
183302 targets = QubitSet (q for obs in observables for q in (obs .targets or circuit_qubits ))
184303 for obs in observables :
185304 if not obs .targets :
@@ -207,12 +326,17 @@ def bind_observables_to_inputs(
207326 well as CompositeEntry.expectation.
208327
209328 Kwargs:
210- inplace (bool): whether or not to return a new circuit binding or use the same one
211- add_measure (bool): whether or not to apply Measure instructions to the circuit
329+ inplace (bool): Whether to return a new circuit binding or use the same one
330+ add_measure (bool): Whether to apply Measure instructions to the circuit. Only
331+ applies when the underlying circuit is a `Circuit`; for OpenQASM string
332+ circuits, the source is preserved verbatim aside from injected Euler-angle
333+ rotations.
212334
213335 Returns:
214336 CircuitBinding: A new circuit binding with the observables bound.
215337 """
338+ if isinstance (self ._circuit , str ):
339+ return self ._bind_observables_to_inputs_str (inplace )
216340 measure = Circuit ()
217341 parameters = self ._input_sets .as_dict () if self ._input_sets else None
218342 if observables := self ._observables :
@@ -236,6 +360,33 @@ def bind_observables_to_inputs(
236360 return self
237361 return CircuitBinding (self ._circuit + measure , input_sets = parameters )
238362
363+ def _bind_observables_to_inputs_str (self , inplace : bool ) -> CircuitBinding :
364+ source = self ._circuit
365+ parameters = self ._input_sets .as_dict () if self ._input_sets else None
366+ if observables := self ._observables :
367+ if isinstance (observables , Sum ):
368+ warnings .warn (
369+ "Binding a Sum discards information on observable weights; please "
370+ "distribute your observable in advance using observable.summands." ,
371+ stacklevel = 2 ,
372+ )
373+ euler_angles = self ._get_euler_angles ()
374+ source = _inject_euler_angles (
375+ source ,
376+ self ._injection_plan ,
377+ self ._euler_rotation_targets (),
378+ euler_angles .keys (),
379+ )
380+ parameters = self ._input_sets * euler_angles if parameters else euler_angles
381+ if inplace :
382+ self ._circuit = source
383+ # Observables are now bound into the source, so no further injection plan is needed.
384+ self ._injection_plan = None
385+ self ._observables = None
386+ self ._input_sets = parameters
387+ return self
388+ return CircuitBinding (source , input_sets = parameters )
389+
239390 def __len__ (self ):
240391 input_sets = self ._input_sets
241392 observables = self ._observables
@@ -260,3 +411,28 @@ def __repr__(self):
260411 f"input_sets={ self ._input_sets } , "
261412 f"observables={ self ._observables } )"
262413 )
414+
415+
416+ def _inject_euler_angles (
417+ source : str ,
418+ plan : _AngleInjectionPlan ,
419+ targets : QubitSet ,
420+ parameter_names : Sequence [str ],
421+ ) -> str :
422+ rotations = []
423+ for q in targets :
424+ theta , phi , omega = euler_angle_parameter_names (q )
425+ formatted = plan .qubit_format .format (int (q ))
426+ rotations .extend ([
427+ f"rz({ theta } ) { formatted } ;" ,
428+ f"rx({ phi } ) { formatted } ;" ,
429+ f"rz({ omega } ) { formatted } ;" ,
430+ ])
431+ for offset , statements in (
432+ (plan .measure_offset , rotations ),
433+ (plan .declarations_offset , [f"input float { name } ;" for name in parameter_names ]),
434+ ):
435+ block = "\n " .join (statements )
436+ prefix = "" if offset == 0 or source [offset - 1 ] == "\n " else "\n "
437+ source = f"{ source [:offset ]} { prefix } { block } \n { source [offset :]} "
438+ return source
0 commit comments