Coverage for /home/runner/work/tket/tket/pytket/pytket/qasm/qasm.py: 92%

1057 statements  

« prev     ^ index     » next       coverage.py v7.8.0, created at 2025-05-09 15:08 +0000

1# Copyright Quantinuum 

2# 

3# Licensed under the Apache License, Version 2.0 (the "License"); 

4# you may not use this file except in compliance with the License. 

5# You may obtain a copy of the License at 

6# 

7# http://www.apache.org/licenses/LICENSE-2.0 

8# 

9# Unless required by applicable law or agreed to in writing, software 

10# distributed under the License is distributed on an "AS IS" BASIS, 

11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 

12# See the License for the specific language governing permissions and 

13# limitations under the License. 

14 

15import itertools 

16import os 

17import re 

18import uuid 

19from collections import OrderedDict 

20from collections.abc import Callable, Generator, Iterable, Iterator, Sequence 

21from dataclasses import dataclass 

22from decimal import Decimal 

23from importlib import import_module 

24from itertools import chain, groupby 

25from typing import Any, NewType, TextIO, TypeVar, Union, cast 

26 

27from lark import Discard, Lark, Token, Transformer, Tree 

28from sympy import Expr, Symbol, pi 

29 

30from pytket._tket.circuit import ( 

31 BarrierOp, 

32 ClExpr, 

33 ClExprOp, 

34 Command, 

35 Conditional, 

36 CopyBitsOp, 

37 MultiBitOp, 

38 RangePredicateOp, 

39 SetBitsOp, 

40 WASMOp, 

41 WiredClExpr, 

42) 

43from pytket._tket.unit_id import _TEMP_BIT_NAME, _TEMP_BIT_REG_BASE 

44from pytket.circuit import ( 

45 Bit, 

46 BitRegister, 

47 Circuit, 

48 Op, 

49 OpType, 

50 Qubit, 

51 QubitRegister, 

52 UnitID, 

53) 

54from pytket.circuit.clexpr import ( 

55 _has_reg_output, 

56 check_register_alignments, 

57 wired_clexpr_from_logic_exp, 

58) 

59from pytket.circuit.decompose_classical import _int_to_bools 

60from pytket.circuit.logic_exp import ( 

61 ArgType, 

62 BitLogicExp, 

63 BitWiseOp, 

64 LogicExp, 

65 PredicateExp, 

66 RegEq, 

67 RegLogicExp, 

68 RegNeg, 

69 RegWiseOp, 

70 create_logic_exp, 

71 create_predicate_exp, 

72) 

73from pytket.passes import ( 

74 AutoRebase, 

75 DecomposeBoxes, 

76 RemoveRedundancies, 

77 scratch_reg_resize_pass, 

78) 

79from pytket.qasm.grammar import grammar 

80from pytket.wasm import WasmFileHandler, WasmModuleHandler 

81 

82 

83class QASMParseError(Exception): 

84 """Error while parsing QASM input.""" 

85 

86 def __init__(self, msg: str, line: int | None = None, fname: str | None = None): 

87 self.msg = msg 

88 self.line = line 

89 self.fname = fname 

90 

91 ctx = "" if fname is None else f"\nFile:{fname}: " 

92 ctx += "" if line is None else f"\nLine:{line}. " 

93 

94 super().__init__(f"{msg}{ctx}") 

95 

96 

97class QASMUnsupportedError(Exception): 

98 """ 

99 Error due to QASM input being incompatible with the supported fragment. 

100 """ 

101 

102 

103Value = Union[int, float, str] # noqa: UP007 

104T = TypeVar("T") 

105 

106_BITOPS = {op.value for op in BitWiseOp} 

107_BITOPS.update(("+", "-")) # both are parsed to XOR 

108_REGOPS = {op.value for op in RegWiseOp} 

109 

110Arg = Union[list, str] # noqa: UP007 

111 

112 

113NOPARAM_COMMANDS = { 

114 "CX": OpType.CX, # built-in gate equivalent to "cx" 

115 "cx": OpType.CX, 

116 "x": OpType.X, 

117 "y": OpType.Y, 

118 "z": OpType.Z, 

119 "h": OpType.H, 

120 "s": OpType.S, 

121 "sdg": OpType.Sdg, 

122 "t": OpType.T, 

123 "tdg": OpType.Tdg, 

124 "sx": OpType.SX, 

125 "sxdg": OpType.SXdg, 

126 "cz": OpType.CZ, 

127 "cy": OpType.CY, 

128 "ch": OpType.CH, 

129 "csx": OpType.CSX, 

130 "ccx": OpType.CCX, 

131 "c3x": OpType.CnX, 

132 "c4x": OpType.CnX, 

133 "ZZ": OpType.ZZMax, 

134 "measure": OpType.Measure, 

135 "reset": OpType.Reset, 

136 "id": OpType.noop, 

137 "barrier": OpType.Barrier, 

138 "swap": OpType.SWAP, 

139 "cswap": OpType.CSWAP, 

140} 

141PARAM_COMMANDS = { 

142 "p": OpType.U1, # alias. https://github.com/Qiskit/qiskit-terra/pull/4765 

143 "u": OpType.U3, # alias. https://github.com/Qiskit/qiskit-terra/pull/4765 

144 "U": OpType.U3, # built-in gate equivalent to "u3" 

145 "u3": OpType.U3, 

146 "u2": OpType.U2, 

147 "u1": OpType.U1, 

148 "rx": OpType.Rx, 

149 "rxx": OpType.XXPhase, 

150 "ry": OpType.Ry, 

151 "rz": OpType.Rz, 

152 "RZZ": OpType.ZZPhase, 

153 "rzz": OpType.ZZPhase, 

154 "Rz": OpType.Rz, 

155 "U1q": OpType.PhasedX, 

156 "crz": OpType.CRz, 

157 "crx": OpType.CRx, 

158 "cry": OpType.CRy, 

159 "cu1": OpType.CU1, 

160 "cu3": OpType.CU3, 

161 "Rxxyyzz": OpType.TK2, 

162} 

163 

164NOPARAM_EXTRA_COMMANDS = { 

165 "v": OpType.V, 

166 "vdg": OpType.Vdg, 

167 "cv": OpType.CV, 

168 "cvdg": OpType.CVdg, 

169 "csxdg": OpType.CSXdg, 

170 "bridge": OpType.BRIDGE, 

171 "iswapmax": OpType.ISWAPMax, 

172 "zzmax": OpType.ZZMax, 

173 "ecr": OpType.ECR, 

174 "cs": OpType.CS, 

175 "csdg": OpType.CSdg, 

176} 

177 

178PARAM_EXTRA_COMMANDS = { 

179 "tk2": OpType.TK2, 

180 "iswap": OpType.ISWAP, 

181 "phasediswap": OpType.PhasedISWAP, 

182 "yyphase": OpType.YYPhase, 

183 "xxphase3": OpType.XXPhase3, 

184 "eswap": OpType.ESWAP, 

185 "fsim": OpType.FSim, 

186} 

187 

188N_PARAMS_EXTRA_COMMANDS = { 

189 OpType.TK2: 3, 

190 OpType.ISWAP: 1, 

191 OpType.PhasedISWAP: 2, 

192 OpType.YYPhase: 1, 

193 OpType.XXPhase3: 1, 

194 OpType.ESWAP: 1, 

195 OpType.FSim: 2, 

196} 

197 

198_tk_to_qasm_noparams = {item[1]: item[0] for item in NOPARAM_COMMANDS.items()} 

199_tk_to_qasm_noparams[OpType.CX] = "cx" # prefer "cx" to "CX" 

200_tk_to_qasm_params = {item[1]: item[0] for item in PARAM_COMMANDS.items()} 

201_tk_to_qasm_params[OpType.U3] = "u3" # prefer "u3" to "U" 

202_tk_to_qasm_params[OpType.Rz] = "rz" # prefer "rz" to "Rz" 

203_tk_to_qasm_extra_noparams = { 

204 item[1]: item[0] for item in NOPARAM_EXTRA_COMMANDS.items() 

205} 

206_tk_to_qasm_extra_params = {item[1]: item[0] for item in PARAM_EXTRA_COMMANDS.items()} 

207 

208_classical_gatestr_map = {"AND": "&", "OR": "|", "XOR": "^"} 

209 

210 

211_all_known_gates = ( 

212 set(NOPARAM_COMMANDS.keys()) 

213 .union(PARAM_COMMANDS.keys()) 

214 .union(PARAM_EXTRA_COMMANDS.keys()) 

215 .union(NOPARAM_EXTRA_COMMANDS.keys()) 

216) 

217_all_string_maps = { 

218 key: val.name 

219 for key, val in chain( 

220 PARAM_COMMANDS.items(), 

221 NOPARAM_COMMANDS.items(), 

222 PARAM_EXTRA_COMMANDS.items(), 

223 NOPARAM_EXTRA_COMMANDS.items(), 

224 ) 

225} 

226 

227unit_regex = re.compile(r"([a-z][a-zA-Z0-9_]*)\[([\d]+)\]") 

228regname_regex = re.compile(r"^[a-z][a-zA-Z0-9_]*$") 

229 

230 

231def _extract_reg(var: Token) -> tuple[str, int]: 

232 match = unit_regex.match(var.value) 

233 if match is None: 

234 raise QASMParseError( 

235 f"Invalid register definition '{var.value}'. Register definitions " 

236 "must follow the pattern '<name> [<size in integer>]'. " 

237 "For example, 'q [5]'. QASM register names must begin with a " 

238 "lowercase letter and may only contain lowercase and uppercase " 

239 "letters, numbers, and underscores." 

240 ) 

241 return match.group(1), int(match.group(2)) 

242 

243 

244def _load_include_module( 

245 header_name: str, flter: bool, decls_only: bool 

246) -> dict[str, dict]: 

247 try: 

248 if decls_only: 

249 include_def: dict[str, dict] = import_module( # noqa: SLF001 

250 f"pytket.qasm.includes._{header_name}_decls" 

251 )._INCLUDE_DECLS 

252 else: 

253 include_def = import_module( # noqa: SLF001 

254 f"pytket.qasm.includes._{header_name}_defs" 

255 )._INCLUDE_DEFS 

256 except ModuleNotFoundError as e: 

257 raise QASMParseError( 

258 f"Header {header_name} is not known and cannot be loaded." 

259 ) from e 

260 return { 

261 gate: include_def[gate] 

262 for gate in include_def 

263 if not flter or gate not in _all_known_gates 

264 } 

265 

266 

267def _bin_par_exp(op: "str") -> Callable[["_CircuitTransformer", list[str]], str]: 

268 def f(self: "_CircuitTransformer", vals: list[str]) -> str: 

269 return f"({vals[0]} {op} {vals[1]})" 

270 

271 return f 

272 

273 

274def _un_par_exp(op: "str") -> Callable[["_CircuitTransformer", list[str]], str]: 

275 def f(self: "_CircuitTransformer", vals: list[str]) -> str: 

276 return f"({op}{vals[0]})" 

277 

278 return f 

279 

280 

281def _un_call_exp(op: "str") -> Callable[["_CircuitTransformer", list[str]], str]: 

282 def f(self: "_CircuitTransformer", vals: list[str]) -> str: 

283 return f"{op}({vals[0]})" 

284 

285 return f 

286 

287 

288def _hashable_uid(arg: list) -> tuple[str, int]: 

289 return arg[0], arg[1][0] 

290 

291 

292def _can_treat_as_bit(arg: ArgType) -> bool: 

293 if isinstance(arg, Bit | BitLogicExp): 

294 return True 

295 if isinstance(arg, int): 

296 return arg in (0, 1) 

297 if isinstance(arg, BitRegister): 

298 return arg.size == 1 

299 return False 

300 

301 

302Reg = NewType("Reg", str) 

303CommandDict = dict[str, Any] 

304 

305 

306@dataclass 

307class _ParsMap: 

308 pars: Iterable[str] 

309 

310 def __iter__(self) -> Iterable[str]: 

311 return self.pars 

312 

313 

314class _CircuitTransformer(Transformer): 

315 def __init__( 

316 self, 

317 return_gate_dict: bool = False, 

318 maxwidth: int = 32, 

319 ) -> None: 

320 super().__init__() 

321 self.q_registers: dict[str, int] = {} 

322 self.c_registers: dict[str, int] = {} 

323 self.gate_dict: dict[str, dict] = {} 

324 self.wasm: WasmModuleHandler | None = None 

325 self.include = "" 

326 self.return_gate_dict = return_gate_dict 

327 self.maxwidth = maxwidth 

328 

329 def _fresh_temp_bit(self) -> list: 

330 if _TEMP_BIT_NAME in self.c_registers: 

331 idx = self.c_registers[_TEMP_BIT_NAME] 

332 else: 

333 idx = 0 

334 self.c_registers[_TEMP_BIT_NAME] = idx + 1 

335 

336 return [_TEMP_BIT_NAME, [idx]] 

337 

338 def _reset_context(self, reset_wasm: bool = True) -> None: 

339 self.q_registers = {} 

340 self.c_registers = {} 

341 self.gate_dict = {} 

342 self.include = "" 

343 if reset_wasm: 

344 self.wasm = None 

345 

346 def _get_reg(self, name: str) -> Reg: 

347 return Reg(name) 

348 

349 def _get_uid(self, iarg: Token) -> list: 

350 name, idx = _extract_reg(iarg) 

351 return [name, [idx]] 

352 

353 def _get_arg(self, arg: Token) -> Arg: 

354 if arg.type == "IARG": 

355 return self._get_uid(arg) 

356 return self._get_reg(arg.value) 

357 

358 def unroll_all_args(self, args: Iterable[Arg]) -> Iterator[list[Any]]: 

359 for arg in args: 

360 if isinstance(arg, str): 

361 size = ( 

362 self.q_registers[arg] 

363 if arg in self.q_registers 

364 else self.c_registers[arg] 

365 ) 

366 yield [[arg, [idx]] for idx in range(size)] 

367 else: 

368 yield [arg] 

369 

370 def margs(self, tree: Iterable[Token]) -> Iterator[Arg]: 

371 return map(self._get_arg, tree) 

372 

373 def iargs(self, tree: Iterable[Token]) -> Iterator[list]: 

374 return map(self._get_uid, tree) 

375 

376 def args(self, tree: Iterable[Token]) -> Iterator[list]: 

377 return ([tok.value, [0]] for tok in tree) 

378 

379 def creg(self, tree: list[Token]) -> None: 

380 name, size = _extract_reg(tree[0]) 

381 if size > self.maxwidth: 

382 raise QASMUnsupportedError( 

383 f"Circuit contains classical register {name} of size {size} > " 

384 f"{self.maxwidth}: try setting the `maxwidth` parameter to a larger " 

385 "value." 

386 ) 

387 self.c_registers[Reg(name)] = size 

388 

389 def qreg(self, tree: list[Token]) -> None: 

390 name, size = _extract_reg(tree[0]) 

391 self.q_registers[Reg(name)] = size 

392 

393 def meas(self, tree: list[Token]) -> Iterable[CommandDict]: 

394 for args in zip(*self.unroll_all_args(self.margs(tree)), strict=False): 

395 yield {"args": list(args), "op": {"type": "Measure"}} 

396 

397 def barr(self, tree: list[Arg]) -> Iterable[CommandDict]: 

398 args = [q for qs in self.unroll_all_args(tree[0]) for q in qs] 

399 signature: list[str] = [] 

400 for arg in args: 

401 if arg[0] in self.c_registers: 

402 signature.append("C") 

403 elif arg[0] in self.q_registers: 403 ↛ 406line 403 didn't jump to line 406 because the condition on line 403 was always true

404 signature.append("Q") 

405 else: 

406 raise QASMParseError( 

407 "UnitID " + str(arg) + " in Barrier arguments is not declared." 

408 ) 

409 yield { 

410 "args": args, 

411 "op": {"signature": signature, "type": "Barrier"}, 

412 } 

413 

414 def reset(self, tree: list[Token]) -> Iterable[CommandDict]: 

415 for qb in next(self.unroll_all_args(self.margs(tree))): 

416 yield {"args": [qb], "op": {"type": "Reset"}} 

417 

418 def pars(self, vals: Iterable[str]) -> _ParsMap: 

419 return _ParsMap(map(str, vals)) 

420 

421 def mixedcall(self, tree: list) -> Iterator[CommandDict]: 

422 child_iter = iter(tree) 

423 

424 optoken = next(child_iter) 

425 opstr = optoken.value 

426 next_tree = next(child_iter) 

427 try: 

428 args = next(child_iter) 

429 pars = cast("_ParsMap", next_tree).pars 

430 except StopIteration: 

431 args = next_tree 

432 pars = [] 

433 

434 treat_as_barrier = [ 

435 "sleep", 

436 "order2", 

437 "order3", 

438 "order4", 

439 "order5", 

440 "order6", 

441 "order7", 

442 "order8", 

443 "order9", 

444 "order10", 

445 "order11", 

446 "order12", 

447 "order13", 

448 "order14", 

449 "order15", 

450 "order16", 

451 "order17", 

452 "order18", 

453 "order19", 

454 "order20", 

455 "group2", 

456 "group3", 

457 "group4", 

458 "group5", 

459 "group6", 

460 "group7", 

461 "group8", 

462 "group9", 

463 "group10", 

464 "group11", 

465 "group12", 

466 "group13", 

467 "group14", 

468 "group15", 

469 "group16", 

470 "group17", 

471 "group18", 

472 "group19", 

473 "group20", 

474 ] 

475 # other opaque gates, which are not handled as barrier 

476 # ["RZZ", "Rxxyyzz", "Rxxyyzz_zphase", "cu", "cp", "rccx", "rc3x", "c3sqrtx"] 

477 

478 args = list(args) 

479 

480 if opstr in treat_as_barrier: 

481 params = [f"{par}" for par in pars] 

482 else: 

483 params = [f"({par})/pi" for par in pars] 

484 if opstr in self.gate_dict: 

485 op: dict[str, Any] = {} 

486 if opstr in treat_as_barrier: 

487 op["type"] = "Barrier" 

488 param_sorted = ",".join(params) 

489 

490 op["data"] = f"{opstr}({param_sorted})" 

491 

492 op["signature"] = [arg[0] for arg in args] 

493 else: 

494 gdef = self.gate_dict[opstr] 

495 op["type"] = "CustomGate" 

496 box = { 

497 "type": "CustomGate", 

498 "id": str(uuid.uuid4()), 

499 "gate": gdef, 

500 } 

501 box["params"] = params 

502 op["box"] = box 

503 params = [] # to stop duplication in to op 

504 else: 

505 try: 

506 optype = _all_string_maps[opstr] 

507 except KeyError as e: 

508 raise QASMParseError( 

509 f"Cannot parse gate of type: {opstr}", optoken.line 

510 ) from e 

511 op = {"type": optype} 

512 if params: 

513 op["params"] = params 

514 # Operations needing special handling: 

515 if optype.startswith("Cn"): 

516 # n-controlled rotations have variable signature 

517 op["n_qb"] = len(args) 

518 elif optype == "Barrier": 

519 op["signature"] = ["Q"] * len(args) 

520 

521 for arg in zip(*self.unroll_all_args(args), strict=False): 

522 yield {"args": list(arg), "op": op} 

523 

524 def gatecall(self, tree: list) -> Iterable[CommandDict]: 

525 return self.mixedcall(tree) 

526 

527 def exp_args(self, tree: Iterable[Token]) -> Iterable[Reg]: 

528 for arg in tree: 

529 if arg.type == "ARG": 529 ↛ 532line 529 didn't jump to line 532 because the condition on line 529 was always true

530 yield self._get_reg(arg.value) 

531 else: 

532 raise QASMParseError( 

533 "Non register arguments not supported for extern call.", arg.line 

534 ) 

535 

536 def _logic_exp(self, tree: list, opstr: str) -> LogicExp: 

537 args, line = self._get_logic_args(tree) 

538 openum: type[BitWiseOp] | type[RegWiseOp] 

539 if opstr in _BITOPS and opstr not in _REGOPS: 539 ↛ 540line 539 didn't jump to line 540 because the condition on line 539 was never true

540 openum = BitWiseOp 

541 elif (opstr in _REGOPS and opstr not in _BITOPS) or all( 

542 isinstance(arg, int) for arg in args 

543 ): 

544 openum = RegWiseOp 

545 elif all(_can_treat_as_bit(arg) for arg in args): 

546 openum = BitWiseOp 

547 else: 

548 openum = RegWiseOp 

549 if openum is BitWiseOp and opstr in ("+", "-"): 

550 op: BitWiseOp | RegWiseOp = BitWiseOp.XOR 

551 else: 

552 op = openum(opstr) 

553 return create_logic_exp(op, args) 

554 

555 def _get_logic_args( 

556 self, tree: Sequence[Token | LogicExp] 

557 ) -> tuple[list[LogicExp | Bit | BitRegister | int], int | None]: 

558 args: list[LogicExp | Bit | BitRegister | int] = [] 

559 line = None 

560 for tok in tree: 

561 if isinstance(tok, LogicExp): 

562 args.append(tok) 

563 elif isinstance(tok, Token): 563 ↛ 574line 563 didn't jump to line 574 because the condition on line 563 was always true

564 line = tok.line 

565 if tok.type == "INT": 

566 args.append(int(tok.value)) 

567 elif tok.type == "IARG": 

568 args.append(Bit(*_extract_reg(tok))) 

569 elif tok.type == "ARG": 569 ↛ 572line 569 didn't jump to line 572 because the condition on line 569 was always true

570 args.append(BitRegister(tok.value, self.c_registers[tok.value])) 

571 else: 

572 raise QASMParseError(f"Could not pass argument {tok}") 

573 else: 

574 raise QASMParseError(f"Could not pass argument {tok}") 

575 return args, line 

576 

577 par_add = _bin_par_exp("+") 

578 par_sub = _bin_par_exp("-") 

579 par_mul = _bin_par_exp("*") 

580 par_div = _bin_par_exp("/") 

581 par_pow = _bin_par_exp("**") 

582 

583 par_neg = _un_par_exp("-") 

584 

585 sqrt = _un_call_exp("sqrt") 

586 sin = _un_call_exp("sin") 

587 cos = _un_call_exp("cos") 

588 tan = _un_call_exp("tan") 

589 ln = _un_call_exp("ln") 

590 

591 b_and = lambda self, tree: self._logic_exp(tree, "&") 

592 b_not = lambda self, tree: self._logic_exp(tree, "~") 

593 b_or = lambda self, tree: self._logic_exp(tree, "|") 

594 xor = lambda self, tree: self._logic_exp(tree, "^") 

595 lshift = lambda self, tree: self._logic_exp(tree, "<<") 

596 rshift = lambda self, tree: self._logic_exp(tree, ">>") 

597 add = lambda self, tree: self._logic_exp(tree, "+") 

598 sub = lambda self, tree: self._logic_exp(tree, "-") 

599 mul = lambda self, tree: self._logic_exp(tree, "*") 

600 div = lambda self, tree: self._logic_exp(tree, "/") 

601 ipow = lambda self, tree: self._logic_exp(tree, "**") 

602 

603 def neg(self, tree: list[Token | LogicExp]) -> RegNeg: 

604 arg = self._get_logic_args(tree)[0][0] 

605 assert isinstance(arg, RegLogicExp | BitRegister | int) 

606 return RegNeg(arg) 

607 

608 def cond(self, tree: list[Token]) -> PredicateExp: 

609 op: BitWiseOp | RegWiseOp 

610 arg: Bit | BitRegister 

611 if tree[1].type == "IARG": 

612 arg = Bit(*_extract_reg(tree[1])) 

613 op = BitWiseOp(str(tree[2])) 

614 else: 

615 arg = BitRegister(tree[1].value, self.c_registers[tree[1].value]) 

616 op = RegWiseOp(str(tree[2])) 

617 

618 return create_predicate_exp(op, [arg, int(tree[3].value)]) 

619 

620 def ifc(self, tree: Sequence) -> Iterable[CommandDict]: 

621 condition = cast("PredicateExp", tree[0]) 

622 

623 var, val = condition.args 

624 condition_bits = [] 

625 

626 if isinstance(var, Bit): 

627 assert condition.op in (BitWiseOp.EQ, BitWiseOp.NEQ) 

628 assert isinstance(val, int) 

629 assert val in (0, 1) 

630 if condition.op == BitWiseOp.NEQ: 

631 condition.op = BitWiseOp.EQ 

632 val = 1 ^ val 

633 condition_bits = [var.to_list()] 

634 

635 else: 

636 assert isinstance(var, BitRegister) 

637 reg_bits = next(self.unroll_all_args([var.name])) 

638 if isinstance(condition, RegEq): 

639 # special case for base qasm 

640 condition_bits = reg_bits 

641 else: 

642 pred_val = cast("int", val) 

643 minval = 0 

644 maxval = (1 << self.maxwidth) - 1 

645 if condition.op == RegWiseOp.LT: 

646 maxval = pred_val - 1 

647 elif condition.op == RegWiseOp.GT: 

648 minval = pred_val + 1 

649 if condition.op in (RegWiseOp.LEQ, RegWiseOp.EQ, RegWiseOp.NEQ): 

650 maxval = pred_val 

651 if condition.op in (RegWiseOp.GEQ, RegWiseOp.EQ, RegWiseOp.NEQ): 

652 minval = pred_val 

653 

654 condition_bit = self._fresh_temp_bit() 

655 yield { 

656 "args": [*reg_bits, condition_bit], 

657 "op": { 

658 "classical": { 

659 "lower": minval, 

660 "n_i": len(reg_bits), 

661 "upper": maxval, 

662 }, 

663 "type": "RangePredicate", 

664 }, 

665 } 

666 condition_bits = [condition_bit] 

667 val = int(condition.op != RegWiseOp.NEQ) 

668 

669 for com in filter(lambda x: x is not None and x is not Discard, tree[1]): 

670 com["args"] = condition_bits + com["args"] 

671 com["op"] = { 

672 "conditional": { 

673 "op": com["op"], 

674 "value": val, 

675 "width": len(condition_bits), 

676 }, 

677 "type": "Conditional", 

678 } 

679 

680 yield com 

681 

682 def cop(self, tree: Sequence[Iterable[CommandDict]]) -> Iterable[CommandDict]: 

683 return tree[0] 

684 

685 def _calc_exp_io( 

686 self, exp: LogicExp, out_args: list 

687 ) -> tuple[list[list], dict[str, Any]]: 

688 all_inps: list[tuple[str, int]] = [] 

689 for inp in exp.all_inputs_ordered(): 

690 if isinstance(inp, Bit): 

691 all_inps.append((inp.reg_name, inp.index[0])) 

692 else: 

693 assert isinstance(inp, BitRegister) 

694 for bit in inp: 

695 all_inps.append((bit.reg_name, bit.index[0])) # noqa: PERF401 

696 outs = (_hashable_uid(arg) for arg in out_args) 

697 o = [] 

698 io = [] 

699 for out in outs: 

700 if out in all_inps: 

701 all_inps.remove(out) 

702 io.append(out) 

703 else: 

704 o.append(out) 

705 

706 exp_args = [[x[0], [x[1]]] for x in chain.from_iterable((all_inps, io, o))] 

707 numbers_dict = { 

708 "n_i": len(all_inps), 

709 "n_io": len(io), 

710 "n_o": len(o), 

711 } 

712 return exp_args, numbers_dict 

713 

714 def _clexpr_dict(self, exp: LogicExp, out_args: list[list]) -> CommandDict: 

715 # Convert the LogicExp to a serialization of a command containing the 

716 # corresponding ClExprOp. 

717 wexpr, args = wired_clexpr_from_logic_exp( 

718 exp, [Bit.from_list(arg) for arg in out_args] 

719 ) 

720 return { 

721 "op": { 

722 "type": "ClExpr", 

723 "expr": wexpr.to_dict(), 

724 }, 

725 "args": [arg.to_list() for arg in args], 

726 } 

727 

728 def assign(self, tree: list) -> Iterable[CommandDict]: # noqa: PLR0912 

729 child_iter = iter(tree) 

730 out_args = list(next(child_iter)) 

731 args_uids = list(self.unroll_all_args(out_args)) 

732 

733 exp_tree = next(child_iter) 

734 

735 exp: str | list | LogicExp | int = "" 

736 line = None 

737 if isinstance(exp_tree, Token): 

738 if exp_tree.type == "INT": 

739 exp = int(exp_tree.value) 

740 elif exp_tree.type in ("ARG", "IARG"): 740 ↛ 742line 740 didn't jump to line 742 because the condition on line 740 was always true

741 exp = self._get_arg(exp_tree) 

742 line = exp_tree.line 

743 elif isinstance(exp_tree, Generator): 

744 # assume to be extern (wasm) call 

745 chained_uids = list(chain.from_iterable(args_uids)) 

746 com = next(exp_tree) 

747 com["args"].pop() # remove the wasmstate from the args 

748 com["args"] += chained_uids 

749 com["args"].append(["_w", [0]]) 

750 com["op"]["wasm"]["n"] += len(chained_uids) 

751 com["op"]["wasm"]["width_o_parameter"] = [ 

752 self.c_registers[reg] for reg in out_args 

753 ] 

754 

755 yield com 

756 return 

757 else: 

758 exp = exp_tree 

759 

760 assert len(out_args) == 1 

761 out_arg = out_args[0] 

762 args = args_uids[0] 

763 if isinstance(out_arg, list): 

764 if isinstance(exp, LogicExp): 

765 yield self._clexpr_dict(exp, args) 

766 elif isinstance(exp, int | bool): 

767 assert exp in (0, 1, True, False) 

768 yield { 

769 "args": args, 

770 "op": {"classical": {"values": [bool(exp)]}, "type": "SetBits"}, 

771 } 

772 elif isinstance(exp, list): 772 ↛ 778line 772 didn't jump to line 778 because the condition on line 772 was always true

773 yield { 

774 "args": [exp, *args], 

775 "op": {"classical": {"n_i": 1}, "type": "CopyBits"}, 

776 } 

777 else: 

778 raise QASMParseError(f"Unexpected expression in assignment {exp}", line) 

779 else: 

780 reg = out_arg 

781 if isinstance(exp, RegLogicExp): 

782 yield self._clexpr_dict(exp, args) 

783 elif isinstance(exp, BitLogicExp): 

784 yield self._clexpr_dict(exp, args[:1]) 

785 elif isinstance(exp, int): 

786 yield { 

787 "args": args, 

788 "op": { 

789 "classical": { 

790 "values": _int_to_bools(exp, self.c_registers[reg]) 

791 }, 

792 "type": "SetBits", 

793 }, 

794 } 

795 

796 elif isinstance(exp, str): 796 ↛ 804line 796 didn't jump to line 804 because the condition on line 796 was always true

797 width = min(self.c_registers[exp], len(args)) 

798 yield { 

799 "args": [[exp, [i]] for i in range(width)] + args[:width], 

800 "op": {"classical": {"n_i": width}, "type": "CopyBits"}, 

801 } 

802 

803 else: 

804 raise QASMParseError(f"Unexpected expression in assignment {exp}", line) 

805 

806 def extern(self, tree: list[Any]) -> Any: 

807 # TODO parse extern defs 

808 return Discard 

809 

810 def ccall(self, tree: list) -> Iterable[CommandDict]: 

811 return self.cce_call(tree) 

812 

813 def cce_call(self, tree: list) -> Iterable[CommandDict]: 

814 nam = tree[0].value 

815 params = list(tree[1]) 

816 if self.wasm is None: 816 ↛ 817line 816 didn't jump to line 817 because the condition on line 816 was never true

817 raise QASMParseError( 

818 "Cannot include extern calls without a wasm module specified.", 

819 tree[0].line, 

820 ) 

821 n_i_vec = [self.c_registers[reg] for reg in params] 

822 

823 wasm_args = list(chain.from_iterable(self.unroll_all_args(params))) 

824 

825 wasm_args.append(["_w", [0]]) 

826 

827 yield { 

828 "args": wasm_args, 

829 "op": { 

830 "type": "WASM", 

831 "wasm": { 

832 "func_name": nam, 

833 "ww_n": 1, 

834 "n": sum(n_i_vec), 

835 "width_i_parameter": n_i_vec, 

836 "width_o_parameter": [], # this will be set in the assign function 

837 "wasm_file_uid": str(self.wasm), 

838 }, 

839 }, 

840 } 

841 

842 def transform(self, tree: Tree) -> dict[str, Any]: 

843 self._reset_context() 

844 return cast("dict[str, Any]", super().transform(tree)) 

845 

846 def gdef(self, tree: list) -> None: 

847 child_iter = iter(tree) 

848 gate = next(child_iter).value 

849 next_tree = next(child_iter) 

850 symbols, args = [], [] 

851 if isinstance(next_tree, _ParsMap): 

852 symbols = list(next_tree.pars) 

853 args = list(next(child_iter)) 

854 else: 

855 args = list(next_tree) 

856 

857 symbol_map = {sym: sym * pi for sym in map(Symbol, symbols)} 

858 rename_map = {Qubit.from_list(qb): Qubit("q", i) for i, qb in enumerate(args)} 

859 

860 new = _CircuitTransformer(maxwidth=self.maxwidth) 

861 circ_dict = new.prog(child_iter) 

862 

863 circ_dict["qubits"] = args 

864 gate_circ = Circuit.from_dict(circ_dict) 

865 

866 # check to see whether gate definition was generated by pytket converter 

867 # if true, add op as pytket Op 

868 existing_op: bool = False 

869 # NOPARAM_EXTRA_COMMANDS and PARAM_EXTRA_COMMANDS are 

870 # gates that aren't in the standard qasm spec but in the standard TKET 

871 # optypes 

872 if gate in NOPARAM_EXTRA_COMMANDS: 

873 qubit_args = [ 

874 Qubit(gate + "q" + str(index), 0) for index in list(range(len(args))) 

875 ] 

876 comparison_circ = _get_gate_circuit( 

877 NOPARAM_EXTRA_COMMANDS[gate], qubit_args 

878 ) 

879 if circuit_to_qasm_str( 

880 comparison_circ, maxwidth=self.maxwidth 

881 ) == circuit_to_qasm_str(gate_circ, maxwidth=self.maxwidth): 

882 existing_op = True 

883 elif gate in PARAM_EXTRA_COMMANDS: 

884 optype = PARAM_EXTRA_COMMANDS[gate] 

885 # we check this here, as _get_gate_circuit will find issue if it isn't true 

886 # the later existing_op=all check will make sure it's the same circuit later 

887 if len(symbols) != N_PARAMS_EXTRA_COMMANDS[optype]: 

888 existing_op = False 

889 else: 

890 qubit_args = [ 

891 Qubit(gate + "q" + str(index), 0) for index in range(len(args)) 

892 ] 

893 comparison_circ = _get_gate_circuit( 

894 optype, 

895 qubit_args, 

896 [ 

897 Symbol("param" + str(index) + "/pi") 

898 for index in range(len(symbols)) 

899 ], 

900 ) 

901 # checks that each command has same string 

902 existing_op = all( 

903 str(g) == str(c) 

904 for g, c in zip( 

905 gate_circ.get_commands(), 

906 comparison_circ.get_commands(), 

907 strict=False, 

908 ) 

909 ) 

910 if not existing_op: 

911 gate_circ.symbol_substitution(symbol_map) 

912 gate_circ.rename_units(cast("dict[UnitID, UnitID]", rename_map)) 

913 self.gate_dict[gate] = { 

914 "definition": gate_circ.to_dict(), 

915 "args": symbols, 

916 "name": gate, 

917 } 

918 

919 opaq = gdef 

920 

921 def oqasm(self, tree: list) -> Any: 

922 return Discard 

923 

924 def incl(self, tree: list[Token]) -> None: 

925 self.include = str(tree[0].value).split(".")[0] 

926 self.gate_dict.update(_load_include_module(self.include, True, False)) 

927 

928 def prog(self, tree: Iterable) -> dict[str, Any]: 

929 outdict: dict[str, Any] = { 

930 "commands": list( 

931 chain.from_iterable( 

932 filter(lambda x: x is not None and x is not Discard, tree) 

933 ) 

934 ) 

935 } 

936 if self.return_gate_dict: 

937 return self.gate_dict 

938 outdict["qubits"] = [ 

939 [reg, [i]] for reg, size in self.q_registers.items() for i in range(size) 

940 ] 

941 outdict["bits"] = [ 

942 [reg, [i]] for reg, size in self.c_registers.items() for i in range(size) 

943 ] 

944 outdict["implicit_permutation"] = [[q, q] for q in outdict["qubits"]] 

945 outdict["phase"] = "0.0" 

946 self._reset_context() 

947 return outdict 

948 

949 

950def _parser(maxwidth: int) -> Lark: 

951 return Lark( 

952 grammar, 

953 start="prog", 

954 debug=False, 

955 parser="lalr", 

956 cache=True, 

957 transformer=_CircuitTransformer(maxwidth=maxwidth), 

958 ) 

959 

960 

961g_parser = None 

962g_maxwidth = 32 

963 

964 

965def _set_parser(maxwidth: int) -> None: 

966 global g_parser, g_maxwidth # noqa: PLW0603 

967 if (g_parser is None) or (g_maxwidth != maxwidth): # type: ignore 

968 g_parser = _parser(maxwidth=maxwidth) 

969 g_maxwidth = maxwidth 

970 

971 

972def circuit_from_qasm( 

973 input_file: Union[str, "os.PathLike[Any]"], 

974 encoding: str = "utf-8", 

975 maxwidth: int = 32, 

976) -> Circuit: 

977 """A method to generate a tket Circuit from a qasm file. 

978 

979 :param input_file: path to qasm file; filename must have ``.qasm`` extension 

980 :param encoding: file encoding (default utf-8) 

981 :param maxwidth: maximum allowed width of classical registers (default 32) 

982 :return: pytket circuit 

983 """ 

984 ext = os.path.splitext(input_file)[-1] 

985 if ext != ".qasm": 985 ↛ 986line 985 didn't jump to line 986 because the condition on line 985 was never true

986 raise TypeError("Can only convert .qasm files") 

987 with open(input_file, encoding=encoding) as f: 

988 try: 

989 circ = circuit_from_qasm_io(f, maxwidth=maxwidth) 

990 except QASMParseError as e: 

991 raise QASMParseError(e.msg, e.line, str(input_file)) # noqa: B904 

992 return circ 

993 

994 

995def circuit_from_qasm_str(qasm_str: str, maxwidth: int = 32) -> Circuit: 

996 """A method to generate a tket Circuit from a qasm string. 

997 

998 :param qasm_str: qasm string 

999 :param maxwidth: maximum allowed width of classical registers (default 32) 

1000 :return: pytket circuit 

1001 """ 

1002 global g_parser # noqa: PLW0602 

1003 _set_parser(maxwidth=maxwidth) 

1004 assert g_parser is not None 

1005 cast("_CircuitTransformer", g_parser.options.transformer)._reset_context( # noqa: SLF001 

1006 reset_wasm=False 

1007 ) 

1008 

1009 circ = Circuit.from_dict(g_parser.parse(qasm_str)) # type: ignore[arg-type] 

1010 cpass = scratch_reg_resize_pass(maxwidth) 

1011 cpass.apply(circ) 

1012 return circ 

1013 

1014 

1015def circuit_from_qasm_io(stream_in: TextIO, maxwidth: int = 32) -> Circuit: 

1016 """A method to generate a tket Circuit from a qasm text stream""" 

1017 return circuit_from_qasm_str(stream_in.read(), maxwidth=maxwidth) 

1018 

1019 

1020def circuit_from_qasm_wasm( 

1021 input_file: Union[str, "os.PathLike[Any]"], 

1022 wasm_file: Union[str, "os.PathLike[Any]"], 

1023 encoding: str = "utf-8", 

1024 maxwidth: int = 32, 

1025) -> Circuit: 

1026 """A method to generate a tket Circuit from a qasm string and external WASM module. 

1027 

1028 :param input_file: path to qasm file; filename must have ``.qasm`` extension 

1029 :param wasm_file: path to WASM file containing functions used in qasm 

1030 :param encoding: encoding of qasm file (default utf-8) 

1031 :param maxwidth: maximum allowed width of classical registers (default 32) 

1032 :return: pytket circuit 

1033 """ 

1034 global g_parser # noqa: PLW0602 

1035 wasm_module = WasmFileHandler(str(wasm_file)) 

1036 _set_parser(maxwidth=maxwidth) 

1037 assert g_parser is not None 

1038 cast("_CircuitTransformer", g_parser.options.transformer).wasm = wasm_module 

1039 return circuit_from_qasm(input_file, encoding=encoding, maxwidth=maxwidth) 

1040 

1041 

1042def circuit_from_qasm_str_wasm( 

1043 qasm_str: str, 

1044 wasm: bytes, 

1045 maxwidth: int = 32, 

1046) -> Circuit: 

1047 """A method to generate a tket Circuit from a qasm string and external WASM module. 

1048 

1049 :param qasm_str: qasm string 

1050 :param wasm: bytes of the corresponding wasm module 

1051 :param maxwidth: maximum allowed width of classical registers (default 32) 

1052 :return: pytket circuit 

1053 """ 

1054 global g_parser # noqa: PLW0602 

1055 wasm_module = WasmModuleHandler(wasm) 

1056 _set_parser(maxwidth=maxwidth) 

1057 assert g_parser is not None 

1058 cast("_CircuitTransformer", g_parser.options.transformer).wasm = wasm_module 

1059 return circuit_from_qasm_str(qasm_str, maxwidth=maxwidth) 

1060 

1061 

1062def circuit_from_qasm_str_wasmmh( 

1063 qasm_str: str, 

1064 wasmmh: WasmModuleHandler, 

1065 maxwidth: int = 32, 

1066) -> Circuit: 

1067 """A method to generate a tket Circuit from a qasm string and external WASM module. 

1068 

1069 :param qasm_str: qasm string 

1070 :param wasm: handler corresponding to the wasm module 

1071 :param maxwidth: maximum allowed width of classical registers (default 32) 

1072 :return: pytket circuit 

1073 """ 

1074 global g_parser # noqa: PLW0602 

1075 _set_parser(maxwidth=maxwidth) 

1076 assert g_parser is not None 

1077 cast("_CircuitTransformer", g_parser.options.transformer).wasm = wasmmh 

1078 return circuit_from_qasm_str(qasm_str, maxwidth=maxwidth) 

1079 

1080 

1081def circuit_to_qasm( 

1082 circ: Circuit, output_file: str, header: str = "qelib1", maxwidth: int = 32 

1083) -> None: 

1084 """Convert a Circuit to QASM and write it to a file. 

1085 

1086 Classical bits in the pytket circuit must be singly-indexed. 

1087 

1088 Note that this will not account for implicit qubit permutations in the Circuit. 

1089 

1090 :param circ: pytket circuit 

1091 :param output_file: path to output qasm file 

1092 :param header: qasm header (default "qelib1") 

1093 :param maxwidth: maximum allowed width of classical registers (default 32) 

1094 """ 

1095 with open(output_file, "w") as out: 

1096 circuit_to_qasm_io(circ, out, header=header, maxwidth=maxwidth) 

1097 

1098 

1099def _filtered_qasm_str(qasm: str) -> str: 

1100 # remove any c registers starting with _TEMP_BIT_NAME 

1101 # that are not being used somewhere else 

1102 lines = qasm.split("\n") 

1103 def_matcher = re.compile(rf"creg ({_TEMP_BIT_NAME}\_*\d*)\[\d+\]") 

1104 arg_matcher = re.compile(rf"({_TEMP_BIT_NAME}\_*\d*)\[\d+\]") 

1105 unused_regs = {} 

1106 for i, line in enumerate(lines): 

1107 if reg := def_matcher.match(line): 

1108 # Mark a reg temporarily as unused 

1109 unused_regs[reg.group(1)] = i 

1110 elif args := arg_matcher.findall(line): 

1111 # If the line contains scratch bits that are used as arguments 

1112 # mark these regs as used 

1113 for arg in args: 

1114 if arg in unused_regs: 

1115 unused_regs.pop(arg) 

1116 # remove unused reg defs 

1117 redundant_lines = sorted(unused_regs.values(), reverse=True) 

1118 for line_index in redundant_lines: 

1119 del lines[line_index] 

1120 return "\n".join(lines) 

1121 

1122 

1123def _is_empty_customgate(op: Op) -> bool: 

1124 return op.type == OpType.CustomGate and op.get_circuit().n_gates == 0 # type: ignore 

1125 

1126 

1127def _check_can_convert_circuit(circ: Circuit, header: str, maxwidth: int) -> None: 

1128 if any( 

1129 circ.n_gates_of_type(typ) 

1130 for typ in ( 

1131 OpType.RangePredicate, 

1132 OpType.MultiBit, 

1133 OpType.ExplicitPredicate, 

1134 OpType.ExplicitModifier, 

1135 OpType.SetBits, 

1136 OpType.CopyBits, 

1137 ) 

1138 ) and (not _hqs_header(header)): 

1139 raise QASMUnsupportedError( 

1140 "Complex classical gates not supported with qelib1: try converting with " 

1141 "`header=hqslib1`" 

1142 ) 

1143 if any(bit.index[0] >= maxwidth for bit in circ.bits): 

1144 raise QASMUnsupportedError( 

1145 f"Circuit contains a classical register larger than {maxwidth}: try " 

1146 "setting the `maxwidth` parameter to a higher value." 

1147 ) 

1148 set_circ_register = {creg.name for creg in circ.c_registers} 

1149 for b in circ.bits: 

1150 if b.reg_name not in set_circ_register: 

1151 raise QASMUnsupportedError( 

1152 f"Circuit contains an invalid classical register {b.reg_name}." 

1153 ) 

1154 # Empty CustomGates should have been removed by DecomposeBoxes(). 

1155 for cmd in circ: 

1156 assert not _is_empty_customgate(cmd.op) 

1157 if isinstance(cmd.op, Conditional): 

1158 assert not _is_empty_customgate(cmd.op.op) 

1159 if not check_register_alignments(circ): 1159 ↛ 1160line 1159 didn't jump to line 1160 because the condition on line 1159 was never true

1160 raise QASMUnsupportedError( 

1161 "Circuit contains classical expressions on registers whose arguments or " 

1162 "outputs are not register-aligned." 

1163 ) 

1164 

1165 

1166def circuit_to_qasm_str( 

1167 circ: Circuit, 

1168 header: str = "qelib1", 

1169 include_gate_defs: set[str] | None = None, 

1170 maxwidth: int = 32, 

1171) -> str: 

1172 """Convert a Circuit to QASM and return the string. 

1173 

1174 Classical bits in the pytket circuit must be singly-indexed. 

1175 

1176 Note that this will not account for implicit qubit permutations in the Circuit. 

1177 

1178 :param circ: pytket circuit 

1179 :param header: qasm header (default "qelib1") 

1180 :param output_file: path to output qasm file 

1181 :param include_gate_defs: optional set of gates to include 

1182 :param maxwidth: maximum allowed width of classical registers (default 32) 

1183 :return: qasm string 

1184 """ 

1185 

1186 qasm_writer = _QasmWriter( 

1187 circ.qubits, circ.bits, header, include_gate_defs, maxwidth 

1188 ) 

1189 circ1 = circ.copy() 

1190 DecomposeBoxes().apply(circ1) 

1191 _check_can_convert_circuit(circ1, header, maxwidth) 

1192 for command in circ1: 

1193 assert isinstance(command, Command) 

1194 qasm_writer.add_op(command.op, command.args) 

1195 return qasm_writer.finalize() 

1196 

1197 

1198TypeReg = TypeVar("TypeReg", BitRegister, QubitRegister) 

1199 

1200 

1201def _retrieve_registers( 

1202 units: list[UnitID], reg_type: type[TypeReg] 

1203) -> dict[str, TypeReg]: 

1204 if any(len(unit.index) != 1 for unit in units): 

1205 raise NotImplementedError("OPENQASM registers must use a single index") 

1206 maxunits = map(lambda x: max(x[1]), groupby(units, key=lambda un: un.reg_name)) # noqa: C417 

1207 return { 

1208 maxunit.reg_name: reg_type(maxunit.reg_name, maxunit.index[0] + 1) 

1209 for maxunit in maxunits 

1210 } 

1211 

1212 

1213def _parse_range(minval: int, maxval: int, maxwidth: int) -> tuple[str, int]: 

1214 if maxwidth > 64: # noqa: PLR2004 1214 ↛ 1215line 1214 didn't jump to line 1215 because the condition on line 1214 was never true

1215 raise NotImplementedError("Register width exceeds maximum of 64.") 

1216 

1217 REGMAX = (1 << maxwidth) - 1 

1218 

1219 if minval > REGMAX: 1219 ↛ 1220line 1219 didn't jump to line 1220 because the condition on line 1219 was never true

1220 raise NotImplementedError("Range's lower bound exceeds register capacity.") 

1221 if minval > maxval: 1221 ↛ 1222line 1221 didn't jump to line 1222 because the condition on line 1221 was never true

1222 raise NotImplementedError("Range's lower bound exceeds upper bound.") 

1223 maxval = min(maxval, REGMAX) 

1224 

1225 if minval == maxval: 

1226 return ("==", minval) 

1227 if minval == 0: 

1228 return ("<=", maxval) 

1229 if maxval == REGMAX: 1229 ↛ 1231line 1229 didn't jump to line 1231 because the condition on line 1229 was always true

1230 return (">=", minval) 

1231 raise NotImplementedError("Range can only be bounded on one side.") 

1232 

1233 

1234def _negate_comparator(comparator: str) -> str: 

1235 if comparator == "==": 

1236 return "!=" 

1237 if comparator == "!=": 1237 ↛ 1238line 1237 didn't jump to line 1238 because the condition on line 1237 was never true

1238 return "==" 

1239 if comparator == "<=": 

1240 return ">" 

1241 if comparator == ">": 1241 ↛ 1242line 1241 didn't jump to line 1242 because the condition on line 1241 was never true

1242 return "<=" 

1243 if comparator == ">=": 1243 ↛ 1245line 1243 didn't jump to line 1245 because the condition on line 1243 was always true

1244 return "<" 

1245 assert comparator == "<" 

1246 return ">=" 

1247 

1248 

1249def _get_optype_and_params(op: Op) -> tuple[OpType, list[float | Expr] | None]: 

1250 optype = op.type 

1251 params = ( 

1252 op.params 

1253 if (optype in _tk_to_qasm_params) or (optype in _tk_to_qasm_extra_params) 

1254 else None 

1255 ) 

1256 if optype == OpType.TK1: 

1257 # convert to U3 

1258 optype = OpType.U3 

1259 params = [op.params[1], op.params[0] - 0.5, op.params[2] + 0.5] 

1260 elif optype == OpType.CustomGate: 1260 ↛ 1261line 1260 didn't jump to line 1261 because the condition on line 1260 was never true

1261 params = op.params 

1262 return optype, params 

1263 

1264 

1265def _get_gate_circuit( 

1266 optype: OpType, qubits: list[Qubit], symbols: list[Symbol] | None = None 

1267) -> Circuit: 

1268 # create Circuit for constructing qasm from 

1269 unitids = cast("list[UnitID]", qubits) 

1270 gate_circ = Circuit() 

1271 for q in qubits: 

1272 gate_circ.add_qubit(q) 

1273 if symbols: 

1274 exprs = [symbol.as_expr() for symbol in symbols] 

1275 gate_circ.add_gate(optype, exprs, unitids) 

1276 else: 

1277 gate_circ.add_gate(optype, unitids) 

1278 AutoRebase({OpType.CX, OpType.U3}).apply(gate_circ) 

1279 RemoveRedundancies().apply(gate_circ) 

1280 

1281 return gate_circ 

1282 

1283 

1284def _hqs_header(header: str) -> bool: 

1285 return header in ["hqslib1", "hqslib1_dev"] 

1286 

1287 

1288@dataclass 

1289class _ConditionString: 

1290 variable: str # variable, e.g. "c[1]" 

1291 comparator: str # comparator, e.g. "==" 

1292 value: int # value, e.g. "1" 

1293 

1294 

1295class _LabelledStringList: 

1296 """ 

1297 Wrapper class for an ordered sequence of strings, where each string has a unique 

1298 label, returned when the string is added, and a string may be removed from the 

1299 sequence given its label. There is a method to retrieve the concatenation of all 

1300 strings in order. The conditions (e.g. "if(c[0]==1)") for some strings are stored 

1301 separately in `conditions`. These conditions will be converted to text when 

1302 retrieving the full string. 

1303 """ 

1304 

1305 def __init__(self) -> None: 

1306 self.strings: OrderedDict[int, str] = OrderedDict() 

1307 self.conditions: dict[int, _ConditionString] = {} 

1308 self.label = 0 

1309 

1310 def add_string(self, string: str) -> int: 

1311 label = self.label 

1312 self.strings[label] = string 

1313 self.label += 1 

1314 return label 

1315 

1316 def get_string(self, label: int) -> str | None: 

1317 return self.strings.get(label, None) 

1318 

1319 def del_string(self, label: int) -> None: 

1320 self.strings.pop(label, None) 

1321 

1322 def get_full_string(self) -> str: 

1323 strings = [] 

1324 for l, s in self.strings.items(): 

1325 condition = self.conditions.get(l) 

1326 if condition is not None: 

1327 strings.append( 

1328 f"if({condition.variable}{condition.comparator}{condition.value}) " 

1329 + s 

1330 ) 

1331 else: 

1332 strings.append(s) 

1333 return "".join(strings) 

1334 

1335 

1336def _make_params_str(params: list[float | Expr] | None) -> str: 

1337 s = "" 

1338 if params is not None: 1338 ↛ 1357line 1338 didn't jump to line 1357 because the condition on line 1338 was always true

1339 n_params = len(params) 

1340 s += "(" 

1341 for i in range(n_params): 

1342 reduced = True 

1343 try: 

1344 p: float | Expr = float(params[i]) 

1345 except TypeError: 

1346 reduced = False 

1347 p = params[i] 

1348 if i < n_params - 1: 

1349 if reduced: 

1350 s += f"{p}*pi," 

1351 else: 

1352 s += f"({p})*pi," 

1353 elif reduced: 

1354 s += f"{p}*pi)" 

1355 else: 

1356 s += f"({p})*pi)" 

1357 s += " " 

1358 return s 

1359 

1360 

1361def _make_args_str(args: Sequence[UnitID]) -> str: 

1362 s = "" 

1363 for i in range(len(args)): 

1364 s += f"{args[i]}" 

1365 if i < len(args) - 1: 

1366 s += "," 

1367 else: 

1368 s += ";\n" 

1369 return s 

1370 

1371 

1372@dataclass 

1373class _ScratchPredicate: 

1374 variable: str # variable, e.g. "c[1]" 

1375 comparator: str # comparator, e.g. "==" 

1376 value: int # value, e.g. "1" 

1377 dest: str # destination bit, e.g. "tk_SCRATCH_BIT[0]" 

1378 

1379 

1380def _vars_overlap(v: str, w: str) -> bool: 

1381 """check if two variables have overlapping bits""" 

1382 v_split = v.split("[") 

1383 w_split = w.split("[") 

1384 if v_split[0] != w_split[0]: 

1385 # different registers 

1386 return False 

1387 # e.g. (a[1], a), (a, a[1]), (a[1], a[1]), (a, a) 

1388 return len(v_split) != len(w_split) or v == w 

1389 

1390 

1391def _var_appears(v: str, s: str) -> bool: 

1392 """check if variable v appears in string s""" 

1393 v_split = v.split("[") 

1394 if len(v_split) == 1: 1394 ↛ 1397line 1394 didn't jump to line 1397 because the condition on line 1394 was never true

1395 # check if v appears in s and is not surrounded by word characters 

1396 # e.g. a = a & b or a = a[1] & b[1] 

1397 return bool(re.search(r"(?<!\w)" + re.escape(v) + r"(?![\w])", s)) 

1398 if re.search(r"(?<!\w)" + re.escape(v), s): 1398 ↛ 1401line 1398 didn't jump to line 1401 because the condition on line 1398 was never true

1399 # check if v appears in s and is not proceeded by word characters 

1400 # e.g. a[1] = a[1] 

1401 return True 

1402 # check the register of v appears in s 

1403 # e.g. a[1] = a & b 

1404 return bool(re.search(r"(?<!\w)" + re.escape(v_split[0]) + r"(?![\[\w])", s)) 

1405 

1406 

1407class _QasmWriter: 

1408 """ 

1409 Helper class for converting a sequence of TKET Commands to QASM, and retrieving the 

1410 final QASM string afterwards. 

1411 """ 

1412 

1413 def __init__( 

1414 self, 

1415 qubits: list[Qubit], 

1416 bits: list[Bit], 

1417 header: str = "qelib1", 

1418 include_gate_defs: set[str] | None = None, 

1419 maxwidth: int = 32, 

1420 ): 

1421 self.header = header 

1422 self.maxwidth = maxwidth 

1423 self.added_gate_definitions: set[str] = set() 

1424 self.include_module_gates = {"measure", "reset", "barrier"} 

1425 self.include_module_gates.update( 

1426 _load_include_module(header, False, True).keys() 

1427 ) 

1428 self.prefix = "" 

1429 self.gatedefs = "" 

1430 self.strings = _LabelledStringList() 

1431 

1432 # Record of `RangePredicate` operations that set a "scratch" bit to 0 or 1 

1433 # depending on the value of the predicate. This map is consulted when we 

1434 # encounter a `Conditional` operation to see if the condition bit is one of 

1435 # these scratch bits, which we can then replace with the original. 

1436 self.range_preds: dict[int, _ScratchPredicate] = {} 

1437 

1438 if include_gate_defs is None: 

1439 self.include_gate_defs = self.include_module_gates 

1440 self.include_gate_defs.update(NOPARAM_EXTRA_COMMANDS.keys()) 

1441 self.include_gate_defs.update(PARAM_EXTRA_COMMANDS.keys()) 

1442 self.prefix = f'OPENQASM 2.0;\ninclude "{header}.inc";\n\n' 

1443 self.qregs = _retrieve_registers( 

1444 cast("list[UnitID]", qubits), QubitRegister 

1445 ) 

1446 self.cregs = _retrieve_registers(cast("list[UnitID]", bits), BitRegister) 

1447 for reg in self.qregs.values(): 

1448 if regname_regex.match(reg.name) is None: 

1449 raise QASMUnsupportedError( 

1450 f"Invalid register name '{reg.name}'. QASM register names must " 

1451 "begin with a lowercase letter and may only contain lowercase " 

1452 "and uppercase letters, numbers, and underscores. " 

1453 "Try renaming the register with `rename_units` first." 

1454 ) 

1455 for bit_reg in self.cregs.values(): 

1456 if regname_regex.match(bit_reg.name) is None: 1456 ↛ 1457line 1456 didn't jump to line 1457 because the condition on line 1456 was never true

1457 raise QASMUnsupportedError( 

1458 f"Invalid register name '{bit_reg.name}'. QASM register names " 

1459 "must begin with a lowercase letter and may only contain " 

1460 "lowercase and uppercase letters, numbers, and underscores. " 

1461 "Try renaming the register with `rename_units` first." 

1462 ) 

1463 else: 

1464 # gate definition, no header necessary for file 

1465 self.include_gate_defs = include_gate_defs 

1466 self.cregs = {} 

1467 self.qregs = {} 

1468 

1469 self.cregs_as_bitseqs = {tuple(creg) for creg in self.cregs.values()} 

1470 

1471 # for holding condition values when writing Conditional blocks 

1472 # the size changes when adding and removing scratch bits 

1473 self.scratch_reg = BitRegister( 

1474 next( 

1475 f"{_TEMP_BIT_REG_BASE}_{i}" 

1476 for i in itertools.count() 

1477 if f"{_TEMP_BIT_REG_BASE}_{i}" not in self.qregs 

1478 ), 

1479 0, 

1480 ) 

1481 # if a string writes to some classical variables, the string label and 

1482 # the affected variables will be recorded. 

1483 self.variable_writes: dict[int, list[str]] = {} 

1484 

1485 def fresh_scratch_bit(self) -> Bit: 

1486 self.scratch_reg = BitRegister(self.scratch_reg.name, self.scratch_reg.size + 1) 

1487 return Bit(self.scratch_reg.name, self.scratch_reg.size - 1) 

1488 

1489 def remove_last_scratch_bit(self) -> None: 

1490 assert self.scratch_reg.size > 0 

1491 self.scratch_reg = BitRegister(self.scratch_reg.name, self.scratch_reg.size - 1) 

1492 

1493 def write_params(self, params: list[float | Expr] | None) -> None: 

1494 params_str = _make_params_str(params) 

1495 self.strings.add_string(params_str) 

1496 

1497 def write_args(self, args: Sequence[UnitID]) -> None: 

1498 args_str = _make_args_str(args) 

1499 self.strings.add_string(args_str) 

1500 

1501 def make_gate_definition( 

1502 self, 

1503 n_qubits: int, 

1504 opstr: str, 

1505 optype: OpType, 

1506 n_params: int | None = None, 

1507 ) -> str: 

1508 s = "gate " + opstr + " " 

1509 symbols: list[Symbol] | None = None 

1510 if n_params is not None: 

1511 # need to add parameters to gate definition 

1512 s += "(" 

1513 symbols = [ 

1514 Symbol("param" + str(index) + "/pi") for index in range(n_params) 

1515 ] 

1516 symbols_header = [Symbol("param" + str(index)) for index in range(n_params)] 

1517 for symbol in symbols_header[:-1]: 

1518 s += symbol.name + ", " 

1519 s += symbols_header[-1].name + ") " 

1520 

1521 # add qubits to gate definition 

1522 qubit_args = [ 

1523 Qubit(opstr + "q" + str(index)) for index in list(range(n_qubits)) 

1524 ] 

1525 for qb in qubit_args[:-1]: 

1526 s += str(qb) + "," 

1527 s += str(qubit_args[-1]) + " {\n" 

1528 # get rebased circuit for constructing qasm 

1529 gate_circ = _get_gate_circuit(optype, qubit_args, symbols) 

1530 # write circuit to qasm 

1531 s += circuit_to_qasm_str( 

1532 gate_circ, self.header, self.include_gate_defs, self.maxwidth 

1533 ) 

1534 s += "}\n" 

1535 return s 

1536 

1537 def mark_as_written(self, label: int, written_variable: str) -> None: 

1538 if label in self.variable_writes: 1538 ↛ 1539line 1538 didn't jump to line 1539 because the condition on line 1538 was never true

1539 self.variable_writes[label].append(written_variable) 

1540 else: 

1541 self.variable_writes[label] = [written_variable] 

1542 

1543 def check_range_predicate(self, op: RangePredicateOp, args: list[Bit]) -> None: 

1544 if (not _hqs_header(self.header)) and op.lower != op.upper: 1544 ↛ 1545line 1544 didn't jump to line 1545 because the condition on line 1544 was never true

1545 raise QASMUnsupportedError( 

1546 "OpenQASM conditions must be on a register's fixed value." 

1547 ) 

1548 variable = args[0].reg_name 

1549 assert isinstance(variable, str) 

1550 if op.n_inputs != self.cregs[variable].size: 

1551 raise QASMUnsupportedError( 

1552 "RangePredicate conditions must be an entire classical register" 

1553 ) 

1554 if args[:-1] != self.cregs[variable].to_list(): 1554 ↛ 1555line 1554 didn't jump to line 1555 because the condition on line 1554 was never true

1555 raise QASMUnsupportedError( 

1556 "RangePredicate conditions must be a single classical register" 

1557 ) 

1558 

1559 def add_range_predicate(self, op: RangePredicateOp, args: list[Bit]) -> None: 

1560 self.check_range_predicate(op, args) 

1561 comparator, value = _parse_range(op.lower, op.upper, self.maxwidth) 

1562 variable = args[0].reg_name 

1563 dest_bit = str(args[-1]) 

1564 label = self.strings.add_string( 

1565 "".join( 

1566 [ 

1567 f"if({variable}{comparator}{value}) " + f"{dest_bit} = 1;\n", 

1568 f"if({variable}{_negate_comparator(comparator)}{value}) " 

1569 f"{dest_bit} = 0;\n", 

1570 ] 

1571 ) 

1572 ) 

1573 # Record this operation. 

1574 # Later if we find a conditional based on dest_bit, we can replace dest_bit with 

1575 # (variable, comparator, value), provided that variable hasn't been written to 

1576 # in the mean time. (So we must watch for that, and remove the record from the 

1577 # list if it is.) 

1578 # Note that we only perform such rewrites for internal scratch bits. 

1579 if dest_bit.startswith(_TEMP_BIT_NAME): 

1580 self.range_preds[label] = _ScratchPredicate( 

1581 variable, comparator, value, dest_bit 

1582 ) 

1583 

1584 def replace_condition(self, pred_label: int) -> bool: 

1585 """Given the label of a predicate p=(var, comp, value, dest, label) 

1586 we scan the lines after p: 

1587 1.if dest is the condition of a conditional line we replace dest with 

1588 the predicate and do 2 for the inner command. 

1589 2.if either the variable or the dest gets written, we stop. 

1590 returns true if a replacement is made. 

1591 """ 

1592 assert pred_label in self.range_preds 

1593 success = False 

1594 pred = self.range_preds[pred_label] 

1595 line_labels = [] 

1596 for label in range(pred_label + 1, self.strings.label): 

1597 string = self.strings.get_string(label) 

1598 if string is None: 

1599 continue 

1600 line_labels.append(label) 

1601 if "\n" not in string: 

1602 continue 

1603 written_variables: list[str] = [] 

1604 # (label, condition) 

1605 conditions: list[tuple[int, _ConditionString]] = [] 

1606 for l in line_labels: 

1607 written_variables.extend(self.variable_writes.get(l, [])) 

1608 cond = self.strings.conditions.get(l) 

1609 if cond: 

1610 conditions.append((l, cond)) 

1611 if len(conditions) == 1 and pred.dest == conditions[0][1].variable: 

1612 # if the condition is dest, replace the condition with pred 

1613 success = True 

1614 if conditions[0][1].value == 1: 

1615 self.strings.conditions[conditions[0][0]] = _ConditionString( 

1616 pred.variable, pred.comparator, pred.value 

1617 ) 

1618 else: 

1619 assert conditions[0][1].value == 0 

1620 self.strings.conditions[conditions[0][0]] = _ConditionString( 

1621 pred.variable, 

1622 _negate_comparator(pred.comparator), 

1623 pred.value, 

1624 ) 

1625 if any(_vars_overlap(pred.dest, v) for v in written_variables) or any( 

1626 _vars_overlap(pred.variable, v) for v in written_variables 

1627 ): 

1628 return success 

1629 line_labels.clear() 

1630 conditions.clear() 

1631 written_variables.clear() 

1632 return success 

1633 

1634 def remove_unused_predicate(self, pred_label: int) -> bool: 

1635 """Given the label of a predicate p=(var, comp, value, dest, label), 

1636 we remove p if dest never appears after p.""" 

1637 assert pred_label in self.range_preds 

1638 pred = self.range_preds[pred_label] 

1639 for label in range(pred_label + 1, self.strings.label): 

1640 string = self.strings.get_string(label) 

1641 if string is None: 

1642 continue 

1643 if _var_appears(pred.dest, string) or ( 

1644 label in self.strings.conditions 

1645 and _vars_overlap(pred.dest, self.strings.conditions[label].variable) 

1646 ): 

1647 return False 

1648 self.range_preds.pop(pred_label) 

1649 self.strings.del_string(pred_label) 

1650 return True 

1651 

1652 def add_conditional(self, op: Conditional, args: Sequence[UnitID]) -> None: 

1653 control_bits = args[: op.width] 

1654 if op.width == 1 and _hqs_header(self.header): 

1655 variable = str(control_bits[0]) 

1656 else: 

1657 variable = control_bits[0].reg_name 

1658 if ( 

1659 _hqs_header(self.header) 

1660 and control_bits != self.cregs[variable].to_list() 

1661 ): 

1662 raise QASMUnsupportedError( 

1663 "hqslib1 QASM conditions must be an entire classical " 

1664 "register or a single bit" 

1665 ) 

1666 if not _hqs_header(self.header): 

1667 if op.width != self.cregs[variable].size: 

1668 raise QASMUnsupportedError( 

1669 "OpenQASM conditions must be an entire classical register" 

1670 ) 

1671 if control_bits != self.cregs[variable].to_list(): 

1672 raise QASMUnsupportedError( 

1673 "OpenQASM conditions must be a single classical register" 

1674 ) 

1675 if op.op.type == OpType.Phase: 

1676 # Conditional phase is ignored. 

1677 return 

1678 if op.op.type == OpType.RangePredicate: 

1679 # Special handling for nested ifs 

1680 # if condition 

1681 # if pred dest = 1 

1682 # if not pred dest = 0 

1683 # can be written as 

1684 # if condition s0 = 1 

1685 # if pred s1 = 1 

1686 # s2 = s0 & s1 

1687 # s3 = s0 & ~s1 

1688 # if s2 dest = 1 

1689 # if s3 dest = 0 

1690 # where s0, s1, s2, and s3 are scratch bits 

1691 s0 = self.fresh_scratch_bit() 

1692 l = self.strings.add_string(f"{s0} = 1;\n") 

1693 # we store the condition in self.strings.conditions 

1694 # as it can be later replaced by `replace_condition` 

1695 # if possible 

1696 self.strings.conditions[l] = _ConditionString(variable, "==", op.value) 

1697 # output the RangePredicate to s1 

1698 s1 = self.fresh_scratch_bit() 

1699 assert isinstance(op.op, RangePredicateOp) 

1700 self.check_range_predicate(op.op, cast("list[Bit]", args[op.width :])) 

1701 pred_comparator, pred_value = _parse_range( 

1702 op.op.lower, op.op.upper, self.maxwidth 

1703 ) 

1704 pred_variable = args[op.width :][0].reg_name 

1705 self.strings.add_string( 

1706 f"if({pred_variable}{pred_comparator}{pred_value}) {s1} = 1;\n" 

1707 ) 

1708 s2 = self.fresh_scratch_bit() 

1709 self.strings.add_string(f"{s2} = {s0} & {s1};\n") 

1710 s3 = self.fresh_scratch_bit() 

1711 self.strings.add_string(f"{s3} = {s0} & (~ {s1});\n") 

1712 self.strings.add_string(f"if({s2}==1) {args[-1]} = 1;\n") 

1713 self.strings.add_string(f"if({s3}==1) {args[-1]} = 0;\n") 

1714 return 

1715 # we assign the condition to a scratch bit, which we will later remove 

1716 # if the condition variable is unchanged. 

1717 scratch_bit = self.fresh_scratch_bit() 

1718 pred_label = self.strings.add_string( 

1719 f"if({variable}=={op.value}) " + f"{scratch_bit} = 1;\n" 

1720 ) 

1721 self.range_preds[pred_label] = _ScratchPredicate( 

1722 variable, "==", op.value, str(scratch_bit) 

1723 ) 

1724 # we will later add condition to all lines starting from next_label 

1725 next_label = self.strings.label 

1726 self.add_op(op.op, args[op.width :]) 

1727 # add conditions to the lines after the predicate 

1728 is_new_line = True 

1729 for label in range(next_label, self.strings.label): 

1730 string = self.strings.get_string(label) 

1731 assert string is not None 

1732 if is_new_line and string != "\n": 

1733 self.strings.conditions[label] = _ConditionString( 

1734 str(scratch_bit), "==", 1 

1735 ) 

1736 is_new_line = "\n" in string 

1737 if self.replace_condition(pred_label) and self.remove_unused_predicate( 

1738 pred_label 

1739 ): 

1740 # remove the unused scratch bit 

1741 self.remove_last_scratch_bit() 

1742 

1743 def add_set_bits(self, op: SetBitsOp, args: list[Bit]) -> None: 

1744 creg_name = args[0].reg_name 

1745 bits, vals = zip(*sorted(zip(args, op.values, strict=False)), strict=False) 

1746 # check if whole register can be set at once 

1747 if bits == tuple(self.cregs[creg_name].to_list()): 

1748 value = int("".join(map(str, map(int, vals[::-1]))), 2) 

1749 label = self.strings.add_string(f"{creg_name} = {value};\n") 

1750 self.mark_as_written(label, f"{creg_name}") 

1751 else: 

1752 for bit, value in zip(bits, vals, strict=False): 

1753 label = self.strings.add_string(f"{bit} = {int(value)};\n") 

1754 self.mark_as_written(label, f"{bit}") 

1755 

1756 def add_copy_bits(self, op: CopyBitsOp, args: list[Bit]) -> None: 

1757 l_args = args[op.n_inputs :] 

1758 r_args = args[: op.n_inputs] 

1759 l_name = l_args[0].reg_name 

1760 r_name = r_args[0].reg_name 

1761 # check if whole register can be set at once 

1762 if ( 

1763 l_args == self.cregs[l_name].to_list() 

1764 and r_args == self.cregs[r_name].to_list() 

1765 ): 

1766 label = self.strings.add_string(f"{l_name} = {r_name};\n") 

1767 self.mark_as_written(label, f"{l_name}") 

1768 else: 

1769 for bit_l, bit_r in zip(l_args, r_args, strict=False): 

1770 label = self.strings.add_string(f"{bit_l} = {bit_r};\n") 

1771 self.mark_as_written(label, f"{bit_l}") 

1772 

1773 def add_multi_bit(self, op: MultiBitOp, args: list[Bit]) -> None: 

1774 basic_op = op.basic_op 

1775 basic_n = basic_op.n_inputs + basic_op.n_outputs + basic_op.n_input_outputs 

1776 n_args = len(args) 

1777 assert n_args % basic_n == 0 

1778 arity = n_args // basic_n 

1779 

1780 # If the operation is register-aligned we can write it more succinctly. 

1781 poss_regs = [ 

1782 tuple(args[basic_n * i + j] for i in range(arity)) for j in range(basic_n) 

1783 ] 

1784 if all(poss_reg in self.cregs_as_bitseqs for poss_reg in poss_regs): 

1785 # The operation is register-aligned. 

1786 self.add_op(basic_op, [poss_regs[j][0].reg_name for j in range(basic_n)]) # type: ignore 

1787 else: 

1788 # The operation is not register-aligned. 

1789 for i in range(arity): 

1790 basic_args = args[basic_n * i : basic_n * (i + 1)] 

1791 self.add_op(basic_op, basic_args) 

1792 

1793 def add_explicit_op(self, op: Op, args: list[Bit]) -> None: 

1794 # &, ^ and | gates 

1795 opstr = str(op) 

1796 if opstr not in _classical_gatestr_map: 1796 ↛ 1797line 1796 didn't jump to line 1797 because the condition on line 1796 was never true

1797 raise QASMUnsupportedError(f"Classical gate {opstr} not supported.") 

1798 label = self.strings.add_string( 

1799 f"{args[-1]} = {args[0]} {_classical_gatestr_map[opstr]} {args[1]};\n" 

1800 ) 

1801 self.mark_as_written(label, f"{args[-1]}") 

1802 

1803 def add_wired_clexpr(self, op: ClExprOp, args: list[Bit]) -> None: 

1804 wexpr: WiredClExpr = op.expr 

1805 # 1. Determine the mappings from bit variables to bits and from register 

1806 # variables to registers. 

1807 expr: ClExpr = wexpr.expr 

1808 bit_posn: dict[int, int] = wexpr.bit_posn 

1809 reg_posn: dict[int, list[int]] = wexpr.reg_posn 

1810 output_posn: list[int] = wexpr.output_posn 

1811 input_bits: dict[int, Bit] = {i: args[j] for i, j in bit_posn.items()} 

1812 input_regs: dict[int, BitRegister] = {} 

1813 all_cregs = set(self.cregs.values()) 

1814 for i, posns in reg_posn.items(): 

1815 reg_args = [args[j] for j in posns] 

1816 for creg in all_cregs: 1816 ↛ 1821line 1816 didn't jump to line 1821 because the loop on line 1816 didn't complete

1817 if creg.to_list() == reg_args: 

1818 input_regs[i] = creg 

1819 break 

1820 else: 

1821 assert ( 

1822 not f"ClExprOp ({wexpr}) contains a register variable (r{i}) that " 

1823 "is not wired to any BitRegister in the circuit." 

1824 ) 

1825 # 2. Write the left-hand side of the assignment. 

1826 output_repr: str | None = None 

1827 output_args: list[Bit] = [args[j] for j in output_posn] 

1828 n_output_args = len(output_args) 

1829 expect_reg_output = _has_reg_output(expr.op) 

1830 if n_output_args == 0: 1830 ↛ 1831line 1830 didn't jump to line 1831 because the condition on line 1830 was never true

1831 raise QASMUnsupportedError("Expression has no output.") 

1832 if n_output_args == 1: 

1833 output_arg = output_args[0] 

1834 output_repr = output_arg.reg_name if expect_reg_output else str(output_arg) 

1835 else: 

1836 if not expect_reg_output: 1836 ↛ 1837line 1836 didn't jump to line 1837 because the condition on line 1836 was never true

1837 raise QASMUnsupportedError("Unexpected output for operation.") 

1838 for creg in all_cregs: 1838 ↛ 1842line 1838 didn't jump to line 1842 because the loop on line 1838 didn't complete

1839 if creg.to_list() == output_args: 

1840 output_repr = creg.name 

1841 break 

1842 assert output_repr is not None 

1843 self.strings.add_string(f"{output_repr} = ") 

1844 # 3. Write the right-hand side of the assignment. 

1845 self.strings.add_string( 

1846 expr.as_qasm(input_bits=input_bits, input_regs=input_regs) 

1847 ) 

1848 self.strings.add_string(";\n") 

1849 

1850 def add_wasm(self, op: WASMOp, args: list[Bit]) -> None: 

1851 inputs: list[str] = [] 

1852 outputs: list[str] = [] 

1853 for reglist, sizes in [(inputs, op.input_widths), (outputs, op.output_widths)]: 

1854 for in_width in sizes: 

1855 bits = args[:in_width] 

1856 args = args[in_width:] 

1857 regname = bits[0].reg_name 

1858 if bits != list(self.cregs[regname]): 1858 ↛ 1859line 1858 didn't jump to line 1859 because the condition on line 1858 was never true

1859 QASMUnsupportedError("WASM ops must act on entire registers.") 

1860 reglist.append(regname) 

1861 if outputs: 

1862 label = self.strings.add_string(f"{', '.join(outputs)} = ") 

1863 self.strings.add_string(f"{op.func_name}({', '.join(inputs)});\n") 

1864 for variable in outputs: 

1865 self.mark_as_written(label, variable) 

1866 

1867 def add_measure(self, args: Sequence[UnitID]) -> None: 

1868 label = self.strings.add_string(f"measure {args[0]} -> {args[1]};\n") 

1869 self.mark_as_written(label, f"{args[1]}") 

1870 

1871 def add_zzphase(self, param: float | Expr, args: Sequence[UnitID]) -> None: 

1872 # as op.params returns reduced parameters, we can assume 

1873 # that 0 <= param < 4 

1874 if param > 1: 

1875 # first get in to 0 <= param < 2 range 

1876 param = Decimal(str(param)) % Decimal("2") 

1877 # then flip 1 <= param < 2 range into 

1878 # -1 <= param < 0 

1879 if param > 1: 

1880 param = -2 + param 

1881 self.strings.add_string("RZZ") 

1882 self.write_params([param]) 

1883 self.write_args(args) 

1884 

1885 def add_cnx(self, args: Sequence[UnitID]) -> None: 

1886 n_ctrls = len(args) - 1 

1887 assert n_ctrls >= 0 

1888 match n_ctrls: 

1889 case 0: 1889 ↛ 1890line 1889 didn't jump to line 1890 because the pattern on line 1889 never matched

1890 self.strings.add_string("x") 

1891 case 1: 1891 ↛ 1892line 1891 didn't jump to line 1892 because the pattern on line 1891 never matched

1892 self.strings.add_string("cx") 

1893 case 2: 1893 ↛ 1894line 1893 didn't jump to line 1894 because the pattern on line 1893 never matched

1894 self.strings.add_string("ccx") 

1895 case 3: 

1896 self.strings.add_string("c3x") 

1897 case 4: 1897 ↛ 1899line 1897 didn't jump to line 1899 because the pattern on line 1897 always matched

1898 self.strings.add_string("c4x") 

1899 case _: 

1900 raise QASMUnsupportedError("CnX with n > 4 not supported in QASM") 

1901 self.strings.add_string(" ") 

1902 self.write_args(args) 

1903 

1904 def add_data(self, op: BarrierOp, args: Sequence[UnitID]) -> None: 

1905 opstr = _tk_to_qasm_noparams[OpType.Barrier] if op.data == "" else op.data 

1906 self.strings.add_string(opstr) 

1907 self.strings.add_string(" ") 

1908 self.write_args(args) 

1909 

1910 def add_gate_noparams(self, op: Op, args: Sequence[UnitID]) -> None: 

1911 self.strings.add_string(_tk_to_qasm_noparams[op.type]) 

1912 self.strings.add_string(" ") 

1913 self.write_args(args) 

1914 

1915 def add_gate_params(self, op: Op, args: Sequence[UnitID]) -> None: 

1916 optype, params = _get_optype_and_params(op) 

1917 self.strings.add_string(_tk_to_qasm_params[optype]) 

1918 self.write_params(params) 

1919 self.write_args(args) 

1920 

1921 def add_extra_noparams(self, op: Op, args: Sequence[UnitID]) -> tuple[str, str]: 

1922 optype = op.type 

1923 opstr = _tk_to_qasm_extra_noparams[optype] 

1924 gatedefstr = "" 

1925 if opstr not in self.added_gate_definitions: 

1926 self.added_gate_definitions.add(opstr) 

1927 gatedefstr = self.make_gate_definition(op.n_qubits, opstr, optype) 

1928 mainstr = opstr + " " + _make_args_str(args) 

1929 return gatedefstr, mainstr 

1930 

1931 def add_extra_params(self, op: Op, args: Sequence[UnitID]) -> tuple[str, str]: 

1932 optype, params = _get_optype_and_params(op) 

1933 assert params is not None 

1934 opstr = _tk_to_qasm_extra_params[optype] 

1935 gatedefstr = "" 

1936 if opstr not in self.added_gate_definitions: 

1937 self.added_gate_definitions.add(opstr) 

1938 gatedefstr = self.make_gate_definition( 

1939 op.n_qubits, opstr, optype, len(params) 

1940 ) 

1941 mainstr = opstr + _make_params_str(params) + _make_args_str(args) 

1942 return gatedefstr, mainstr 

1943 

1944 def add_op(self, op: Op, args: Sequence[UnitID]) -> None: # noqa: PLR0912 

1945 optype, _params = _get_optype_and_params(op) 

1946 if optype == OpType.RangePredicate: 

1947 assert isinstance(op, RangePredicateOp) 

1948 self.add_range_predicate(op, cast("list[Bit]", args)) 

1949 elif optype == OpType.Conditional: 

1950 assert isinstance(op, Conditional) 

1951 self.add_conditional(op, args) 

1952 elif optype == OpType.Phase: 

1953 # global phase is ignored in QASM 

1954 pass 

1955 elif optype == OpType.SetBits: 

1956 assert isinstance(op, SetBitsOp) 

1957 self.add_set_bits(op, cast("list[Bit]", args)) 

1958 elif optype == OpType.CopyBits: 

1959 assert isinstance(op, CopyBitsOp) 

1960 self.add_copy_bits(op, cast("list[Bit]", args)) 

1961 elif optype == OpType.MultiBit: 

1962 assert isinstance(op, MultiBitOp) 

1963 self.add_multi_bit(op, cast("list[Bit]", args)) 

1964 elif optype in (OpType.ExplicitPredicate, OpType.ExplicitModifier): 

1965 self.add_explicit_op(op, cast("list[Bit]", args)) 

1966 elif optype == OpType.ClExpr: 

1967 assert isinstance(op, ClExprOp) 

1968 self.add_wired_clexpr(op, cast("list[Bit]", args)) 

1969 elif optype == OpType.WASM: 

1970 assert isinstance(op, WASMOp) 

1971 self.add_wasm(op, cast("list[Bit]", args)) 

1972 elif optype == OpType.Measure: 

1973 self.add_measure(args) 

1974 elif _hqs_header(self.header) and optype == OpType.ZZPhase: 

1975 # special handling for zzphase 

1976 assert len(op.params) == 1 

1977 self.add_zzphase(op.params[0], args) 

1978 elif optype == OpType.CnX: 

1979 self.add_cnx(args) 

1980 elif optype == OpType.Barrier and self.header == "hqslib1_dev": 

1981 assert isinstance(op, BarrierOp) 

1982 self.add_data(op, args) 

1983 elif ( 

1984 optype in _tk_to_qasm_noparams 

1985 and _tk_to_qasm_noparams[optype] in self.include_module_gates 

1986 ): 

1987 self.add_gate_noparams(op, args) 

1988 elif ( 

1989 optype in _tk_to_qasm_params 

1990 and _tk_to_qasm_params[optype] in self.include_module_gates 

1991 ): 

1992 self.add_gate_params(op, args) 

1993 elif optype in _tk_to_qasm_extra_noparams: 

1994 gatedefstr, mainstr = self.add_extra_noparams(op, args) 

1995 self.gatedefs += gatedefstr 

1996 self.strings.add_string(mainstr) 

1997 elif optype in _tk_to_qasm_extra_params: 1997 ↛ 2002line 1997 didn't jump to line 2002 because the condition on line 1997 was always true

1998 gatedefstr, mainstr = self.add_extra_params(op, args) 

1999 self.gatedefs += gatedefstr 

2000 self.strings.add_string(mainstr) 

2001 else: 

2002 raise QASMUnsupportedError(f"Cannot print command of type: {op.get_name()}") 

2003 

2004 def finalize(self) -> str: 

2005 # try removing unused predicates 

2006 pred_labels = list(self.range_preds.keys()) 

2007 for label in pred_labels: 

2008 # try replacing conditions with a predicate 

2009 self.replace_condition(label) 

2010 # try removing the predicate 

2011 self.remove_unused_predicate(label) 

2012 reg_strings = _LabelledStringList() 

2013 for reg in self.qregs.values(): 

2014 reg_strings.add_string(f"qreg {reg.name}[{reg.size}];\n") 

2015 for bit_reg in self.cregs.values(): 

2016 reg_strings.add_string(f"creg {bit_reg.name}[{bit_reg.size}];\n") 

2017 if self.scratch_reg.size > 0: 

2018 reg_strings.add_string( 

2019 f"creg {self.scratch_reg.name}[{self.scratch_reg.size}];\n" 

2020 ) 

2021 return ( 

2022 self.prefix 

2023 + self.gatedefs 

2024 + _filtered_qasm_str( 

2025 reg_strings.get_full_string() + self.strings.get_full_string() 

2026 ) 

2027 ) 

2028 

2029 

2030def circuit_to_qasm_io( 

2031 circ: Circuit, 

2032 stream_out: TextIO, 

2033 header: str = "qelib1", 

2034 include_gate_defs: set[str] | None = None, 

2035 maxwidth: int = 32, 

2036) -> None: 

2037 """Convert a Circuit to QASM and write to a text stream. 

2038 

2039 Classical bits in the pytket circuit must be singly-indexed. 

2040 

2041 Note that this will not account for implicit qubit permutations in the Circuit. 

2042 

2043 :param circ: pytket circuit 

2044 :param stream_out: text stream to be written to 

2045 :param header: qasm header (default "qelib1") 

2046 :param include_gate_defs: optional set of gates to include 

2047 :param maxwidth: maximum allowed width of classical registers (default 32) 

2048 """ 

2049 stream_out.write( 

2050 circuit_to_qasm_str( 

2051 circ, header=header, include_gate_defs=include_gate_defs, maxwidth=maxwidth 

2052 ) 

2053 )