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

1039 statements  

« prev     ^ index     » next       coverage.py v7.6.12, created at 2025-03-14 11:30 +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 check_register_alignments, 

56 has_reg_output, 

57 wired_clexpr_from_logic_exp, 

58) 

59from pytket.circuit.decompose_classical import int_to_bools 

60from pytket.circuit.logic_exp import ( 

61 BitLogicExp, 

62 BitWiseOp, 

63 LogicExp, 

64 PredicateExp, 

65 RegEq, 

66 RegLogicExp, 

67 RegNeg, 

68 RegWiseOp, 

69 create_logic_exp, 

70 create_predicate_exp, 

71) 

72from pytket.passes import ( 

73 AutoRebase, 

74 DecomposeBoxes, 

75 RemoveRedundancies, 

76 scratch_reg_resize_pass, 

77) 

78from pytket.qasm.grammar import grammar 

79from pytket.wasm import WasmFileHandler 

80 

81 

82class QASMParseError(Exception): 

83 """Error while parsing QASM input.""" 

84 

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

86 self.msg = msg 

87 self.line = line 

88 self.fname = fname 

89 

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

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

92 

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

94 

95 

96class QASMUnsupportedError(Exception): 

97 pass 

98 

99 

100Value = Union[int, float, str] 

101T = TypeVar("T") 

102 

103_BITOPS = set(op.value for op in BitWiseOp) 

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

105_REGOPS = set(op.value for op in RegWiseOp) 

106 

107Arg = Union[list, str] 

108 

109 

110NOPARAM_COMMANDS = { 

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

112 "cx": OpType.CX, 

113 "x": OpType.X, 

114 "y": OpType.Y, 

115 "z": OpType.Z, 

116 "h": OpType.H, 

117 "s": OpType.S, 

118 "sdg": OpType.Sdg, 

119 "t": OpType.T, 

120 "tdg": OpType.Tdg, 

121 "sx": OpType.SX, 

122 "sxdg": OpType.SXdg, 

123 "cz": OpType.CZ, 

124 "cy": OpType.CY, 

125 "ch": OpType.CH, 

126 "csx": OpType.CSX, 

127 "ccx": OpType.CCX, 

128 "c3x": OpType.CnX, 

129 "c4x": OpType.CnX, 

130 "ZZ": OpType.ZZMax, 

131 "measure": OpType.Measure, 

132 "reset": OpType.Reset, 

133 "id": OpType.noop, 

134 "barrier": OpType.Barrier, 

135 "swap": OpType.SWAP, 

136 "cswap": OpType.CSWAP, 

137} 

138PARAM_COMMANDS = { 

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

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

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

142 "u3": OpType.U3, 

143 "u2": OpType.U2, 

144 "u1": OpType.U1, 

145 "rx": OpType.Rx, 

146 "rxx": OpType.XXPhase, 

147 "ry": OpType.Ry, 

148 "rz": OpType.Rz, 

149 "RZZ": OpType.ZZPhase, 

150 "rzz": OpType.ZZPhase, 

151 "Rz": OpType.Rz, 

152 "U1q": OpType.PhasedX, 

153 "crz": OpType.CRz, 

154 "crx": OpType.CRx, 

155 "cry": OpType.CRy, 

156 "cu1": OpType.CU1, 

157 "cu3": OpType.CU3, 

158 "Rxxyyzz": OpType.TK2, 

159} 

160 

161NOPARAM_EXTRA_COMMANDS = { 

162 "v": OpType.V, 

163 "vdg": OpType.Vdg, 

164 "cv": OpType.CV, 

165 "cvdg": OpType.CVdg, 

166 "csxdg": OpType.CSXdg, 

167 "bridge": OpType.BRIDGE, 

168 "iswapmax": OpType.ISWAPMax, 

169 "zzmax": OpType.ZZMax, 

170 "ecr": OpType.ECR, 

171 "cs": OpType.CS, 

172 "csdg": OpType.CSdg, 

173} 

174 

175PARAM_EXTRA_COMMANDS = { 

176 "tk2": OpType.TK2, 

177 "iswap": OpType.ISWAP, 

178 "phasediswap": OpType.PhasedISWAP, 

179 "yyphase": OpType.YYPhase, 

180 "xxphase3": OpType.XXPhase3, 

181 "eswap": OpType.ESWAP, 

182 "fsim": OpType.FSim, 

183} 

184 

185N_PARAMS_EXTRA_COMMANDS = { 

186 OpType.TK2: 3, 

187 OpType.ISWAP: 1, 

188 OpType.PhasedISWAP: 2, 

189 OpType.YYPhase: 1, 

190 OpType.XXPhase3: 1, 

191 OpType.ESWAP: 1, 

192 OpType.FSim: 2, 

193} 

194 

195_tk_to_qasm_noparams = dict((item[1], item[0]) for item in NOPARAM_COMMANDS.items()) 

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

197_tk_to_qasm_params = dict((item[1], item[0]) for item in PARAM_COMMANDS.items()) 

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

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

200_tk_to_qasm_extra_noparams = dict( 

201 (item[1], item[0]) for item in NOPARAM_EXTRA_COMMANDS.items() 

202) 

203_tk_to_qasm_extra_params = dict( 

204 (item[1], item[0]) for item in PARAM_EXTRA_COMMANDS.items() 

205) 

206 

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

208 

209 

210_all_known_gates = ( 

211 set(NOPARAM_COMMANDS.keys()) 

212 .union(PARAM_COMMANDS.keys()) 

213 .union(PARAM_EXTRA_COMMANDS.keys()) 

214 .union(NOPARAM_EXTRA_COMMANDS.keys()) 

215) 

216_all_string_maps = { 

217 key: val.name 

218 for key, val in chain( 

219 PARAM_COMMANDS.items(), 

220 NOPARAM_COMMANDS.items(), 

221 PARAM_EXTRA_COMMANDS.items(), 

222 NOPARAM_EXTRA_COMMANDS.items(), 

223 ) 

224} 

225 

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

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

228 

229 

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

231 match = unit_regex.match(var.value) 

232 if match is None: 

233 raise QASMParseError( 

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

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

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

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

238 "letters, numbers, and underscores." 

239 ) 

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

241 

242 

243def _load_include_module( 

244 header_name: str, flter: bool, decls_only: bool 

245) -> dict[str, dict]: 

246 try: 

247 if decls_only: 

248 include_def: dict[str, dict] = import_module( 

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

250 )._INCLUDE_DECLS 

251 else: 

252 include_def = import_module( 

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

254 )._INCLUDE_DEFS 

255 except ModuleNotFoundError as e: 

256 raise QASMParseError( 

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

258 ) from e 

259 return { 

260 gate: include_def[gate] 

261 for gate in include_def 

262 if not flter or gate not in _all_known_gates 

263 } 

264 

265 

266def _bin_par_exp(op: "str") -> Callable[["CircuitTransformer", list[str]], str]: 

267 def f(self: "CircuitTransformer", vals: list[str]) -> str: 

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

269 

270 return f 

271 

272 

273def _un_par_exp(op: "str") -> Callable[["CircuitTransformer", list[str]], str]: 

274 def f(self: "CircuitTransformer", vals: list[str]) -> str: 

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

276 

277 return f 

278 

279 

280def _un_call_exp(op: "str") -> Callable[["CircuitTransformer", list[str]], str]: 

281 def f(self: "CircuitTransformer", vals: list[str]) -> str: 

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

283 

284 return f 

285 

286 

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

288 return arg[0], arg[1][0] 

289 

290 

291Reg = NewType("Reg", str) 

292CommandDict = dict[str, Any] 

293 

294 

295@dataclass 

296class ParsMap: 

297 pars: Iterable[str] 

298 

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

300 return self.pars 

301 

302 

303class CircuitTransformer(Transformer): 

304 def __init__( 

305 self, 

306 return_gate_dict: bool = False, 

307 maxwidth: int = 32, 

308 ) -> None: 

309 super().__init__() 

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

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

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

313 self.wasm: WasmFileHandler | None = None 

314 self.include = "" 

315 self.return_gate_dict = return_gate_dict 

316 self.maxwidth = maxwidth 

317 

318 def _fresh_temp_bit(self) -> list: 

319 if _TEMP_BIT_NAME in self.c_registers: 

320 idx = self.c_registers[_TEMP_BIT_NAME] 

321 else: 

322 idx = 0 

323 self.c_registers[_TEMP_BIT_NAME] = idx + 1 

324 

325 return [_TEMP_BIT_NAME, [idx]] 

326 

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

328 self.q_registers = {} 

329 self.c_registers = {} 

330 self.gate_dict = {} 

331 self.include = "" 

332 if reset_wasm: 

333 self.wasm = None 

334 

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

336 return Reg(name) 

337 

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

339 name, idx = _extract_reg(iarg) 

340 return [name, [idx]] 

341 

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

343 if arg.type == "IARG": 

344 return self._get_uid(arg) 

345 return self._get_reg(arg.value) 

346 

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

348 for arg in args: 

349 if isinstance(arg, str): 

350 size = ( 

351 self.q_registers[arg] 

352 if arg in self.q_registers 

353 else self.c_registers[arg] 

354 ) 

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

356 else: 

357 yield [arg] 

358 

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

360 return map(self._get_arg, tree) 

361 

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

363 return map(self._get_uid, tree) 

364 

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

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

367 

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

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

370 if size > self.maxwidth: 

371 raise QASMUnsupportedError( 

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

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

374 "value." 

375 ) 

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

377 

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

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

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

381 

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

383 for args in zip(*self.unroll_all_args(self.margs(tree))): 

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

385 

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

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

388 signature: list[str] = [] 

389 for arg in args: 

390 if arg[0] in self.c_registers: 

391 signature.append("C") 

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

393 signature.append("Q") 

394 else: 

395 raise QASMParseError( 

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

397 ) 

398 yield { 

399 "args": args, 

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

401 } 

402 

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

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

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

406 

407 def pars(self, vals: Iterable[str]) -> ParsMap: 

408 return ParsMap(map(str, vals)) 

409 

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

411 child_iter = iter(tree) 

412 

413 optoken = next(child_iter) 

414 opstr = optoken.value 

415 next_tree = next(child_iter) 

416 try: 

417 args = next(child_iter) 

418 pars = cast(ParsMap, next_tree).pars 

419 except StopIteration: 

420 args = next_tree 

421 pars = [] 

422 

423 treat_as_barrier = [ 

424 "sleep", 

425 "order2", 

426 "order3", 

427 "order4", 

428 "order5", 

429 "order6", 

430 "order7", 

431 "order8", 

432 "order9", 

433 "order10", 

434 "order11", 

435 "order12", 

436 "order13", 

437 "order14", 

438 "order15", 

439 "order16", 

440 "order17", 

441 "order18", 

442 "order19", 

443 "order20", 

444 "group2", 

445 "group3", 

446 "group4", 

447 "group5", 

448 "group6", 

449 "group7", 

450 "group8", 

451 "group9", 

452 "group10", 

453 "group11", 

454 "group12", 

455 "group13", 

456 "group14", 

457 "group15", 

458 "group16", 

459 "group17", 

460 "group18", 

461 "group19", 

462 "group20", 

463 ] 

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

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

466 

467 args = list(args) 

468 

469 if opstr in treat_as_barrier: 

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

471 else: 

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

473 if opstr in self.gate_dict: 

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

475 if opstr in treat_as_barrier: 

476 op["type"] = "Barrier" 

477 param_sorted = ",".join(params) 

478 

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

480 

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

482 else: 

483 gdef = self.gate_dict[opstr] 

484 op["type"] = "CustomGate" 

485 box = { 

486 "type": "CustomGate", 

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

488 "gate": gdef, 

489 } 

490 box["params"] = params 

491 op["box"] = box 

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

493 else: 

494 try: 

495 optype = _all_string_maps[opstr] 

496 except KeyError as e: 

497 raise QASMParseError( 

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

499 ) from e 

500 op = {"type": optype} 

501 if params: 

502 op["params"] = params 

503 # Operations needing special handling: 

504 if optype.startswith("Cn"): 

505 # n-controlled rotations have variable signature 

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

507 elif optype == "Barrier": 

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

509 

510 for arg in zip(*self.unroll_all_args(args)): 

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

512 

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

514 return self.mixedcall(tree) 

515 

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

517 for arg in tree: 

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

519 yield self._get_reg(arg.value) 

520 else: 

521 raise QASMParseError( 

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

523 ) 

524 

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

526 args, line = self._get_logic_args(tree) 

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

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

529 openum = BitWiseOp 

530 elif ( 

531 opstr in _REGOPS 

532 and opstr not in _BITOPS 

533 or all(isinstance(arg, int) for arg in args) 

534 ): 

535 openum = RegWiseOp 

536 elif all(isinstance(arg, (Bit, BitLogicExp, int)) for arg in args): 

537 if all(arg in (0, 1) for arg in args if isinstance(arg, int)): 537 ↛ 540line 537 didn't jump to line 540 because the condition on line 537 was always true

538 openum = BitWiseOp 

539 else: 

540 raise QASMParseError( 

541 "Bits can only be operated with (0, 1) literals." 

542 f" Incomaptible arguments {args}", 

543 line, 

544 ) 

545 else: 

546 openum = RegWiseOp 

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

548 op: BitWiseOp | RegWiseOp = BitWiseOp.XOR 

549 else: 

550 op = openum(opstr) 

551 return create_logic_exp(op, args) 

552 

553 def _get_logic_args( 

554 self, tree: Sequence[Token | LogicExp] 

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

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

557 line = None 

558 for tok in tree: 

559 if isinstance(tok, LogicExp): 

560 args.append(tok) 

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

562 line = tok.line 

563 if tok.type == "INT": 

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

565 elif tok.type == "IARG": 

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

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

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

569 else: 

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

571 else: 

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

573 return args, line 

574 

575 par_add = _bin_par_exp("+") 

576 par_sub = _bin_par_exp("-") 

577 par_mul = _bin_par_exp("*") 

578 par_div = _bin_par_exp("/") 

579 par_pow = _bin_par_exp("**") 

580 

581 par_neg = _un_par_exp("-") 

582 

583 sqrt = _un_call_exp("sqrt") 

584 sin = _un_call_exp("sin") 

585 cos = _un_call_exp("cos") 

586 tan = _un_call_exp("tan") 

587 ln = _un_call_exp("ln") 

588 

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

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

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

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

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

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

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

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

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

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

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

600 

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

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

603 assert isinstance(arg, (RegLogicExp, BitRegister, int)) 

604 return RegNeg(arg) 

605 

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

607 op: BitWiseOp | RegWiseOp 

608 arg: Bit | BitRegister 

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

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

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

612 else: 

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

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

615 

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

617 

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

619 condition = cast(PredicateExp, tree[0]) 

620 

621 var, val = condition.args 

622 condition_bits = [] 

623 

624 if isinstance(var, Bit): 

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

626 assert val in (0, 1) 

627 condition_bits = [var.to_list()] 

628 

629 else: 

630 assert isinstance(var, BitRegister) 

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

632 if isinstance(condition, RegEq): 

633 # special case for base qasm 

634 condition_bits = reg_bits 

635 else: 

636 pred_val = cast(int, val) 

637 minval = 0 

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

639 if condition.op == RegWiseOp.LT: 

640 maxval = pred_val - 1 

641 elif condition.op == RegWiseOp.GT: 

642 minval = pred_val + 1 

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

644 maxval = pred_val 

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

646 minval = pred_val 

647 

648 condition_bit = self._fresh_temp_bit() 

649 yield { 

650 "args": reg_bits + [condition_bit], 

651 "op": { 

652 "classical": { 

653 "lower": minval, 

654 "n_i": len(reg_bits), 

655 "upper": maxval, 

656 }, 

657 "type": "RangePredicate", 

658 }, 

659 } 

660 condition_bits = [condition_bit] 

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

662 

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

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

665 com["op"] = { 

666 "conditional": { 

667 "op": com["op"], 

668 "value": val, 

669 "width": len(condition_bits), 

670 }, 

671 "type": "Conditional", 

672 } 

673 

674 yield com 

675 

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

677 return tree[0] 

678 

679 def _calc_exp_io( 

680 self, exp: LogicExp, out_args: list 

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

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

683 for inp in exp.all_inputs_ordered(): 

684 if isinstance(inp, Bit): 

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

686 else: 

687 assert isinstance(inp, BitRegister) 

688 for bit in inp: 

689 all_inps.append((bit.reg_name, bit.index[0])) 

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

691 o = [] 

692 io = [] 

693 for out in outs: 

694 if out in all_inps: 

695 all_inps.remove(out) 

696 io.append(out) 

697 else: 

698 o.append(out) 

699 

700 exp_args = list( 

701 map(lambda x: [x[0], [x[1]]], chain.from_iterable((all_inps, io, o))) 

702 ) 

703 numbers_dict = { 

704 "n_i": len(all_inps), 

705 "n_io": len(io), 

706 "n_o": len(o), 

707 } 

708 return exp_args, numbers_dict 

709 

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

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

712 # corresponding ClExprOp. 

713 wexpr, args = wired_clexpr_from_logic_exp( 

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

715 ) 

716 return { 

717 "op": { 

718 "type": "ClExpr", 

719 "expr": wexpr.to_dict(), 

720 }, 

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

722 } 

723 

724 def assign(self, tree: list) -> Iterable[CommandDict]: 

725 child_iter = iter(tree) 

726 out_args = list(next(child_iter)) 

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

728 

729 exp_tree = next(child_iter) 

730 

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

732 line = None 

733 if isinstance(exp_tree, Token): 

734 if exp_tree.type == "INT": 

735 exp = int(exp_tree.value) 

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

737 exp = self._get_arg(exp_tree) 

738 line = exp_tree.line 

739 elif isinstance(exp_tree, Generator): 

740 # assume to be extern (wasm) call 

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

742 com = next(exp_tree) 

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

744 com["args"] += chained_uids 

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

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

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

748 self.c_registers[reg] for reg in out_args 

749 ] 

750 

751 yield com 

752 return 

753 else: 

754 exp = exp_tree 

755 

756 assert len(out_args) == 1 

757 out_arg = out_args[0] 

758 args = args_uids[0] 

759 if isinstance(out_arg, list): 

760 if isinstance(exp, LogicExp): 

761 yield self._clexpr_dict(exp, args) 

762 elif isinstance(exp, (int, bool)): 

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

764 yield { 

765 "args": args, 

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

767 } 

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

769 yield { 

770 "args": [exp] + args, 

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

772 } 

773 else: 

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

775 else: 

776 reg = out_arg 

777 if isinstance(exp, RegLogicExp): 

778 yield self._clexpr_dict(exp, args) 

779 elif isinstance(exp, BitLogicExp): 779 ↛ 780line 779 didn't jump to line 780 because the condition on line 779 was never true

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

781 elif isinstance(exp, int): 

782 yield { 

783 "args": args, 

784 "op": { 

785 "classical": { 

786 "values": int_to_bools(exp, self.c_registers[reg]) 

787 }, 

788 "type": "SetBits", 

789 }, 

790 } 

791 

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

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

794 yield { 

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

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

797 } 

798 

799 else: 

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

801 

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

803 # TODO parse extern defs 

804 return Discard 

805 

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

807 return self.cce_call(tree) 

808 

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

810 nam = tree[0].value 

811 params = list(tree[1]) 

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

813 raise QASMParseError( 

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

815 tree[0].line, 

816 ) 

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

818 

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

820 

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

822 

823 yield { 

824 "args": wasm_args, 

825 "op": { 

826 "type": "WASM", 

827 "wasm": { 

828 "func_name": nam, 

829 "ww_n": 1, 

830 "n": sum(n_i_vec), 

831 "width_i_parameter": n_i_vec, 

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

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

834 }, 

835 }, 

836 } 

837 

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

839 self._reset_context() 

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

841 

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

843 child_iter = iter(tree) 

844 gate = next(child_iter).value 

845 next_tree = next(child_iter) 

846 symbols, args = [], [] 

847 if isinstance(next_tree, ParsMap): 

848 symbols = list(next_tree.pars) 

849 args = list(next(child_iter)) 

850 else: 

851 args = list(next_tree) 

852 

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

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

855 

856 new = CircuitTransformer(maxwidth=self.maxwidth) 

857 circ_dict = new.prog(child_iter) 

858 

859 circ_dict["qubits"] = args 

860 gate_circ = Circuit.from_dict(circ_dict) 

861 

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

863 # if true, add op as pytket Op 

864 existing_op: bool = False 

865 # NOPARAM_EXTRA_COMMANDS and PARAM_EXTRA_COMMANDS are 

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

867 # optypes 

868 if gate in NOPARAM_EXTRA_COMMANDS: 

869 qubit_args = [ 

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

871 ] 

872 comparison_circ = _get_gate_circuit( 

873 NOPARAM_EXTRA_COMMANDS[gate], qubit_args 

874 ) 

875 if circuit_to_qasm_str( 

876 comparison_circ, maxwidth=self.maxwidth 

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

878 existing_op = True 

879 elif gate in PARAM_EXTRA_COMMANDS: 

880 optype = PARAM_EXTRA_COMMANDS[gate] 

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

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

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

884 existing_op = False 

885 else: 

886 qubit_args = [ 

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

888 ] 

889 comparison_circ = _get_gate_circuit( 

890 optype, 

891 qubit_args, 

892 [ 

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

894 for index in range(len(symbols)) 

895 ], 

896 ) 

897 # checks that each command has same string 

898 existing_op = all( 

899 str(g) == str(c) 

900 for g, c in zip( 

901 gate_circ.get_commands(), comparison_circ.get_commands() 

902 ) 

903 ) 

904 if not existing_op: 

905 gate_circ.symbol_substitution(symbol_map) 

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

907 self.gate_dict[gate] = { 

908 "definition": gate_circ.to_dict(), 

909 "args": symbols, 

910 "name": gate, 

911 } 

912 

913 opaq = gdef 

914 

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

916 return Discard 

917 

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

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

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

921 

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

923 outdict: dict[str, Any] = { 

924 "commands": list( 

925 chain.from_iterable( 

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

927 ) 

928 ) 

929 } 

930 if self.return_gate_dict: 

931 return self.gate_dict 

932 outdict["qubits"] = [ 

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

934 ] 

935 outdict["bits"] = [ 

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

937 ] 

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

939 outdict["phase"] = "0.0" 

940 self._reset_context() 

941 return outdict 

942 

943 

944def parser(maxwidth: int) -> Lark: 

945 return Lark( 

946 grammar, 

947 start="prog", 

948 debug=False, 

949 parser="lalr", 

950 cache=True, 

951 transformer=CircuitTransformer(maxwidth=maxwidth), 

952 ) 

953 

954 

955g_parser = None 

956g_maxwidth = 32 

957 

958 

959def set_parser(maxwidth: int) -> None: 

960 global g_parser, g_maxwidth 

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

962 g_parser = parser(maxwidth=maxwidth) 

963 g_maxwidth = maxwidth 

964 

965 

966def circuit_from_qasm( 

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

968 encoding: str = "utf-8", 

969 maxwidth: int = 32, 

970) -> Circuit: 

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

972 

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

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

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

976 :return: pytket circuit 

977 """ 

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

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

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

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

982 try: 

983 circ = circuit_from_qasm_io(f, maxwidth=maxwidth) 

984 except QASMParseError as e: 

985 raise QASMParseError(e.msg, e.line, str(input_file)) 

986 return circ 

987 

988 

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

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

991 

992 :param qasm_str: qasm string 

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

994 :return: pytket circuit 

995 """ 

996 global g_parser 

997 set_parser(maxwidth=maxwidth) 

998 assert g_parser is not None 

999 cast(CircuitTransformer, g_parser.options.transformer)._reset_context( 

1000 reset_wasm=False 

1001 ) 

1002 

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

1004 cpass = scratch_reg_resize_pass(maxwidth) 

1005 cpass.apply(circ) 

1006 return circ 

1007 

1008 

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

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

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

1012 

1013 

1014def circuit_from_qasm_wasm( 

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

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

1017 encoding: str = "utf-8", 

1018 maxwidth: int = 32, 

1019) -> Circuit: 

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

1021 

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

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

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

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

1026 :return: pytket circuit 

1027 """ 

1028 global g_parser 

1029 wasm_module = WasmFileHandler(str(wasm_file)) 

1030 set_parser(maxwidth=maxwidth) 

1031 assert g_parser is not None 

1032 cast(CircuitTransformer, g_parser.options.transformer).wasm = wasm_module 

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

1034 

1035 

1036def circuit_to_qasm( 

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

1038) -> None: 

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

1040 

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

1042 

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

1044 

1045 :param circ: pytket circuit 

1046 :param output_file: path to output qasm file 

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

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

1049 """ 

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

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

1052 

1053 

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

1055 # remove any c registers starting with _TEMP_BIT_NAME 

1056 # that are not being used somewhere else 

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

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

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

1060 unused_regs = dict() 

1061 for i, line in enumerate(lines): 

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

1063 # Mark a reg temporarily as unused 

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

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

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

1067 # mark these regs as used 

1068 for arg in args: 

1069 if arg in unused_regs: 

1070 unused_regs.pop(arg) 

1071 # remove unused reg defs 

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

1073 for line_index in redundant_lines: 

1074 del lines[line_index] 

1075 return "\n".join(lines) 

1076 

1077 

1078def is_empty_customgate(op: Op) -> bool: 

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

1080 

1081 

1082def check_can_convert_circuit(circ: Circuit, header: str, maxwidth: int) -> None: 

1083 if any( 

1084 circ.n_gates_of_type(typ) 

1085 for typ in ( 

1086 OpType.RangePredicate, 

1087 OpType.MultiBit, 

1088 OpType.ExplicitPredicate, 

1089 OpType.ExplicitModifier, 

1090 OpType.SetBits, 

1091 OpType.CopyBits, 

1092 ) 

1093 ) and (not hqs_header(header)): 

1094 raise QASMUnsupportedError( 

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

1096 "`header=hqslib1`" 

1097 ) 

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

1099 raise QASMUnsupportedError( 

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

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

1102 ) 

1103 set_circ_register = set([creg.name for creg in circ.c_registers]) 

1104 for b in circ.bits: 

1105 if b.reg_name not in set_circ_register: 

1106 raise QASMUnsupportedError( 

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

1108 ) 

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

1110 for cmd in circ: 

1111 assert not is_empty_customgate(cmd.op) 

1112 if isinstance(cmd.op, Conditional): 

1113 assert not is_empty_customgate(cmd.op.op) 

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

1115 raise QASMUnsupportedError( 

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

1117 "outputs are not register-aligned." 

1118 ) 

1119 

1120 

1121def circuit_to_qasm_str( 

1122 circ: Circuit, 

1123 header: str = "qelib1", 

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

1125 maxwidth: int = 32, 

1126) -> str: 

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

1128 

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

1130 

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

1132 

1133 :param circ: pytket circuit 

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

1135 :param output_file: path to output qasm file 

1136 :param include_gate_defs: optional set of gates to include 

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

1138 :return: qasm string 

1139 """ 

1140 

1141 qasm_writer = QasmWriter( 

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

1143 ) 

1144 circ1 = circ.copy() 

1145 DecomposeBoxes().apply(circ1) 

1146 check_can_convert_circuit(circ1, header, maxwidth) 

1147 for command in circ1: 

1148 assert isinstance(command, Command) 

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

1150 return qasm_writer.finalize() 

1151 

1152 

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

1154 

1155 

1156def _retrieve_registers( 

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

1158) -> dict[str, TypeReg]: 

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

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

1161 maxunits = map(lambda x: max(x[1]), groupby(units, key=lambda un: un.reg_name)) 

1162 return { 

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

1164 for maxunit in maxunits 

1165 } 

1166 

1167 

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

1169 if maxwidth > 64: 1169 ↛ 1170line 1169 didn't jump to line 1170 because the condition on line 1169 was never true

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

1171 

1172 REGMAX = (1 << maxwidth) - 1 

1173 

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

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

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

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

1178 maxval = min(maxval, REGMAX) 

1179 

1180 if minval == maxval: 

1181 return ("==", minval) 

1182 if minval == 0: 

1183 return ("<=", maxval) 

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

1185 return (">=", minval) 

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

1187 

1188 

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

1190 if comparator == "==": 

1191 return "!=" 

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

1193 return "==" 

1194 if comparator == "<=": 

1195 return ">" 

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

1197 return "<=" 

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

1199 return "<" 

1200 assert comparator == "<" 

1201 return ">=" 

1202 

1203 

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

1205 optype = op.type 

1206 params = ( 

1207 op.params 

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

1209 else None 

1210 ) 

1211 if optype == OpType.TK1: 

1212 # convert to U3 

1213 optype = OpType.U3 

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

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

1216 params = op.params 

1217 return optype, params 

1218 

1219 

1220def _get_gate_circuit( 

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

1222) -> Circuit: 

1223 # create Circuit for constructing qasm from 

1224 unitids = cast(list[UnitID], qubits) 

1225 gate_circ = Circuit() 

1226 for q in qubits: 

1227 gate_circ.add_qubit(q) 

1228 if symbols: 

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

1230 gate_circ.add_gate(optype, exprs, unitids) 

1231 else: 

1232 gate_circ.add_gate(optype, unitids) 

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

1234 RemoveRedundancies().apply(gate_circ) 

1235 

1236 return gate_circ 

1237 

1238 

1239def hqs_header(header: str) -> bool: 

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

1241 

1242 

1243@dataclass 

1244class ConditionString: 

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

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

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

1248 

1249 

1250class LabelledStringList: 

1251 """ 

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

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

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

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

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

1257 retrieving the full string. 

1258 """ 

1259 

1260 def __init__(self) -> None: 

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

1262 self.conditions: dict[int, ConditionString] = dict() 

1263 self.label = 0 

1264 

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

1266 label = self.label 

1267 self.strings[label] = string 

1268 self.label += 1 

1269 return label 

1270 

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

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

1273 

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

1275 self.strings.pop(label, None) 

1276 

1277 def get_full_string(self) -> str: 

1278 strings = [] 

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

1280 condition = self.conditions.get(l) 

1281 if condition is not None: 

1282 strings.append( 

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

1284 + s 

1285 ) 

1286 else: 

1287 strings.append(s) 

1288 return "".join(strings) 

1289 

1290 

1291def make_params_str(params: list[float | Expr] | None) -> str: 

1292 s = "" 

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

1294 n_params = len(params) 

1295 s += "(" 

1296 for i in range(n_params): 

1297 reduced = True 

1298 try: 

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

1300 except TypeError: 

1301 reduced = False 

1302 p = params[i] 

1303 if i < n_params - 1: 

1304 if reduced: 

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

1306 else: 

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

1308 elif reduced: 

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

1310 else: 

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

1312 s += " " 

1313 return s 

1314 

1315 

1316def make_args_str(args: Sequence[UnitID]) -> str: 

1317 s = "" 

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

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

1320 if i < len(args) - 1: 

1321 s += "," 

1322 else: 

1323 s += ";\n" 

1324 return s 

1325 

1326 

1327@dataclass 

1328class ScratchPredicate: 

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

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

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

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

1333 

1334 

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

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

1337 v_split = v.split("[") 

1338 w_split = w.split("[") 

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

1340 # different registers 

1341 return False 

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

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

1344 

1345 

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

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

1348 v_split = v.split("[") 

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

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

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

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

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

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

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

1356 return True 

1357 # check the register of v appears in s 

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

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

1360 

1361 

1362class QasmWriter: 

1363 """ 

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

1365 final QASM string afterwards. 

1366 """ 

1367 

1368 def __init__( 

1369 self, 

1370 qubits: list[Qubit], 

1371 bits: list[Bit], 

1372 header: str = "qelib1", 

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

1374 maxwidth: int = 32, 

1375 ): 

1376 self.header = header 

1377 self.maxwidth = maxwidth 

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

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

1380 self.include_module_gates.update( 

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

1382 ) 

1383 self.prefix = "" 

1384 self.gatedefs = "" 

1385 self.strings = LabelledStringList() 

1386 

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

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

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

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

1391 self.range_preds: dict[int, ScratchPredicate] = dict() 

1392 

1393 if include_gate_defs is None: 

1394 self.include_gate_defs = self.include_module_gates 

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

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

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

1398 self.qregs = _retrieve_registers(cast(list[UnitID], qubits), QubitRegister) 

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

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

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

1402 raise QASMUnsupportedError( 

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

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

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

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

1407 ) 

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

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

1410 raise QASMUnsupportedError( 

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

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

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

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

1415 ) 

1416 else: 

1417 # gate definition, no header necessary for file 

1418 self.include_gate_defs = include_gate_defs 

1419 self.cregs = {} 

1420 self.qregs = {} 

1421 

1422 self.cregs_as_bitseqs = set(tuple(creg) for creg in self.cregs.values()) 

1423 

1424 # for holding condition values when writing Conditional blocks 

1425 # the size changes when adding and removing scratch bits 

1426 self.scratch_reg = BitRegister( 

1427 next( 

1428 f"{_TEMP_BIT_REG_BASE}_{i}" 

1429 for i in itertools.count() 

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

1431 ), 

1432 0, 

1433 ) 

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

1435 # the affected variables will be recorded. 

1436 self.variable_writes: dict[int, list[str]] = dict() 

1437 

1438 def fresh_scratch_bit(self) -> Bit: 

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

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

1441 

1442 def remove_last_scratch_bit(self) -> None: 

1443 assert self.scratch_reg.size > 0 

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

1445 

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

1447 params_str = make_params_str(params) 

1448 self.strings.add_string(params_str) 

1449 

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

1451 args_str = make_args_str(args) 

1452 self.strings.add_string(args_str) 

1453 

1454 def make_gate_definition( 

1455 self, 

1456 n_qubits: int, 

1457 opstr: str, 

1458 optype: OpType, 

1459 n_params: int | None = None, 

1460 ) -> str: 

1461 s = "gate " + opstr + " " 

1462 symbols: list[Symbol] | None = None 

1463 if n_params is not None: 

1464 # need to add parameters to gate definition 

1465 s += "(" 

1466 symbols = [ 

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

1468 ] 

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

1470 for symbol in symbols_header[:-1]: 

1471 s += symbol.name + ", " 

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

1473 

1474 # add qubits to gate definition 

1475 qubit_args = [ 

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

1477 ] 

1478 for qb in qubit_args[:-1]: 

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

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

1481 # get rebased circuit for constructing qasm 

1482 gate_circ = _get_gate_circuit(optype, qubit_args, symbols) 

1483 # write circuit to qasm 

1484 s += circuit_to_qasm_str( 

1485 gate_circ, self.header, self.include_gate_defs, self.maxwidth 

1486 ) 

1487 s += "}\n" 

1488 return s 

1489 

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

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

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

1493 else: 

1494 self.variable_writes[label] = [written_variable] 

1495 

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

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

1498 raise QASMUnsupportedError( 

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

1500 ) 

1501 variable = args[0].reg_name 

1502 assert isinstance(variable, str) 

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

1504 raise QASMUnsupportedError( 

1505 "RangePredicate conditions must be an entire classical register" 

1506 ) 

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

1508 raise QASMUnsupportedError( 

1509 "RangePredicate conditions must be a single classical register" 

1510 ) 

1511 

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

1513 self.check_range_predicate(op, args) 

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

1515 variable = args[0].reg_name 

1516 dest_bit = str(args[-1]) 

1517 label = self.strings.add_string( 

1518 "".join( 

1519 [ 

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

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

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

1523 ] 

1524 ) 

1525 ) 

1526 # Record this operation. 

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

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

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

1530 # list if it is.) 

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

1532 if dest_bit.startswith(_TEMP_BIT_NAME): 

1533 self.range_preds[label] = ScratchPredicate( 

1534 variable, comparator, value, dest_bit 

1535 ) 

1536 

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

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

1539 we scan the lines after p: 

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

1541 the predicate and do 2 for the inner command. 

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

1543 returns true if a replacement is made. 

1544 """ 

1545 assert pred_label in self.range_preds 

1546 success = False 

1547 pred = self.range_preds[pred_label] 

1548 line_labels = [] 

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

1550 string = self.strings.get_string(label) 

1551 if string is None: 

1552 continue 

1553 line_labels.append(label) 

1554 if "\n" not in string: 

1555 continue 

1556 written_variables: list[str] = [] 

1557 # (label, condition) 

1558 conditions: list[tuple[int, ConditionString]] = [] 

1559 for l in line_labels: 

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

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

1562 if cond: 

1563 conditions.append((l, cond)) 

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

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

1566 success = True 

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

1568 self.strings.conditions[conditions[0][0]] = ConditionString( 

1569 pred.variable, pred.comparator, pred.value 

1570 ) 

1571 else: 

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

1573 self.strings.conditions[conditions[0][0]] = ConditionString( 

1574 pred.variable, 

1575 _negate_comparator(pred.comparator), 

1576 pred.value, 

1577 ) 

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

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

1580 ): 

1581 return success 

1582 line_labels.clear() 

1583 conditions.clear() 

1584 written_variables.clear() 

1585 return success 

1586 

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

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

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

1590 assert pred_label in self.range_preds 

1591 pred = self.range_preds[pred_label] 

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

1593 string = self.strings.get_string(label) 

1594 if string is None: 

1595 continue 

1596 if ( 

1597 _var_appears(pred.dest, string) 

1598 or label in self.strings.conditions 

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

1600 ): 

1601 return False 

1602 self.range_preds.pop(pred_label) 

1603 self.strings.del_string(pred_label) 

1604 return True 

1605 

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

1607 control_bits = args[: op.width] 

1608 if op.width == 1 and hqs_header(self.header): 

1609 variable = str(control_bits[0]) 

1610 else: 

1611 variable = control_bits[0].reg_name 

1612 if ( 

1613 hqs_header(self.header) 

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

1615 ): 

1616 raise QASMUnsupportedError( 

1617 "hqslib1 QASM conditions must be an entire classical " 

1618 "register or a single bit" 

1619 ) 

1620 if not hqs_header(self.header): 

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

1622 raise QASMUnsupportedError( 

1623 "OpenQASM conditions must be an entire classical register" 

1624 ) 

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

1626 raise QASMUnsupportedError( 

1627 "OpenQASM conditions must be a single classical register" 

1628 ) 

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

1630 # Conditional phase is ignored. 

1631 return 

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

1633 # Special handling for nested ifs 

1634 # if condition 

1635 # if pred dest = 1 

1636 # if not pred dest = 0 

1637 # can be written as 

1638 # if condition s0 = 1 

1639 # if pred s1 = 1 

1640 # s2 = s0 & s1 

1641 # s3 = s0 & ~s1 

1642 # if s2 dest = 1 

1643 # if s3 dest = 0 

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

1645 s0 = self.fresh_scratch_bit() 

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

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

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

1649 # if possible 

1650 self.strings.conditions[l] = ConditionString(variable, "==", op.value) 

1651 # output the RangePredicate to s1 

1652 s1 = self.fresh_scratch_bit() 

1653 assert isinstance(op.op, RangePredicateOp) 

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

1655 pred_comparator, pred_value = _parse_range( 

1656 op.op.lower, op.op.upper, self.maxwidth 

1657 ) 

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

1659 self.strings.add_string( 

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

1661 ) 

1662 s2 = self.fresh_scratch_bit() 

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

1664 s3 = self.fresh_scratch_bit() 

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

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

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

1668 return 

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

1670 # if the condition variable is unchanged. 

1671 scratch_bit = self.fresh_scratch_bit() 

1672 pred_label = self.strings.add_string( 

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

1674 ) 

1675 self.range_preds[pred_label] = ScratchPredicate( 

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

1677 ) 

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

1679 next_label = self.strings.label 

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

1681 # add conditions to the lines after the predicate 

1682 is_new_line = True 

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

1684 string = self.strings.get_string(label) 

1685 assert string is not None 

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

1687 self.strings.conditions[label] = ConditionString( 

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

1689 ) 

1690 is_new_line = "\n" in string 

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

1692 pred_label 

1693 ): 

1694 # remove the unused scratch bit 

1695 self.remove_last_scratch_bit() 

1696 

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

1698 creg_name = args[0].reg_name 

1699 bits, vals = zip(*sorted(zip(args, op.values))) 

1700 # check if whole register can be set at once 

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

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

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

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

1705 else: 

1706 for bit, value in zip(bits, vals): 

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

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

1709 

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

1711 l_args = args[op.n_inputs :] 

1712 r_args = args[: op.n_inputs] 

1713 l_name = l_args[0].reg_name 

1714 r_name = r_args[0].reg_name 

1715 # check if whole register can be set at once 

1716 if ( 

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

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

1719 ): 

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

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

1722 else: 

1723 for bit_l, bit_r in zip(l_args, r_args): 

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

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

1726 

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

1728 basic_op = op.basic_op 

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

1730 n_args = len(args) 

1731 assert n_args % basic_n == 0 

1732 arity = n_args // basic_n 

1733 

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

1735 poss_regs = [ 

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

1737 ] 

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

1739 # The operation is register-aligned. 

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

1741 else: 

1742 # The operation is not register-aligned. 

1743 for i in range(arity): 

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

1745 self.add_op(basic_op, basic_args) 

1746 

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

1748 # &, ^ and | gates 

1749 opstr = str(op) 

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

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

1752 label = self.strings.add_string( 

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

1754 ) 

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

1756 

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

1758 wexpr: WiredClExpr = op.expr 

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

1760 # variables to registers. 

1761 expr: ClExpr = wexpr.expr 

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

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

1764 output_posn: list[int] = wexpr.output_posn 

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

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

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

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

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

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

1771 if creg.to_list() == reg_args: 

1772 input_regs[i] = creg 

1773 break 

1774 else: 

1775 assert ( 

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

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

1778 ) 

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

1780 output_repr: str | None = None 

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

1782 n_output_args = len(output_args) 

1783 expect_reg_output = has_reg_output(expr.op) 

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

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

1786 if n_output_args == 1: 

1787 output_arg = output_args[0] 

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

1789 else: 

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

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

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

1793 if creg.to_list() == output_args: 

1794 output_repr = creg.name 

1795 break 

1796 assert output_repr is not None 

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

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

1799 self.strings.add_string( 

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

1801 ) 

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

1803 

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

1805 inputs: list[str] = [] 

1806 outputs: list[str] = [] 

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

1808 for in_width in sizes: 

1809 bits = args[:in_width] 

1810 args = args[in_width:] 

1811 regname = bits[0].reg_name 

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

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

1814 reglist.append(regname) 

1815 if outputs: 

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

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

1818 for variable in outputs: 

1819 self.mark_as_written(label, variable) 

1820 

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

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

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

1824 

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

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

1827 # that 0 <= param < 4 

1828 if param > 1: 

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

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

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

1832 # -1 <= param < 0 

1833 if param > 1: 

1834 param = -2 + param 

1835 self.strings.add_string("RZZ") 

1836 self.write_params([param]) 

1837 self.write_args(args) 

1838 

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

1840 n_ctrls = len(args) - 1 

1841 assert n_ctrls >= 0 

1842 match n_ctrls: 

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

1844 self.strings.add_string("x") 

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

1846 self.strings.add_string("cx") 

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

1848 self.strings.add_string("ccx") 

1849 case 3: 

1850 self.strings.add_string("c3x") 

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

1852 self.strings.add_string("c4x") 

1853 case _: 

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

1855 self.strings.add_string(" ") 

1856 self.write_args(args) 

1857 

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

1859 if op.data == "": 1859 ↛ 1860line 1859 didn't jump to line 1860 because the condition on line 1859 was never true

1860 opstr = _tk_to_qasm_noparams[OpType.Barrier] 

1861 else: 

1862 opstr = op.data 

1863 self.strings.add_string(opstr) 

1864 self.strings.add_string(" ") 

1865 self.write_args(args) 

1866 

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

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

1869 self.strings.add_string(" ") 

1870 self.write_args(args) 

1871 

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

1873 optype, params = _get_optype_and_params(op) 

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

1875 self.write_params(params) 

1876 self.write_args(args) 

1877 

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

1879 optype = op.type 

1880 opstr = _tk_to_qasm_extra_noparams[optype] 

1881 gatedefstr = "" 

1882 if opstr not in self.added_gate_definitions: 

1883 self.added_gate_definitions.add(opstr) 

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

1885 mainstr = opstr + " " + make_args_str(args) 

1886 return gatedefstr, mainstr 

1887 

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

1889 optype, params = _get_optype_and_params(op) 

1890 assert params is not None 

1891 opstr = _tk_to_qasm_extra_params[optype] 

1892 gatedefstr = "" 

1893 if opstr not in self.added_gate_definitions: 

1894 self.added_gate_definitions.add(opstr) 

1895 gatedefstr = self.make_gate_definition( 

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

1897 ) 

1898 mainstr = opstr + make_params_str(params) + make_args_str(args) 

1899 return gatedefstr, mainstr 

1900 

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

1902 optype, _params = _get_optype_and_params(op) 

1903 if optype == OpType.RangePredicate: 

1904 assert isinstance(op, RangePredicateOp) 

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

1906 elif optype == OpType.Conditional: 

1907 assert isinstance(op, Conditional) 

1908 self.add_conditional(op, args) 

1909 elif optype == OpType.Phase: 

1910 # global phase is ignored in QASM 

1911 pass 

1912 elif optype == OpType.SetBits: 

1913 assert isinstance(op, SetBitsOp) 

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

1915 elif optype == OpType.CopyBits: 

1916 assert isinstance(op, CopyBitsOp) 

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

1918 elif optype == OpType.MultiBit: 

1919 assert isinstance(op, MultiBitOp) 

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

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

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

1923 elif optype == OpType.ClExpr: 

1924 assert isinstance(op, ClExprOp) 

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

1926 elif optype == OpType.WASM: 

1927 assert isinstance(op, WASMOp) 

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

1929 elif optype == OpType.Measure: 

1930 self.add_measure(args) 

1931 elif hqs_header(self.header) and optype == OpType.ZZPhase: 

1932 # special handling for zzphase 

1933 assert len(op.params) == 1 

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

1935 elif optype == OpType.CnX: 

1936 self.add_cnx(args) 

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

1938 assert isinstance(op, BarrierOp) 

1939 self.add_data(op, args) 

1940 elif ( 

1941 optype in _tk_to_qasm_noparams 

1942 and _tk_to_qasm_noparams[optype] in self.include_module_gates 

1943 ): 

1944 self.add_gate_noparams(op, args) 

1945 elif ( 

1946 optype in _tk_to_qasm_params 

1947 and _tk_to_qasm_params[optype] in self.include_module_gates 

1948 ): 

1949 self.add_gate_params(op, args) 

1950 elif optype in _tk_to_qasm_extra_noparams: 

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

1952 self.gatedefs += gatedefstr 

1953 self.strings.add_string(mainstr) 

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

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

1956 self.gatedefs += gatedefstr 

1957 self.strings.add_string(mainstr) 

1958 else: 

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

1960 

1961 def finalize(self) -> str: 

1962 # try removing unused predicates 

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

1964 for label in pred_labels: 

1965 # try replacing conditions with a predicate 

1966 self.replace_condition(label) 

1967 # try removing the predicate 

1968 self.remove_unused_predicate(label) 

1969 reg_strings = LabelledStringList() 

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

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

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

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

1974 if self.scratch_reg.size > 0: 

1975 reg_strings.add_string( 

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

1977 ) 

1978 return ( 

1979 self.prefix 

1980 + self.gatedefs 

1981 + _filtered_qasm_str( 

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

1983 ) 

1984 ) 

1985 

1986 

1987def circuit_to_qasm_io( 

1988 circ: Circuit, 

1989 stream_out: TextIO, 

1990 header: str = "qelib1", 

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

1992 maxwidth: int = 32, 

1993) -> None: 

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

1995 

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

1997 

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

1999 

2000 :param circ: pytket circuit 

2001 :param stream_out: text stream to be written to 

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

2003 :param include_gate_defs: optional set of gates to include 

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

2005 """ 

2006 stream_out.write( 

2007 circuit_to_qasm_str( 

2008 circ, header=header, include_gate_defs=include_gate_defs, maxwidth=maxwidth 

2009 ) 

2010 )