Coverage for /home/runner/work/tket/tket/pytket/pytket/utils/symbolic.py: 69%
166 statements
« prev ^ index » next coverage.py v7.6.12, created at 2025-03-14 11:30 +0000
« 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.
15"""Collection of methods to calculate symbolic statevectors and unitaries,
16for symbolic circuits. This uses the sympy.physics.quantum module and produces
17sympy objects. The implementations are slow and scale poorly, so this is
18only suitable for very small (up to 5 qubit) circuits."""
19from collections.abc import Callable
20from typing import cast
22import numpy as np
23import sympy
24from sympy import (
25 BlockDiagMatrix,
26 BlockMatrix,
27 Expr,
28 I,
29 Identity,
30 ImmutableMatrix,
31 Matrix,
32 Mul,
33 diag,
34 eye,
35 zeros,
36)
37from sympy.physics.quantum import gate as symgate
38from sympy.physics.quantum import represent
39from sympy.physics.quantum.qapply import qapply
40from sympy.physics.quantum.qubit import Qubit, matrix_to_qubit
41from sympy.physics.quantum.tensorproduct import matrix_tensor_product
43from pytket.circuit import Circuit, Op, OpType
45# gates that have an existing definition in sympy
46_FIXED_GATE_MAP: dict[OpType, type[symgate.Gate]] = {
47 OpType.H: symgate.HadamardGate,
48 OpType.S: symgate.PhaseGate,
49 OpType.CX: symgate.CNotGate,
50 OpType.SWAP: symgate.SwapGate,
51 OpType.T: symgate.TGate,
52 OpType.X: symgate.XGate,
53 OpType.Y: symgate.YGate,
54 OpType.Z: symgate.ZGate,
55}
57ParamsType = list[Expr | float]
58# Make sure the return matrix is Immutable https://github.com/sympy/sympy/issues/18733
59SymGateFunc = Callable[[ParamsType], ImmutableMatrix]
60SymGateMap = dict[OpType, SymGateFunc]
62# Begin matrix definitions for symbolic OpTypes
63# matches internal TKET definitions
64# see OpType documentation
67def symb_controlled(target: SymGateFunc) -> SymGateFunc:
68 return lambda x: ImmutableMatrix(BlockDiagMatrix(Identity(2), target(x)))
71def symb_rz(params: ParamsType) -> ImmutableMatrix:
72 return ImmutableMatrix(
73 [
74 [sympy.exp(-I * (sympy.pi / 2) * params[0]), 0],
75 [0, sympy.exp(I * (sympy.pi / 2) * params[0])],
76 ]
77 )
80def symb_rx(params: ParamsType) -> ImmutableMatrix:
81 costerm = sympy.cos((sympy.pi / 2) * params[0])
82 sinterm = -I * sympy.sin((sympy.pi / 2) * params[0])
83 return ImmutableMatrix(
84 [
85 [costerm, sinterm],
86 [sinterm, costerm],
87 ]
88 )
91def symb_ry(params: ParamsType) -> ImmutableMatrix:
92 costerm = sympy.cos((sympy.pi / 2) * params[0])
93 sinterm = sympy.sin((sympy.pi / 2) * params[0])
94 return ImmutableMatrix(
95 [
96 [costerm, -sinterm],
97 [sinterm, costerm],
98 ]
99 )
102def symb_u3(params: ParamsType) -> ImmutableMatrix:
103 theta, phi, lam = params
104 costerm = sympy.cos((sympy.pi / 2) * theta)
105 sinterm = sympy.sin((sympy.pi / 2) * theta)
106 return ImmutableMatrix(
107 [
108 [costerm, -sinterm * sympy.exp(I * sympy.pi * lam)],
109 [
110 sinterm * sympy.exp(I * sympy.pi * phi),
111 costerm * sympy.exp(I * sympy.pi * (phi + lam)),
112 ],
113 ]
114 )
117def symb_u2(params: ParamsType) -> ImmutableMatrix:
118 return symb_u3([0.5] + params)
121def symb_u1(params: ParamsType) -> ImmutableMatrix:
122 return symb_u3([0.0, 0.0] + params)
125def symb_tk1(params: ParamsType) -> ImmutableMatrix:
126 return symb_rz([params[0]]) * symb_rx([params[1]]) * symb_rz([params[2]])
129def symb_tk2(params: ParamsType) -> ImmutableMatrix:
130 return (
131 symb_xxphase([params[0]])
132 * symb_yyphase([params[1]])
133 * symb_zzphase([params[2]])
134 )
137def symb_iswap(params: ParamsType) -> ImmutableMatrix:
138 alpha = params[0]
139 costerm = sympy.cos((sympy.pi / 2) * alpha)
140 sinterm = sympy.sin((sympy.pi / 2) * alpha)
141 return ImmutableMatrix(
142 [
143 [1, 0, 0, 0],
144 [0, costerm, I * sinterm, 0],
145 [0, I * sinterm, costerm, 0],
146 [0, 0, 0, 1],
147 ]
148 )
151def symb_phasediswap(params: ParamsType) -> ImmutableMatrix:
152 p, alpha = params
153 costerm = sympy.cos((sympy.pi / 2) * alpha)
154 sinterm = I * sympy.sin((sympy.pi / 2) * alpha)
155 phase = sympy.exp(2 * I * sympy.pi * p)
156 return ImmutableMatrix(
157 [
158 [1, 0, 0, 0],
159 [0, costerm, sinterm * phase, 0],
160 [0, sinterm / phase, costerm, 0],
161 [0, 0, 0, 1],
162 ]
163 )
166def symb_xxphase(params: ParamsType) -> ImmutableMatrix:
167 alpha = params[0]
168 c = sympy.cos((sympy.pi / 2) * alpha)
169 s = -I * sympy.sin((sympy.pi / 2) * alpha)
170 return ImmutableMatrix(
171 [
172 [c, 0, 0, s],
173 [0, c, s, 0],
174 [0, s, c, 0],
175 [s, 0, 0, c],
176 ]
177 )
180def symb_yyphase(params: ParamsType) -> ImmutableMatrix:
181 alpha = params[0]
182 c = sympy.cos((sympy.pi / 2) * alpha)
183 s = I * sympy.sin((sympy.pi / 2) * alpha)
184 return ImmutableMatrix(
185 [
186 [c, 0, 0, s],
187 [0, c, -s, 0],
188 [0, -s, c, 0],
189 [s, 0, 0, c],
190 ]
191 )
194def symb_zzphase(params: ParamsType) -> ImmutableMatrix:
195 alpha = params[0]
196 t = sympy.exp(I * (sympy.pi / 2) * alpha)
197 return ImmutableMatrix(diag(1 / t, t, t, 1 / t))
200def symb_xxphase3(params: ParamsType) -> ImmutableMatrix:
201 xxphase2 = symb_xxphase(params)
202 res1 = matrix_tensor_product(xxphase2, eye(2))
203 res2 = Matrix(
204 BlockMatrix(
205 [
206 [xxphase2[:2, :2], zeros(2), xxphase2[:2, 2:], zeros(2)],
207 [zeros(2), xxphase2[:2, :2], zeros(2), xxphase2[:2, 2:]],
208 [xxphase2[2:, :2], zeros(2), xxphase2[2:, 2:], zeros(2)],
209 [zeros(2), xxphase2[2:, :2], zeros(2), xxphase2[2:, 2:]],
210 ]
211 )
212 )
213 res3 = matrix_tensor_product(eye(2), xxphase2)
214 return ImmutableMatrix(res1 * res2 * res3)
217def symb_phasedx(params: ParamsType) -> ImmutableMatrix:
218 alpha, beta = params
220 return symb_rz([beta]) * symb_rx([alpha]) * symb_rz([-beta])
223def symb_eswap(params: ParamsType) -> ImmutableMatrix:
224 alpha = params[0]
225 c = sympy.cos((sympy.pi / 2) * alpha)
226 s = -I * sympy.sin((sympy.pi / 2) * alpha)
227 t = sympy.exp(-I * (sympy.pi / 2) * alpha)
229 return ImmutableMatrix(
230 [
231 [t, 0, 0, 0],
232 [0, c, s, 0],
233 [0, s, c, 0],
234 [0, 0, 0, t],
235 ]
236 )
239def symb_fsim(params: ParamsType) -> ImmutableMatrix:
240 alpha, beta = params
241 c = sympy.cos(sympy.pi * alpha)
242 s = -I * sympy.sin(sympy.pi * alpha)
243 t = sympy.exp(-I * sympy.pi * beta)
245 return ImmutableMatrix(
246 [
247 [1, 0, 0, 0],
248 [0, c, s, 0],
249 [0, s, c, 0],
250 [0, 0, 0, t],
251 ]
252 )
255def symb_gpi(params: ParamsType) -> ImmutableMatrix:
256 t = sympy.exp(I * sympy.pi * params[0])
258 return ImmutableMatrix(
259 [
260 [0, 1 / t],
261 [t, 0],
262 ]
263 )
266def symb_gpi2(params: ParamsType) -> ImmutableMatrix:
267 t = sympy.exp(I * sympy.pi * params[0])
268 c = 1 / sympy.sqrt(2)
270 return c * ImmutableMatrix(
271 [
272 [1, -I / t],
273 [-I * t, 1],
274 ]
275 )
278def symb_aams(params: ParamsType) -> ImmutableMatrix:
279 alpha, beta, gamma = params
280 c = sympy.cos(sympy.pi / 2 * alpha)
281 s = sympy.sin(sympy.pi / 2 * alpha)
282 s1 = -I * sympy.exp(I * sympy.pi * (-beta - gamma)) * s
283 s2 = -I * sympy.exp(I * sympy.pi * (-beta + gamma)) * s
284 s3 = -I * sympy.exp(I * sympy.pi * (beta - gamma)) * s
285 s4 = -I * sympy.exp(I * sympy.pi * (beta + gamma)) * s
287 return ImmutableMatrix(
288 [
289 [c, 0, 0, s1],
290 [0, c, s2, 0],
291 [0, s3, c, 0],
292 [s4, 0, 0, c],
293 ]
294 )
297# end symbolic matrix definitions
300class SymGateRegister:
301 """Static class holding mapping from OpType to callable generating symbolic matrix.
302 Allows users to add their own definitions, or override existing definitions."""
304 _g_map: SymGateMap = {
305 OpType.Rx: symb_rx,
306 OpType.Ry: symb_ry,
307 OpType.Rz: symb_rz,
308 OpType.TK1: symb_tk1,
309 OpType.TK2: symb_tk2,
310 OpType.U1: symb_u1,
311 OpType.U2: symb_u2,
312 OpType.U3: symb_u3,
313 OpType.CRx: symb_controlled(symb_rx),
314 OpType.CRy: symb_controlled(symb_ry),
315 OpType.CRz: symb_controlled(symb_rz),
316 OpType.CU1: symb_controlled(symb_u1),
317 OpType.CU3: symb_controlled(symb_u3),
318 OpType.ISWAP: symb_iswap,
319 OpType.PhasedISWAP: symb_phasediswap,
320 OpType.XXPhase: symb_xxphase,
321 OpType.YYPhase: symb_yyphase,
322 OpType.ZZPhase: symb_zzphase,
323 OpType.XXPhase3: symb_xxphase3,
324 OpType.PhasedX: symb_phasedx,
325 OpType.ESWAP: symb_eswap,
326 OpType.FSim: symb_fsim,
327 OpType.GPI: symb_gpi,
328 OpType.GPI2: symb_gpi2,
329 OpType.AAMS: symb_aams,
330 }
332 @classmethod
333 def register_func(cls, typ: OpType, f: SymGateFunc, replace: bool = False) -> None:
334 """Register a callable for an optype.
336 :param typ: OpType to register
337 :type typ: OpType
338 :param f: Callable for generating symbolic matrix.
339 :type f: SymGateFunc
340 :param replace: Whether to replace existing entry, defaults to False
341 :type replace: bool, optional
342 """
343 if typ not in cls._g_map or replace:
344 cls._g_map[typ] = f
346 @classmethod
347 def get_func(cls, typ: OpType) -> SymGateFunc:
348 """Get registered callable."""
349 return cls._g_map[typ]
351 @classmethod
352 def is_registered(cls, typ: OpType) -> bool:
353 """Check if type has a callable registered."""
354 return typ in cls._g_map
357def _op_to_sympy_gate(op: Op, targets: list[int]) -> symgate.Gate:
358 # convert Op to sympy gate
359 if op.type in _FIXED_GATE_MAP:
360 return _FIXED_GATE_MAP[op.type](*targets)
361 if op.is_gate(): 361 ↛ 365line 361 didn't jump to line 365 because the condition on line 361 was always true
362 # check if symbolic definition is needed
363 float_params = all(isinstance(p, float) for p in op.params)
364 else:
365 raise ValueError(
366 f"Circuit can only contain unitary gates, operation {op} not valid."
367 )
369 # pytket matrix basis indexing is in opposite order to sympy
370 targets.reverse()
371 if (not float_params) and SymGateRegister.is_registered(op.type):
372 u_mat = SymGateRegister.get_func(op.type)(op.params)
373 else:
374 try:
375 # use internal tket unitary definition
376 u_mat = ImmutableMatrix(op.get_unitary())
377 except RuntimeError as e:
378 # to catch tket failure to get Op unitary
379 # most likely due to symbolic parameters.
380 raise ValueError(
381 f"{op.type} is not supported for symbolic conversion."
382 " Try registering your own symbolic matrix representation"
383 " with SymGateRegister.func."
384 ) from e
385 return symgate.UGate(targets, u_mat)
388def circuit_to_symbolic_gates(circ: Circuit) -> Mul:
389 """Generate a multiplication expression of sympy gates from Circuit
391 :param circ: Input circuit
392 :type circ: Circuit
393 :raises ValueError: If circ does not match a unitary operation.
394 :return: Symbolic gate multiplication expression.
395 :rtype: Mul
396 """
397 outmat = symgate.IdentityGate(0)
398 nqb = circ.n_qubits
399 qubit_map = {qb: nqb - 1 - i for i, qb in enumerate(circ.qubits)}
400 for com in circ:
401 op = com.op
402 if op.type == OpType.Barrier: 402 ↛ 403line 402 didn't jump to line 403 because the condition on line 402 was never true
403 continue
404 args = com.args
405 try:
406 targs = [qubit_map[q] for q in args] # type: ignore
407 except KeyError as e:
408 raise ValueError(
409 f"Gates can only act on qubits. Operation {com} not valid."
410 ) from e
411 gate = _op_to_sympy_gate(op, targs)
413 outmat = gate * outmat
415 for i in range(len(qubit_map)):
416 outmat = symgate.IdentityGate(i) * outmat
418 return outmat * sympy.exp(circ.phase * sympy.pi * I)
421def circuit_to_symbolic_unitary(circ: Circuit) -> ImmutableMatrix:
422 """Generate a symbolic unitary from Circuit.
424 Unitary matches pytket default ILO BasisOrder.
426 :param circ: Input circuit
427 :type circ: Circuit
428 :return: Symbolic unitary.
429 :rtype: ImmutableMatrix
430 """
431 gates = circuit_to_symbolic_gates(circ)
432 nqb = circ.n_qubits
433 try:
434 return cast(ImmutableMatrix, represent(gates, nqubits=circ.n_qubits))
435 except NotImplementedError:
436 # sympy can't represent n>1 qubit unitaries very well
437 # so if it fails we will just calculate columns using the statevectors
438 # for all possible input basis states
439 matrix_dim = 1 << nqb
440 input_states = (Qubit(f"{i:0{nqb}b}") for i in range(matrix_dim))
441 outmat = Matrix([])
442 for col, input_state in enumerate(input_states):
443 outmat = outmat.col_insert(col, represent(qapply(gates * input_state)))
445 return ImmutableMatrix(outmat)
448def circuit_apply_symbolic_qubit(circ: Circuit, input_qb: Expr) -> Qubit:
449 """Apply circuit to an input state to calculate output symbolic state.
451 :param circ: Input Circuit.
452 :type circ: Circuit
453 :param input_qb: Sympy Qubit expression corresponding to a state.
454 :type input_qb: Expr
455 :return: Output state after circuit acts on input_qb.
456 :rtype: Qubit
457 """
458 gates = circuit_to_symbolic_gates(circ)
460 return cast(Qubit, qapply(gates * input_qb))
463def circuit_apply_symbolic_statevector(
464 circ: Circuit, input_state: np.ndarray | ImmutableMatrix | None = None
465) -> ImmutableMatrix:
466 """Apply circuit to an optional input statevector
467 to calculate output symbolic statevector.
468 If no input statevector given, the all zero state is assumed.
469 Statevector follows pytket default ILO BasisOrder.
471 :param circ: Input Circuit.
472 :type circ: Circuit
473 :param input_state: Input statevector as a column vector, defaults to None.
474 :type input_state: Optional[Union[np.ndarray, ImmutableMatrix]], optional
475 :return: Symbolic state after circ acts on input_state.
476 :rtype: ImmutableMatrix
477 """
478 if input_state: 478 ↛ 479line 478 didn't jump to line 479 because the condition on line 478 was never true
479 input_qb = matrix_to_qubit(input_state)
480 else:
481 input_qb = Qubit("0" * circ.n_qubits)
482 return cast(
483 ImmutableMatrix,
484 represent(circuit_apply_symbolic_qubit(circ, cast(Qubit, input_qb))),
485 )