2626from collections import OrderedDict , deque
2727from functools import partial
2828from io import StringIO
29- from typing import Any , Callable , Optional , Sequence , cast
29+ from typing import Any , Callable , Optional , Sequence , Union , cast
3030
3131import numpy as np
3232import openqasm3 .ast as qasm3_ast
@@ -271,22 +271,21 @@ def _visit_quantum_register(
271271 return []
272272 return [register ]
273273
274- # pylint: disable-next=too-many-locals,too-many-branches
274+ # pylint: disable-next=too-many-locals,too-many-branches,too-many-statements
275275 def _get_op_bits (
276276 self ,
277277 operation : Any ,
278278 qubits : bool = True ,
279279 function_qubit_sizes : Optional [dict [str , int ]] = None ,
280- ) -> list [qasm3_ast .IndexedIdentifier ]:
280+ ) -> list [Union [ qasm3_ast .IndexedIdentifier , qasm3_ast . Identifier ] ]:
281281 """Get the quantum / classical bits for the operation.
282-
283282 Args:
284283 operation (Any): The operation to get qubits for.
285284 qubits (bool): Whether the bits are quantum bits or classical bits. Defaults to True.
286285 Returns:
287- list[qasm3_ast.IndexedIdentifier] : The bits for the operation.
286+ The quantum or classical bits for the operation.
288287 """
289- openqasm_bits = []
288+ openqasm_bits : list [ Union [ qasm3_ast . IndexedIdentifier , qasm3_ast . Identifier ]] = []
290289 bit_list = []
291290
292291 if isinstance (operation , qasm3_ast .QuantumMeasurementStatement ):
@@ -316,6 +315,20 @@ def _get_op_bits(
316315 else :
317316 reg_name = bit .name
318317
318+ if qubits and reg_name .startswith ("$" ):
319+ # Physical qubit reference (e.g. $0, $1).
320+ if not reg_name [1 :].isdigit ():
321+ raise_qasm3_error (
322+ f"Invalid physical qubit identifier '{ reg_name } ': "
323+ f"expected a non-negative integer index after '$'" ,
324+ error_node = operation ,
325+ span = operation .span ,
326+ )
327+ self ._register_physical_qubit (reg_name )
328+ # Keep as an Identifier so it serialises as "$0" rather than "$0[0]".
329+ openqasm_bits .append (qasm3_ast .Identifier (reg_name ))
330+ continue
331+
319332 max_register_size = 0
320333 reg_var = self ._scope_manager .get_from_visible_scope (reg_name )
321334 if reg_var is None :
@@ -374,7 +387,6 @@ def _get_op_bits(
374387 )
375388 for bit_id in bit_ids
376389 ]
377-
378390 openqasm_bits .extend (new_bits )
379391
380392 return openqasm_bits
@@ -558,17 +570,33 @@ def _visit_measurement( # pylint: disable=too-many-locals,too-many-branches,too
558570 if isinstance (source , qasm3_ast .Identifier ):
559571 is_pulse_gate = False
560572 if source .name .startswith ("$" ) and source .name [1 :].isdigit ():
561- is_pulse_gate = True
562- statement .measure .qubit .name = f"__PYQASM_QUBITS__[{ source .name [1 :]} ]"
573+ if self ._openpulse_grammar_declared :
574+ # OpenPulse program: rename to the internal virtual register used by the
575+ # pulse visitor, and validate the index is in range.
576+ is_pulse_gate = True
577+ statement .measure .qubit .name = f"__PYQASM_QUBITS__[{ source .name [1 :]} ]"
578+ if (
579+ self ._total_pulse_qubits <= 0
580+ and sum (self ._global_qreg_size_map .values ()) == 0
581+ ):
582+ raise_qasm3_error (
583+ "Invalid no of qubits in pulse level measurement" ,
584+ error_node = statement ,
585+ span = statement .span ,
586+ )
587+ else :
588+ # Plain QASM program: keep the physical qubit identifier as-is.
589+ is_pulse_gate = True
590+ self ._register_physical_qubit (source .name )
563591 elif source .name .startswith ("__PYQASM_QUBITS__" ):
564592 is_pulse_gate = True
565593 statement .measure .qubit .name = source .name
566- if self ._total_pulse_qubits <= 0 and sum (self ._global_qreg_size_map .values ()) == 0 :
567- raise_qasm3_error (
568- "Invalid no of qubits in pulse level measurement" ,
569- error_node = statement ,
570- span = statement .span ,
571- )
594+ if self ._total_pulse_qubits <= 0 and sum (self ._global_qreg_size_map .values ()) == 0 :
595+ raise_qasm3_error (
596+ "Invalid no of qubits in pulse level measurement" ,
597+ error_node = statement ,
598+ span = statement .span ,
599+ )
572600 if is_pulse_gate :
573601 return [statement ]
574602 # # TODO: handle in-function measurements
@@ -748,12 +776,20 @@ def _visit_barrier( # pylint: disable=too-many-locals, too-many-branches
748776 for op_qubit in barrier .qubits :
749777 if isinstance (op_qubit , qasm3_ast .Identifier ):
750778 if op_qubit .name .startswith ("$" ) and op_qubit .name [1 :].isdigit ():
751- if int (op_qubit .name [1 :]) >= self ._total_pulse_qubits :
752- raise_qasm3_error (
753- f"Invalid pulse qubit index `{ op_qubit .name } ` on barrier" ,
754- error_node = barrier ,
755- span = barrier .span ,
756- )
779+ phys_idx = int (op_qubit .name [1 :])
780+ # In an OpenPulse program all physical qubits are declared up-front via
781+ # defcal; validate that the index is within the known range.
782+ # In a plain QASM program there are no such declarations, so any
783+ # non-negative index is valid.
784+ if self ._openpulse_grammar_declared :
785+ if phys_idx >= self ._total_pulse_qubits :
786+ raise_qasm3_error (
787+ f"Invalid pulse qubit index `{ op_qubit .name } ` on barrier" ,
788+ error_node = barrier ,
789+ span = barrier .span ,
790+ )
791+ else :
792+ self ._register_physical_qubit (op_qubit .name )
757793 valid_open_pulse_qubits = True
758794 if valid_open_pulse_qubits :
759795 return [barrier ]
@@ -868,7 +904,7 @@ def _visit_gate_definition(self, definition: qasm3_ast.QuantumGateDefinition) ->
868904
869905 def _unroll_multiple_target_qubits (
870906 self , operation : qasm3_ast .QuantumGate , gate_qubit_count : int
871- ) -> list [list [qasm3_ast .IndexedIdentifier ]]:
907+ ) -> list [list [Union [ qasm3_ast .IndexedIdentifier , qasm3_ast . Identifier ] ]]:
872908 """Unroll the complete list of all qubits that the given operation is applied to.
873909 E.g. this maps 'cx q[0], q[1], q[2], q[3]' to [[q[0], q[1]], [q[2], q[3]]]
874910
@@ -895,7 +931,7 @@ def _unroll_multiple_target_qubits(
895931 def _broadcast_gate_operation (
896932 self ,
897933 gate_function : Callable ,
898- all_targets : list [list [qasm3_ast .IndexedIdentifier ]],
934+ all_targets : list [list [Union [ qasm3_ast .IndexedIdentifier , qasm3_ast . Identifier ] ]],
899935 ctrls : Optional [list [qasm3_ast .IndexedIdentifier ]] = None ,
900936 ) -> list [qasm3_ast .QuantumGate ]:
901937 """Broadcasts the application of a gate onto multiple sets of target qubits.
@@ -917,9 +953,42 @@ def _broadcast_gate_operation(
917953 result .extend (gate_function (* ctrls , * targets ))
918954 return result
919955
956+ def _register_physical_qubit (self , name : str ) -> int :
957+ """Register a physical qubit ``$n`` for depth / count tracking if not already known.
958+
959+ Args:
960+ name: The physical qubit identifier string (e.g. ``"$0"``).
961+
962+ Returns:
963+ The physical qubit index.
964+ """
965+ phys_idx = int (name [1 :])
966+ if (name , phys_idx ) not in self ._module ._qubit_depths :
967+ self ._module ._qubit_depths [(name , phys_idx )] = QubitDepthNode (name , phys_idx )
968+ self ._total_pulse_qubits = max (self ._total_pulse_qubits , phys_idx + 1 )
969+ self ._module .num_qubits = max (self ._module .num_qubits , phys_idx + 1 )
970+ return phys_idx
971+
972+ @staticmethod
973+ def _get_qubit_name_and_id (
974+ qubit : qasm3_ast .IndexedIdentifier | qasm3_ast .Identifier ,
975+ ) -> tuple [str , int ]:
976+ """Return (register_name, qubit_index) for virtual or physical qubits.
977+
978+ Physical qubits are represented as ``Identifier("$n")`` and carry their
979+ index in the name itself. Virtual qubits are ``IndexedIdentifier`` with
980+ an explicit index in ``.indices``.
981+ """
982+ if isinstance (qubit , qasm3_ast .Identifier ):
983+ # Physical qubit: name is "$n", index is n.
984+ return qubit .name , int (qubit .name [1 :])
985+ assert isinstance (qubit .indices [0 ], list )
986+ qubit_id = Qasm3ExprEvaluator .evaluate_expression (qubit .indices [0 ][0 ])[0 ] # type: ignore
987+ return qubit .name .name , qubit_id
988+
920989 def _update_qubit_depth_for_gate (
921990 self ,
922- all_targets : list [list [qasm3_ast .IndexedIdentifier ]],
991+ all_targets : list [list [Union [ qasm3_ast .IndexedIdentifier , qasm3_ast . Identifier ] ]],
923992 ctrls : list [qasm3_ast .IndexedIdentifier ],
924993 ):
925994 """Updates the depth of the circuit after applying a broadcasted gate.
@@ -934,18 +1003,14 @@ def _update_qubit_depth_for_gate(
9341003 for qubit_subset in all_targets :
9351004 max_involved_depth = 0
9361005 for qubit in qubit_subset + ctrls :
937- assert isinstance (qubit .indices [0 ], list )
938- _qid_ = qubit .indices [0 ][0 ]
939- qubit_id = Qasm3ExprEvaluator .evaluate_expression (_qid_ )[0 ] # type: ignore
940- qubit_node = self ._module ._qubit_depths [(qubit .name .name , qubit_id )]
1006+ qubit_name , qubit_id = self ._get_qubit_name_and_id (qubit )
1007+ qubit_node = self ._module ._qubit_depths [(qubit_name , qubit_id )]
9411008 qubit_node .num_gates += 1
9421009 max_involved_depth = max (max_involved_depth , qubit_node .depth + 1 )
9431010
9441011 for qubit in qubit_subset + ctrls :
945- assert isinstance (qubit .indices [0 ], list )
946- _qid_ = qubit .indices [0 ][0 ]
947- qubit_id = Qasm3ExprEvaluator .evaluate_expression (_qid_ )[0 ] # type: ignore
948- qubit_node = self ._module ._qubit_depths [(qubit .name .name , qubit_id )]
1012+ qubit_name , qubit_id = self ._get_qubit_name_and_id (qubit )
1013+ qubit_node = self ._module ._qubit_depths [(qubit_name , qubit_id )]
9491014 qubit_node .depth = max_involved_depth
9501015
9511016 # pylint: disable=too-many-branches, too-many-locals
@@ -1043,12 +1108,8 @@ def _visit_basic_gate_operation(
10431108 # get qreg in branching operations
10441109 for qubit_subset in unrolled_targets + [ctrls ]:
10451110 for qubit in qubit_subset :
1046- assert isinstance (qubit .indices , list ) and len (qubit .indices ) > 0
1047- assert isinstance (qubit .indices [0 ], list ) and len (qubit .indices [0 ]) > 0
1048- qubit_idx = Qasm3ExprEvaluator .evaluate_expression (qubit .indices [0 ][0 ])[
1049- 0
1050- ]
1051- self ._is_branch_qubits .add ((qubit .name .name , qubit_idx ))
1111+ qubit_name , qubit_idx = QasmVisitor ._get_qubit_name_and_id (qubit )
1112+ self ._is_branch_qubits .add ((qubit_name , qubit_idx ))
10521113
10531114 # check for duplicate bits
10541115 for final_gate in result :
@@ -1096,9 +1157,9 @@ def _visit_custom_gate_operation(
10961157 ctrls = []
10971158 gate_name : str = operation .name .name
10981159 gate_definition : qasm3_ast .QuantumGateDefinition = self ._custom_gates [gate_name ]
1099- op_qubits : list [qasm3_ast .IndexedIdentifier ] = self . _get_op_bits (
1100- operation , qubits = True
1101- ) # type: ignore [assignment]
1160+ op_qubits : list [Union [ qasm3_ast .IndexedIdentifier , qasm3_ast . Identifier ]] = (
1161+ self . _get_op_bits ( operation , qubits = True )
1162+ )
11021163
11031164 Qasm3Validator .validate_gate_call (operation , gate_definition , len (op_qubits ))
11041165 # we need this because the gates applied inside a gate definition use the
@@ -1164,10 +1225,8 @@ def _visit_custom_gate_operation(
11641225 # get qubit registers in branching operations
11651226 for qubit_subset in [op_qubits ] + [ctrls ]:
11661227 for qubit in qubit_subset :
1167- assert isinstance (qubit .indices , list ) and len (qubit .indices ) > 0
1168- assert isinstance (qubit .indices [0 ], list ) and len (qubit .indices [0 ]) > 0
1169- qubit_idx = Qasm3ExprEvaluator .evaluate_expression (qubit .indices [0 ][0 ])[0 ]
1170- self ._is_branch_qubits .add ((qubit .name .name , qubit_idx ))
1228+ qubit_name , qubit_idx = QasmVisitor ._get_qubit_name_and_id (qubit )
1229+ self ._is_branch_qubits .add ((qubit_name , qubit_idx ))
11711230
11721231 self ._scope_manager .pop_scope ()
11731232 self ._scope_manager .restore_context ()
0 commit comments