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

1043 statements  

« prev     ^ index     » next       coverage.py v7.8.0, created at 2025-04-17 10:53 +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 isinstance(val, int) 

627 assert val in (0, 1) 

628 if condition.op == BitWiseOp.NEQ: 

629 condition.op = BitWiseOp.EQ 

630 val = 1 ^ val 

631 condition_bits = [var.to_list()] 

632 

633 else: 

634 assert isinstance(var, BitRegister) 

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

636 if isinstance(condition, RegEq): 

637 # special case for base qasm 

638 condition_bits = reg_bits 

639 else: 

640 pred_val = cast(int, val) 

641 minval = 0 

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

643 if condition.op == RegWiseOp.LT: 

644 maxval = pred_val - 1 

645 elif condition.op == RegWiseOp.GT: 

646 minval = pred_val + 1 

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

648 maxval = pred_val 

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

650 minval = pred_val 

651 

652 condition_bit = self._fresh_temp_bit() 

653 yield { 

654 "args": reg_bits + [condition_bit], 

655 "op": { 

656 "classical": { 

657 "lower": minval, 

658 "n_i": len(reg_bits), 

659 "upper": maxval, 

660 }, 

661 "type": "RangePredicate", 

662 }, 

663 } 

664 condition_bits = [condition_bit] 

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

666 

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

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

669 com["op"] = { 

670 "conditional": { 

671 "op": com["op"], 

672 "value": val, 

673 "width": len(condition_bits), 

674 }, 

675 "type": "Conditional", 

676 } 

677 

678 yield com 

679 

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

681 return tree[0] 

682 

683 def _calc_exp_io( 

684 self, exp: LogicExp, out_args: list 

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

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

687 for inp in exp.all_inputs_ordered(): 

688 if isinstance(inp, Bit): 

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

690 else: 

691 assert isinstance(inp, BitRegister) 

692 for bit in inp: 

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

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

695 o = [] 

696 io = [] 

697 for out in outs: 

698 if out in all_inps: 

699 all_inps.remove(out) 

700 io.append(out) 

701 else: 

702 o.append(out) 

703 

704 exp_args = list( 

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

706 ) 

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]: 

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): 783 ↛ 784line 783 didn't jump to line 784 because the condition on line 783 was never true

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(), comparison_circ.get_commands() 

906 ) 

907 ) 

908 if not existing_op: 

909 gate_circ.symbol_substitution(symbol_map) 

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

911 self.gate_dict[gate] = { 

912 "definition": gate_circ.to_dict(), 

913 "args": symbols, 

914 "name": gate, 

915 } 

916 

917 opaq = gdef 

918 

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

920 return Discard 

921 

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

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

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

925 

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

927 outdict: dict[str, Any] = { 

928 "commands": list( 

929 chain.from_iterable( 

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

931 ) 

932 ) 

933 } 

934 if self.return_gate_dict: 

935 return self.gate_dict 

936 outdict["qubits"] = [ 

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

938 ] 

939 outdict["bits"] = [ 

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

941 ] 

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

943 outdict["phase"] = "0.0" 

944 self._reset_context() 

945 return outdict 

946 

947 

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

949 return Lark( 

950 grammar, 

951 start="prog", 

952 debug=False, 

953 parser="lalr", 

954 cache=True, 

955 transformer=CircuitTransformer(maxwidth=maxwidth), 

956 ) 

957 

958 

959g_parser = None 

960g_maxwidth = 32 

961 

962 

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

964 global g_parser, g_maxwidth 

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

966 g_parser = parser(maxwidth=maxwidth) 

967 g_maxwidth = maxwidth 

968 

969 

970def circuit_from_qasm( 

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

972 encoding: str = "utf-8", 

973 maxwidth: int = 32, 

974) -> Circuit: 

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

976 

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

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

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

980 :return: pytket circuit 

981 """ 

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

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

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

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

986 try: 

987 circ = circuit_from_qasm_io(f, maxwidth=maxwidth) 

988 except QASMParseError as e: 

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

990 return circ 

991 

992 

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

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

995 

996 :param qasm_str: qasm string 

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

998 :return: pytket circuit 

999 """ 

1000 global g_parser 

1001 set_parser(maxwidth=maxwidth) 

1002 assert g_parser is not None 

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

1004 reset_wasm=False 

1005 ) 

1006 

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

1008 cpass = scratch_reg_resize_pass(maxwidth) 

1009 cpass.apply(circ) 

1010 return circ 

1011 

1012 

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

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

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

1016 

1017 

1018def circuit_from_qasm_wasm( 

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

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

1021 encoding: str = "utf-8", 

1022 maxwidth: int = 32, 

1023) -> Circuit: 

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

1025 

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

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

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

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

1030 :return: pytket circuit 

1031 """ 

1032 global g_parser 

1033 wasm_module = WasmFileHandler(str(wasm_file)) 

1034 set_parser(maxwidth=maxwidth) 

1035 assert g_parser is not None 

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

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

1038 

1039 

1040def circuit_to_qasm( 

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

1042) -> None: 

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

1044 

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

1046 

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

1048 

1049 :param circ: pytket circuit 

1050 :param output_file: path to output qasm file 

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

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

1053 """ 

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

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

1056 

1057 

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

1059 # remove any c registers starting with _TEMP_BIT_NAME 

1060 # that are not being used somewhere else 

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

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

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

1064 unused_regs = dict() 

1065 for i, line in enumerate(lines): 

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

1067 # Mark a reg temporarily as unused 

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

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

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

1071 # mark these regs as used 

1072 for arg in args: 

1073 if arg in unused_regs: 

1074 unused_regs.pop(arg) 

1075 # remove unused reg defs 

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

1077 for line_index in redundant_lines: 

1078 del lines[line_index] 

1079 return "\n".join(lines) 

1080 

1081 

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

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

1084 

1085 

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

1087 if any( 

1088 circ.n_gates_of_type(typ) 

1089 for typ in ( 

1090 OpType.RangePredicate, 

1091 OpType.MultiBit, 

1092 OpType.ExplicitPredicate, 

1093 OpType.ExplicitModifier, 

1094 OpType.SetBits, 

1095 OpType.CopyBits, 

1096 ) 

1097 ) and (not hqs_header(header)): 

1098 raise QASMUnsupportedError( 

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

1100 "`header=hqslib1`" 

1101 ) 

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

1103 raise QASMUnsupportedError( 

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

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

1106 ) 

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

1108 for b in circ.bits: 

1109 if b.reg_name not in set_circ_register: 

1110 raise QASMUnsupportedError( 

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

1112 ) 

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

1114 for cmd in circ: 

1115 assert not is_empty_customgate(cmd.op) 

1116 if isinstance(cmd.op, Conditional): 

1117 assert not is_empty_customgate(cmd.op.op) 

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

1119 raise QASMUnsupportedError( 

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

1121 "outputs are not register-aligned." 

1122 ) 

1123 

1124 

1125def circuit_to_qasm_str( 

1126 circ: Circuit, 

1127 header: str = "qelib1", 

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

1129 maxwidth: int = 32, 

1130) -> str: 

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

1132 

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

1134 

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

1136 

1137 :param circ: pytket circuit 

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

1139 :param output_file: path to output qasm file 

1140 :param include_gate_defs: optional set of gates to include 

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

1142 :return: qasm string 

1143 """ 

1144 

1145 qasm_writer = QasmWriter( 

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

1147 ) 

1148 circ1 = circ.copy() 

1149 DecomposeBoxes().apply(circ1) 

1150 check_can_convert_circuit(circ1, header, maxwidth) 

1151 for command in circ1: 

1152 assert isinstance(command, Command) 

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

1154 return qasm_writer.finalize() 

1155 

1156 

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

1158 

1159 

1160def _retrieve_registers( 

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

1162) -> dict[str, TypeReg]: 

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

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

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

1166 return { 

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

1168 for maxunit in maxunits 

1169 } 

1170 

1171 

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

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

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

1175 

1176 REGMAX = (1 << maxwidth) - 1 

1177 

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

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

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

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

1182 maxval = min(maxval, REGMAX) 

1183 

1184 if minval == maxval: 

1185 return ("==", minval) 

1186 if minval == 0: 

1187 return ("<=", maxval) 

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

1189 return (">=", minval) 

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

1191 

1192 

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

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 == "<=": 

1199 return ">" 

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

1201 return "<=" 

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

1203 return "<" 

1204 assert comparator == "<" 

1205 return ">=" 

1206 

1207 

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

1209 optype = op.type 

1210 params = ( 

1211 op.params 

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

1213 else None 

1214 ) 

1215 if optype == OpType.TK1: 

1216 # convert to U3 

1217 optype = OpType.U3 

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

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

1220 params = op.params 

1221 return optype, params 

1222 

1223 

1224def _get_gate_circuit( 

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

1226) -> Circuit: 

1227 # create Circuit for constructing qasm from 

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

1229 gate_circ = Circuit() 

1230 for q in qubits: 

1231 gate_circ.add_qubit(q) 

1232 if symbols: 

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

1234 gate_circ.add_gate(optype, exprs, unitids) 

1235 else: 

1236 gate_circ.add_gate(optype, unitids) 

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

1238 RemoveRedundancies().apply(gate_circ) 

1239 

1240 return gate_circ 

1241 

1242 

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

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

1245 

1246 

1247@dataclass 

1248class ConditionString: 

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

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

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

1252 

1253 

1254class LabelledStringList: 

1255 """ 

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

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

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

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

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

1261 retrieving the full string. 

1262 """ 

1263 

1264 def __init__(self) -> None: 

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

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

1267 self.label = 0 

1268 

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

1270 label = self.label 

1271 self.strings[label] = string 

1272 self.label += 1 

1273 return label 

1274 

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

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

1277 

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

1279 self.strings.pop(label, None) 

1280 

1281 def get_full_string(self) -> str: 

1282 strings = [] 

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

1284 condition = self.conditions.get(l) 

1285 if condition is not None: 

1286 strings.append( 

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

1288 + s 

1289 ) 

1290 else: 

1291 strings.append(s) 

1292 return "".join(strings) 

1293 

1294 

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

1296 s = "" 

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

1298 n_params = len(params) 

1299 s += "(" 

1300 for i in range(n_params): 

1301 reduced = True 

1302 try: 

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

1304 except TypeError: 

1305 reduced = False 

1306 p = params[i] 

1307 if i < n_params - 1: 

1308 if reduced: 

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

1310 else: 

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

1312 elif reduced: 

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

1314 else: 

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

1316 s += " " 

1317 return s 

1318 

1319 

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

1321 s = "" 

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

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

1324 if i < len(args) - 1: 

1325 s += "," 

1326 else: 

1327 s += ";\n" 

1328 return s 

1329 

1330 

1331@dataclass 

1332class ScratchPredicate: 

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

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

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

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

1337 

1338 

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

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

1341 v_split = v.split("[") 

1342 w_split = w.split("[") 

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

1344 # different registers 

1345 return False 

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

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

1348 

1349 

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

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

1352 v_split = v.split("[") 

1353 if len(v_split) == 1: 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 surrounded by word characters 

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

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

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

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

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

1360 return True 

1361 # check the register of v appears in s 

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

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

1364 

1365 

1366class QasmWriter: 

1367 """ 

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

1369 final QASM string afterwards. 

1370 """ 

1371 

1372 def __init__( 

1373 self, 

1374 qubits: list[Qubit], 

1375 bits: list[Bit], 

1376 header: str = "qelib1", 

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

1378 maxwidth: int = 32, 

1379 ): 

1380 self.header = header 

1381 self.maxwidth = maxwidth 

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

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

1384 self.include_module_gates.update( 

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

1386 ) 

1387 self.prefix = "" 

1388 self.gatedefs = "" 

1389 self.strings = LabelledStringList() 

1390 

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

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

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

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

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

1396 

1397 if include_gate_defs is None: 

1398 self.include_gate_defs = self.include_module_gates 

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

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

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

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

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

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

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

1406 raise QASMUnsupportedError( 

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

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

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

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

1411 ) 

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

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

1414 raise QASMUnsupportedError( 

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

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

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

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

1419 ) 

1420 else: 

1421 # gate definition, no header necessary for file 

1422 self.include_gate_defs = include_gate_defs 

1423 self.cregs = {} 

1424 self.qregs = {} 

1425 

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

1427 

1428 # for holding condition values when writing Conditional blocks 

1429 # the size changes when adding and removing scratch bits 

1430 self.scratch_reg = BitRegister( 

1431 next( 

1432 f"{_TEMP_BIT_REG_BASE}_{i}" 

1433 for i in itertools.count() 

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

1435 ), 

1436 0, 

1437 ) 

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

1439 # the affected variables will be recorded. 

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

1441 

1442 def fresh_scratch_bit(self) -> Bit: 

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

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

1445 

1446 def remove_last_scratch_bit(self) -> None: 

1447 assert self.scratch_reg.size > 0 

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

1449 

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

1451 params_str = make_params_str(params) 

1452 self.strings.add_string(params_str) 

1453 

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

1455 args_str = make_args_str(args) 

1456 self.strings.add_string(args_str) 

1457 

1458 def make_gate_definition( 

1459 self, 

1460 n_qubits: int, 

1461 opstr: str, 

1462 optype: OpType, 

1463 n_params: int | None = None, 

1464 ) -> str: 

1465 s = "gate " + opstr + " " 

1466 symbols: list[Symbol] | None = None 

1467 if n_params is not None: 

1468 # need to add parameters to gate definition 

1469 s += "(" 

1470 symbols = [ 

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

1472 ] 

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

1474 for symbol in symbols_header[:-1]: 

1475 s += symbol.name + ", " 

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

1477 

1478 # add qubits to gate definition 

1479 qubit_args = [ 

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

1481 ] 

1482 for qb in qubit_args[:-1]: 

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

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

1485 # get rebased circuit for constructing qasm 

1486 gate_circ = _get_gate_circuit(optype, qubit_args, symbols) 

1487 # write circuit to qasm 

1488 s += circuit_to_qasm_str( 

1489 gate_circ, self.header, self.include_gate_defs, self.maxwidth 

1490 ) 

1491 s += "}\n" 

1492 return s 

1493 

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

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

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

1497 else: 

1498 self.variable_writes[label] = [written_variable] 

1499 

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

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

1502 raise QASMUnsupportedError( 

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

1504 ) 

1505 variable = args[0].reg_name 

1506 assert isinstance(variable, str) 

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

1508 raise QASMUnsupportedError( 

1509 "RangePredicate conditions must be an entire classical register" 

1510 ) 

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

1512 raise QASMUnsupportedError( 

1513 "RangePredicate conditions must be a single classical register" 

1514 ) 

1515 

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

1517 self.check_range_predicate(op, args) 

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

1519 variable = args[0].reg_name 

1520 dest_bit = str(args[-1]) 

1521 label = self.strings.add_string( 

1522 "".join( 

1523 [ 

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

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

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

1527 ] 

1528 ) 

1529 ) 

1530 # Record this operation. 

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

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

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

1534 # list if it is.) 

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

1536 if dest_bit.startswith(_TEMP_BIT_NAME): 

1537 self.range_preds[label] = ScratchPredicate( 

1538 variable, comparator, value, dest_bit 

1539 ) 

1540 

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

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

1543 we scan the lines after p: 

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

1545 the predicate and do 2 for the inner command. 

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

1547 returns true if a replacement is made. 

1548 """ 

1549 assert pred_label in self.range_preds 

1550 success = False 

1551 pred = self.range_preds[pred_label] 

1552 line_labels = [] 

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

1554 string = self.strings.get_string(label) 

1555 if string is None: 

1556 continue 

1557 line_labels.append(label) 

1558 if "\n" not in string: 

1559 continue 

1560 written_variables: list[str] = [] 

1561 # (label, condition) 

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

1563 for l in line_labels: 

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

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

1566 if cond: 

1567 conditions.append((l, cond)) 

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

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

1570 success = True 

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

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

1573 pred.variable, pred.comparator, pred.value 

1574 ) 

1575 else: 

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

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

1578 pred.variable, 

1579 _negate_comparator(pred.comparator), 

1580 pred.value, 

1581 ) 

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

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

1584 ): 

1585 return success 

1586 line_labels.clear() 

1587 conditions.clear() 

1588 written_variables.clear() 

1589 return success 

1590 

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

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

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

1594 assert pred_label in self.range_preds 

1595 pred = self.range_preds[pred_label] 

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 if ( 

1601 _var_appears(pred.dest, string) 

1602 or label in self.strings.conditions 

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

1604 ): 

1605 return False 

1606 self.range_preds.pop(pred_label) 

1607 self.strings.del_string(pred_label) 

1608 return True 

1609 

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

1611 control_bits = args[: op.width] 

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

1613 variable = str(control_bits[0]) 

1614 else: 

1615 variable = control_bits[0].reg_name 

1616 if ( 

1617 hqs_header(self.header) 

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

1619 ): 

1620 raise QASMUnsupportedError( 

1621 "hqslib1 QASM conditions must be an entire classical " 

1622 "register or a single bit" 

1623 ) 

1624 if not hqs_header(self.header): 

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

1626 raise QASMUnsupportedError( 

1627 "OpenQASM conditions must be an entire classical register" 

1628 ) 

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

1630 raise QASMUnsupportedError( 

1631 "OpenQASM conditions must be a single classical register" 

1632 ) 

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

1634 # Conditional phase is ignored. 

1635 return 

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

1637 # Special handling for nested ifs 

1638 # if condition 

1639 # if pred dest = 1 

1640 # if not pred dest = 0 

1641 # can be written as 

1642 # if condition s0 = 1 

1643 # if pred s1 = 1 

1644 # s2 = s0 & s1 

1645 # s3 = s0 & ~s1 

1646 # if s2 dest = 1 

1647 # if s3 dest = 0 

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

1649 s0 = self.fresh_scratch_bit() 

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

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

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

1653 # if possible 

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

1655 # output the RangePredicate to s1 

1656 s1 = self.fresh_scratch_bit() 

1657 assert isinstance(op.op, RangePredicateOp) 

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

1659 pred_comparator, pred_value = _parse_range( 

1660 op.op.lower, op.op.upper, self.maxwidth 

1661 ) 

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

1663 self.strings.add_string( 

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

1665 ) 

1666 s2 = self.fresh_scratch_bit() 

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

1668 s3 = self.fresh_scratch_bit() 

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

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

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

1672 return 

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

1674 # if the condition variable is unchanged. 

1675 scratch_bit = self.fresh_scratch_bit() 

1676 pred_label = self.strings.add_string( 

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

1678 ) 

1679 self.range_preds[pred_label] = ScratchPredicate( 

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

1681 ) 

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

1683 next_label = self.strings.label 

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

1685 # add conditions to the lines after the predicate 

1686 is_new_line = True 

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

1688 string = self.strings.get_string(label) 

1689 assert string is not None 

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

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

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

1693 ) 

1694 is_new_line = "\n" in string 

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

1696 pred_label 

1697 ): 

1698 # remove the unused scratch bit 

1699 self.remove_last_scratch_bit() 

1700 

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

1702 creg_name = args[0].reg_name 

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

1704 # check if whole register can be set at once 

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

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

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

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

1709 else: 

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

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

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

1713 

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

1715 l_args = args[op.n_inputs :] 

1716 r_args = args[: op.n_inputs] 

1717 l_name = l_args[0].reg_name 

1718 r_name = r_args[0].reg_name 

1719 # check if whole register can be set at once 

1720 if ( 

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

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

1723 ): 

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

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

1726 else: 

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

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

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

1730 

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

1732 basic_op = op.basic_op 

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

1734 n_args = len(args) 

1735 assert n_args % basic_n == 0 

1736 arity = n_args // basic_n 

1737 

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

1739 poss_regs = [ 

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

1741 ] 

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

1743 # The operation is register-aligned. 

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

1745 else: 

1746 # The operation is not register-aligned. 

1747 for i in range(arity): 

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

1749 self.add_op(basic_op, basic_args) 

1750 

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

1752 # &, ^ and | gates 

1753 opstr = str(op) 

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

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

1756 label = self.strings.add_string( 

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

1758 ) 

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

1760 

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

1762 wexpr: WiredClExpr = op.expr 

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

1764 # variables to registers. 

1765 expr: ClExpr = wexpr.expr 

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

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

1768 output_posn: list[int] = wexpr.output_posn 

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

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

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

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

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

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

1775 if creg.to_list() == reg_args: 

1776 input_regs[i] = creg 

1777 break 

1778 else: 

1779 assert ( 

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

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

1782 ) 

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

1784 output_repr: str | None = None 

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

1786 n_output_args = len(output_args) 

1787 expect_reg_output = has_reg_output(expr.op) 

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

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

1790 if n_output_args == 1: 

1791 output_arg = output_args[0] 

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

1793 else: 

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

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

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

1797 if creg.to_list() == output_args: 

1798 output_repr = creg.name 

1799 break 

1800 assert output_repr is not None 

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

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

1803 self.strings.add_string( 

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

1805 ) 

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

1807 

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

1809 inputs: list[str] = [] 

1810 outputs: list[str] = [] 

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

1812 for in_width in sizes: 

1813 bits = args[:in_width] 

1814 args = args[in_width:] 

1815 regname = bits[0].reg_name 

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

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

1818 reglist.append(regname) 

1819 if outputs: 

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

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

1822 for variable in outputs: 

1823 self.mark_as_written(label, variable) 

1824 

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

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

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

1828 

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

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

1831 # that 0 <= param < 4 

1832 if param > 1: 

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

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

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

1836 # -1 <= param < 0 

1837 if param > 1: 

1838 param = -2 + param 

1839 self.strings.add_string("RZZ") 

1840 self.write_params([param]) 

1841 self.write_args(args) 

1842 

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

1844 n_ctrls = len(args) - 1 

1845 assert n_ctrls >= 0 

1846 match n_ctrls: 

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

1848 self.strings.add_string("x") 

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

1850 self.strings.add_string("cx") 

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

1852 self.strings.add_string("ccx") 

1853 case 3: 

1854 self.strings.add_string("c3x") 

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

1856 self.strings.add_string("c4x") 

1857 case _: 

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

1859 self.strings.add_string(" ") 

1860 self.write_args(args) 

1861 

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

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

1864 opstr = _tk_to_qasm_noparams[OpType.Barrier] 

1865 else: 

1866 opstr = op.data 

1867 self.strings.add_string(opstr) 

1868 self.strings.add_string(" ") 

1869 self.write_args(args) 

1870 

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

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

1873 self.strings.add_string(" ") 

1874 self.write_args(args) 

1875 

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

1877 optype, params = _get_optype_and_params(op) 

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

1879 self.write_params(params) 

1880 self.write_args(args) 

1881 

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

1883 optype = op.type 

1884 opstr = _tk_to_qasm_extra_noparams[optype] 

1885 gatedefstr = "" 

1886 if opstr not in self.added_gate_definitions: 

1887 self.added_gate_definitions.add(opstr) 

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

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

1890 return gatedefstr, mainstr 

1891 

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

1893 optype, params = _get_optype_and_params(op) 

1894 assert params is not None 

1895 opstr = _tk_to_qasm_extra_params[optype] 

1896 gatedefstr = "" 

1897 if opstr not in self.added_gate_definitions: 

1898 self.added_gate_definitions.add(opstr) 

1899 gatedefstr = self.make_gate_definition( 

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

1901 ) 

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

1903 return gatedefstr, mainstr 

1904 

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

1906 optype, _params = _get_optype_and_params(op) 

1907 if optype == OpType.RangePredicate: 

1908 assert isinstance(op, RangePredicateOp) 

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

1910 elif optype == OpType.Conditional: 

1911 assert isinstance(op, Conditional) 

1912 self.add_conditional(op, args) 

1913 elif optype == OpType.Phase: 

1914 # global phase is ignored in QASM 

1915 pass 

1916 elif optype == OpType.SetBits: 

1917 assert isinstance(op, SetBitsOp) 

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

1919 elif optype == OpType.CopyBits: 

1920 assert isinstance(op, CopyBitsOp) 

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

1922 elif optype == OpType.MultiBit: 

1923 assert isinstance(op, MultiBitOp) 

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

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

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

1927 elif optype == OpType.ClExpr: 

1928 assert isinstance(op, ClExprOp) 

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

1930 elif optype == OpType.WASM: 

1931 assert isinstance(op, WASMOp) 

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

1933 elif optype == OpType.Measure: 

1934 self.add_measure(args) 

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

1936 # special handling for zzphase 

1937 assert len(op.params) == 1 

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

1939 elif optype == OpType.CnX: 

1940 self.add_cnx(args) 

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

1942 assert isinstance(op, BarrierOp) 

1943 self.add_data(op, args) 

1944 elif ( 

1945 optype in _tk_to_qasm_noparams 

1946 and _tk_to_qasm_noparams[optype] in self.include_module_gates 

1947 ): 

1948 self.add_gate_noparams(op, args) 

1949 elif ( 

1950 optype in _tk_to_qasm_params 

1951 and _tk_to_qasm_params[optype] in self.include_module_gates 

1952 ): 

1953 self.add_gate_params(op, args) 

1954 elif optype in _tk_to_qasm_extra_noparams: 

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

1956 self.gatedefs += gatedefstr 

1957 self.strings.add_string(mainstr) 

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

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

1960 self.gatedefs += gatedefstr 

1961 self.strings.add_string(mainstr) 

1962 else: 

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

1964 

1965 def finalize(self) -> str: 

1966 # try removing unused predicates 

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

1968 for label in pred_labels: 

1969 # try replacing conditions with a predicate 

1970 self.replace_condition(label) 

1971 # try removing the predicate 

1972 self.remove_unused_predicate(label) 

1973 reg_strings = LabelledStringList() 

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

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

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

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

1978 if self.scratch_reg.size > 0: 

1979 reg_strings.add_string( 

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

1981 ) 

1982 return ( 

1983 self.prefix 

1984 + self.gatedefs 

1985 + _filtered_qasm_str( 

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

1987 ) 

1988 ) 

1989 

1990 

1991def circuit_to_qasm_io( 

1992 circ: Circuit, 

1993 stream_out: TextIO, 

1994 header: str = "qelib1", 

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

1996 maxwidth: int = 32, 

1997) -> None: 

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

1999 

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

2001 

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

2003 

2004 :param circ: pytket circuit 

2005 :param stream_out: text stream to be written to 

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

2007 :param include_gate_defs: optional set of gates to include 

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

2009 """ 

2010 stream_out.write( 

2011 circuit_to_qasm_str( 

2012 circ, header=header, include_gate_defs=include_gate_defs, maxwidth=maxwidth 

2013 ) 

2014 )