├── .gitignore
├── time.png
├── ham_sim.py
├── LICENSE
├── angle_sequence.py
├── README.md
├── completion.py
├── decomposition.py
└── LPoly.py
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 |
--------------------------------------------------------------------------------
/time.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alibaba-edu/angle-sequence/HEAD/time.png
--------------------------------------------------------------------------------
/ham_sim.py:
--------------------------------------------------------------------------------
1 | from scipy.special import jn
2 | import numpy
3 | import time
4 | from angle_sequence import angle_sequence
5 | from LPoly import LPoly
6 |
7 |
8 | def hamiltonian_coefficients(tau, eps):
9 | n = 2*int(numpy.e / 4 * tau - numpy.log(eps) / 2)
10 | return jn(numpy.arange(-n, n + 1, 1), tau)
11 |
12 | def ham_sim(tau, eps, suc):
13 | t = time.time()
14 | a = hamiltonian_coefficients(tau, eps / 10)
15 | return angle_sequence(a, .9 * eps, suc), time.time()-t
16 |
17 | ham_sim(100, 1e-4, 1-1e-4)
18 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2020 alibaba-edu
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/angle_sequence.py:
--------------------------------------------------------------------------------
1 | import completion
2 | import decomposition
3 | import LPoly
4 | import time
5 |
6 | def angle_sequence(p, eps=1e-4, suc=1-1e-4):
7 | """
8 | Solve for the angle sequence corresponding to the array p, with eps error budget and suc success probability.
9 | The bigger the error budget and the smaller the success probability, the better the numerical stability of the process.
10 | """
11 | p = LPoly.LPoly(p, -len(p) + 1)
12 | # Capitalization: eps/2 amount of error budget is put to the highest power for sake of numerical stability.
13 | p_new = suc * (p + LPoly.LPoly([eps / 4], p.degree) + LPoly.LPoly([eps / 4], -p.degree))
14 |
15 | # Completion phase
16 | t = time.time()
17 | g = completion.completion_from_root_finding(p_new)
18 | t_comp = time.time()
19 | print("Completion part finished within time ", t_comp - t)
20 |
21 | # Decomposition phase
22 | seq = decomposition.angseq(g)
23 | t_dec = time.time()
24 | print("Decomposition part finished within time ", t_dec - t_comp)
25 | print(seq)
26 |
27 | # Make sure that the reconstructed element lies in the desired error tolerance regime
28 | g_recon = LPoly.LAlg.unitary_from_angles(seq)
29 | final_error = (1/suc * g_recon.IPoly - p).inf_norm
30 | if final_error < eps:
31 | return seq
32 | else:
33 | raise ValueError("The angle finding program failed on given instance, with an error of {}. Please relax the error budget and/ or the success probability.".format(final_error))
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Angle Sequence: Finding Angles for Quantum Signal Processing
2 |
3 | ## Introduction
4 |
5 | [Quantum signal processing](https://journals.aps.org/prl/abstract/10.1103/PhysRevLett.118.010501) is a framework for quantum algorithms including Hamiltonian simulation, quantum linear system solving, amplitude amplification, etc.
6 |
7 | Quantum signal processing performs spectral transformation of any unitary $U$, given access to an ancilla qubit, a controlled version of $U$ and single-qubit rotations on the ancilla qubit. It first truncates an arbitrary spectral transformation function into a Laurent polynomial, then finds a way to decompose the Laurent polynomial into a sequence of products of controlled-$U$ and single qubit rotations on the ancilla. Such routines achieve optimal gate complexity for many of the quantum algorithmic tasks mentioned above.
8 |
9 | Our software package provides a lightweight solution for classically solving for the single-qubit rotation angles given the Laurent polynomial, a task called *angle sequence finding* or *angle finding*. Our package only depends on `numpy` and `scipy` and works under machine precision. Please see below for a chart giving the performance of our algorithm for the task of Hamiltonian simulation:
10 |
11 |
12 |

13 |
14 |
15 | Please see the [arXiv manuscript](https://arxiv.org/abs/2003.02831) for more details.
16 |
17 | ## Code Structure and Usage
18 |
19 |
20 | * `angle_sequence.py` is the main module of the algorithm.
21 | * `LPoly.py` defines two classes `LPoly` and `LAlg`, representing Laurent polynomials and Low algebra elements respectively.
22 | * `completion.py` describes the completion algorithm: Given a Laurent polynomial element $F(\tilde{w})$, find its counterpart $G(\tilde{w})$ such that $F(\tilde{w})+G(\tilde{w})*iX$ is a unitary element.
23 | * `decomposition.py` describes the halving algorithm: Given a unitary parity Low algebra element $V(\tilde{w})$, decompose it as a unique product of degree-0 rotations $\exp\{i\theta X\}$ and degree-1 monomials $w$.
24 | * `ham_sim.py` shows an example of how the angle sequence for Hamiltonian simulation can be found.
25 |
26 | To find the angle sequence corresponding to a real Laurent polynomial $A(\tilde{w}) = \sum_{i=-n}\^n a_i\tilde{w}\^i$, simply run:
27 |
28 | from angle_sequence import angle_sequence
29 | ang_seq = angle_sequence([a_{-n}, a_{-n+2}, ..., a_n])
30 | print(ang_seq)
31 |
32 |
33 | ## Publication
34 | * Rui Chao, Dawei Ding, András Gilyén, Cupjin Huang, and Mario Szegedy. [Finding Angles for Quantum Signal Processing with Machine Precision](https://arxiv.org/abs/2003.02831). arXiv preprint arXiv:2003.02831 (2020).
35 |
--------------------------------------------------------------------------------
/completion.py:
--------------------------------------------------------------------------------
1 | from LPoly import LPoly, LAlg, Id
2 | import numpy
3 |
4 | def root_classes(p):
5 | '''
6 | Find all the roots of Id - (p * ~p) that lies within the upper unit circle.
7 | '''
8 | poly = (Id - (p * ~p)).coefs
9 | roots = numpy.roots(poly)
10 | # poly is a real, self-inverse Laurent polynomial with no root on the unit circle. All real roots come in reciprocal pairs,
11 | # and all complex roots come in quadruples (r, r*, 1/r, 1/r*).
12 | # For each pair of real roots, select the one within the unit circle.
13 | # For each quadruple of complex roots, select the pair within the unit circle.
14 | imag_roots = []
15 | real_roots = []
16 | for i in roots:
17 | if (numpy.abs(i) < 1) and (numpy.imag(i) > -1e-8):
18 | if numpy.imag(i) == 0.:
19 | real_roots.append(numpy.real(i))
20 | else:
21 | imag_roots.append(i)
22 | norm = poly[-1]
23 | return real_roots, imag_roots, norm
24 |
25 | def poly_from_roots(real_roots, imag_roots, norm, seed=None):
26 | '''
27 | Construct the counter part with the roots and the norm.
28 | '''
29 | # Randomly choose whether to pick the real root (the pair of complex roots) inside or outside the unit circle.
30 | # This is to reduce the range of the coefficients appearing in the final product.
31 | degree = len(real_roots) + 2 * len(imag_roots)
32 | lst = []
33 | if seed is None:
34 | seed = numpy.random.randint(2, size=len(imag_roots) + len(real_roots))
35 | for i, root in enumerate(imag_roots):
36 | if seed[i]:
37 | root = 1 / root
38 | lst.append(LPoly([numpy.abs(root) ** 2, -2 * numpy.real(root), 1]))
39 | for i, root in enumerate(real_roots):
40 | if seed[i + len(imag_roots)]:
41 | root = 1 / root
42 | lst.append(LPoly([-root, 1]))
43 |
44 | # Multiply all the polynomial factors via fft for numerical stability.
45 | pp = int(numpy.floor(numpy.log2(degree)))+1
46 | lst_fft = numpy.pi * numpy.linspace(0, 1 - 1/2**pp, 2**pp)
47 | coef_mat = numpy.log(numpy.array([i.eval(lst_fft) for i in lst]))
48 | coef_fft = numpy.exp(numpy.sum(coef_mat, axis=0))
49 | coefs = numpy.real(numpy.fft.fft(coef_fft, 1 << pp))[:degree+1] / (1 << pp)
50 |
51 |
52 | # Normalization
53 | xpoly = LPoly(coefs * numpy.sqrt(norm / coefs[0]), -len(coefs) + 1)
54 | return xpoly
55 |
56 | def completion_from_root_finding(p, seed=None):
57 | """
58 | Find a Low Algebra element g such that the identity components are given by the input p.
59 | """
60 | real_roots, imag_roots, norm = root_classes(p)
61 | xpoly = poly_from_roots(real_roots, imag_roots, norm)
62 | return LAlg(p, xpoly)
63 |
--------------------------------------------------------------------------------
/decomposition.py:
--------------------------------------------------------------------------------
1 | from LPoly import LPoly, LAlg, w
2 | import numpy
3 | from scipy.linalg import toeplitz
4 |
5 | def linear_system(g, ldeg):
6 | """
7 | Let vec(l) = l.IPoly.coefs + l.XPoly.coefs, i.e. to regard a Low Algebra element l of degree ldeg as a real vector in R^{2n+2}.
8 | M is the matrix representation of g in the sense that M * vec(l) = vec(l * g) for all l with degree ldeg.
9 | The linear system m, s consist of two parts; the first part ensures that the degree of g * l does not exceed deg - ldeg;
10 | the second part ensures that l(Id) = Id, fixing the SU(2) freedom left from the previous set of constraints.
11 | """
12 | deg = g.degree
13 | aligned_icoefs = g.IPoly.aligned(-deg, deg)
14 | aligned_xcoefs = g.XPoly.aligned(-deg, deg)
15 | def vec_to_mat(vec):
16 | return toeplitz(numpy.hstack((vec, [0] * ldeg)), [0] * (ldeg + 1))
17 | M = numpy.vstack((numpy.hstack((vec_to_mat(aligned_icoefs),
18 | vec_to_mat(-aligned_xcoefs[::-1]))),
19 | numpy.hstack((vec_to_mat(aligned_xcoefs),
20 | vec_to_mat(aligned_icoefs[::-1])))))
21 |
22 | a = numpy.ones(ldeg + 1, dtype=float)
23 | b = numpy.zeros(ldeg + 1, dtype=float)
24 | m = numpy.vstack((numpy.hstack((a, b)), numpy.hstack((b, a)), M[:ldeg, :], M[deg+1: deg+2*ldeg+1, :], M[-ldeg:, :]))
25 | s = numpy.zeros(m.shape[0], dtype=float)
26 | s[0] = 1
27 | return m, s
28 |
29 | def decompose(g, ldeg):
30 | """
31 | Let
32 | g = exp(iθ_0 * X) * w * exp(iθ_1 * X) * ... w * exp(iθ_deg * X)
33 | One wants to solve for
34 | l = exp(iθ_0 * X) * w * exp(iθ_1 * X) * ... w * exp(-i(Σ_{i=1}^{deg-ldeg-1}θ_j) * X),
35 | and
36 | r = ~l * g = (exp(iΣ_{j=1}^{deg-ldeg}θ_j * X) * w * exp(iθ_{deg-ldeg+1} * X) * ... w * exp(iθ_deg * X).
37 |
38 | The linear system (m, s) is such that
39 |
40 | deg(l * g) <= deg - ldeg
41 | l(Id) = Id
42 |
43 | One can show that such a system has a unique solution.
44 | """
45 | deg = g.degree
46 | m, s = linear_system(g, ldeg)
47 | lstsq = numpy.linalg.lstsq(m, s, rcond=-1)
48 | v = lstsq[0]
49 |
50 | li = v[:ldeg+1]
51 | lx = v[-ldeg-1:]
52 | l = LAlg(LPoly(li, -ldeg), LPoly(lx, -ldeg))
53 | r = LAlg.truncate(l * g, -g.degree + ldeg, g.degree - ldeg)
54 |
55 | return ~l, r
56 |
57 | def angseq(g):
58 | '''
59 | Divide-and-conquer approach to solve for the whole angle sequence.
60 | '''
61 | deg = g.degree
62 | if deg == 1:
63 | return g.left_and_right_angles
64 | else:
65 | l, r = decompose(g, deg//2)
66 | a = angseq(l)
67 | b = angseq(r)
68 | return a[:-1] + [a[-1] + b[0]] + b[1:]
69 |
--------------------------------------------------------------------------------
/LPoly.py:
--------------------------------------------------------------------------------
1 | import numpy
2 | PRES = 5000
3 |
4 | class LPoly():
5 | '''
6 | Laurent polynomials with parity constraint.
7 | '''
8 |
9 | def __init__(self, coefs, dmin=0):
10 | self.coefs = numpy.array(coefs)
11 | if len(self.coefs) == 0:
12 | self.dmin = dmin
13 | self.iszero = True
14 | self.coefs = [0]
15 | else:
16 | assert(len(self.coefs.shape) == 1), self.coefs
17 | self.dmin = dmin
18 | self.iszero = False
19 |
20 | @property
21 | def dmax(self):
22 | return 2 * len(self.coefs) + self.dmin - 2
23 |
24 | @property
25 | def degree(self):
26 | """
27 | The degree of a Laurent polynomial is defined to be the maximum absolute value of the powers over all nonzero terms.
28 | """
29 | return max(-self.dmin, self.dmax)
30 |
31 | @property
32 | def norm(self):
33 | """
34 | The norm of a Laurent polynomial is defined to be the l2 norm of the coefficients regarded as a vector.
35 | """
36 | return numpy.linalg.norm(self.coefs)
37 |
38 | @property
39 | def inf_norm(self):
40 | """
41 | The infinity norm of a Laurent polynomial is defined to be the maximum modulus over the unit circle.
42 | """
43 | i, x = self.curve
44 | return numpy.amax(
45 | numpy.absolute(i + 1j * x))
46 |
47 | @property
48 | def parity(self):
49 | '''
50 | Parity of the polynomial. 0 for even parity Laurent polynomials and 1 for odd parity Laurent polynomials.
51 | '''
52 | return self.dmin % 2
53 |
54 | @property
55 | def curve(self):
56 | values = numpy.exp(numpy.outer(numpy.linspace(-self.parity * 1j * numpy.pi, 1j*numpy.pi, PRES), range(self.dmin, self.dmax+1, 2))).dot(self.coefs)
57 | return numpy.real(values), numpy.imag(values)
58 |
59 | def __getitem__(self, key):
60 | if (key - self.dmin) % 2:
61 | return 0
62 | pos = (key - self.dmin) // 2
63 | if (pos < len(self.coefs)) and (pos >= 0):
64 | return self.coefs[pos]
65 | else:
66 | return 0
67 |
68 | def __mul__(self, other):
69 | if isinstance(other, LAlg):
70 | return LAlg(self * other.IPoly, self * other.XPoly)
71 | if not isinstance(other, LPoly):
72 | return LPoly(other * self.coefs, self.dmin)
73 | if self.iszero or other.iszero:
74 | return LPoly([])
75 | dmin = self.dmin + other.dmin
76 | coefs = numpy.convolve(self.coefs, other.coefs)
77 | return LPoly(coefs, dmin)
78 |
79 | def __rmul__(self, other):
80 | if isinstance(other, LAlg):
81 | return LAlg(self * other.IPoly, ~self * other.XPoly)
82 | elif not isinstance(other, LPoly):
83 | return LPoly(other * self.coefs, self.dmin)
84 |
85 | def __add__(self, other):
86 | '''
87 | Only Laurent polynomials of the same parity can be added together in order to preserve parity.
88 | '''
89 | if self.iszero:
90 | return LPoly(other.coefs, other.dmin)
91 | if other.iszero:
92 | return LPoly(self.coefs, self.dmin)
93 | assert (self.parity == other.parity), "not of the same parity"
94 | dmin = min(self.dmin, other.dmin)
95 | dmax = max(self.dmax, other.dmax)
96 | coefs = self.aligned(dmin, dmax) + other.aligned(dmin, dmax)
97 | return LPoly(coefs, dmin)
98 |
99 | def __neg__(self):
100 | return LPoly(-1 * self.coefs, self.dmin)
101 |
102 | def __invert__(self):
103 | '''
104 | Conjugation of a Laurent polynomial maps f(w) to f(w^-1).
105 | '''
106 | dmin = -self.dmax
107 | coefs = self.coefs[::-1]
108 | return LPoly(coefs, dmin)
109 |
110 | def __sub__(self, other):
111 | return self + (-other)
112 |
113 | def __str__(self):
114 | return " + ".join(["{} * w ^ ({})".format(self.coefs[i],
115 | self.dmin + 2 * i)
116 | for i in range(len(self.coefs))])
117 |
118 | def aligned(self, dmin, dmax):
119 | if(self.iszero):
120 | return numpy.zeros((dmax-dmin)//2 + 1)
121 | else:
122 | assert (dmin <= self.dmin) and (dmax >= self.dmax), "interval not valid"
123 | return numpy.hstack((numpy.zeros((self.dmin - dmin)//2),
124 | numpy.array(self.coefs),
125 | numpy.zeros((dmax - self.dmax)//2)))
126 |
127 | def eval(self, angles):
128 | '''
129 | Evalute the Laurent polynomial f(w) at w = exp(i * angle) for angle iterating over angles. Returns a complex array .
130 | '''
131 | if self.iszero:
132 | return 1
133 | res = self.coefs.dot(numpy.exp(1j*numpy.outer(numpy.arange(self.dmin, self.dmax + 1, 2), angles)))
134 | return res
135 |
136 | @classmethod
137 | def truncate(cls, p, dmin, dmax):
138 | lb = min(dmin, p.dmin)
139 | ub = max(dmax, p.dmax)
140 | return LPoly(p.aligned(lb, ub+2)[(dmin-lb) // 2:(dmax-ub)//2-1], dmin)
141 |
142 | @classmethod
143 | def isconsistent(cls, a, b):
144 | if a.iszero:
145 | return True
146 | if b.iszero:
147 | return True
148 | return a.parity == b.parity
149 |
150 | class LAlg():
151 | '''
152 | Low algebra elements with parity constraints.
153 | A Low algebra element g is a matrix-valued Laurent polynomial with the given form
154 | g(w) = IPoly(w) + XPoly(w) * iX,
155 | where IPoly and XPoly are real Laurent polynomials sharing the same parity,
156 | and the elements w and X satisfy the relations (iX)^2 = Id and XwXw = Id.
157 | '''
158 |
159 | def __init__(self, IPoly=LPoly([], 0), XPoly=LPoly([], 0)):
160 | self.IPoly = IPoly
161 | self.XPoly = XPoly
162 | assert LPoly.isconsistent(self.IPoly, self.XPoly),\
163 | "The algebra element does not have a consistent parity"
164 |
165 | @property
166 | def degree(self):
167 | return max(self.IPoly.degree, self.XPoly.degree)
168 |
169 | @property
170 | def norm(self):
171 | return numpy.sqrt(self.IPoly.norm**2 + self.XPoly.norm**2)
172 |
173 | @property
174 | def parity(self):
175 | return self.IPoly.parity
176 |
177 | def __str__(self):
178 | res = []
179 | if not self.IPoly.iszero:
180 | res.append(str(self.IPoly))
181 | if not self.XPoly.iszero:
182 | res.append("( " + str(self.XPoly) + " ) * iX")
183 | return " + ".join(res)
184 |
185 | def __add__(self, other):
186 | if isinstance(other, LPoly):
187 | return LAlg(self.IPoly + other, self.XPoly)
188 | return LAlg(self.IPoly + other.IPoly, self.XPoly + other.XPoly)
189 |
190 | def __neg__(self):
191 | return LAlg(-self.IPoly, -self.XPoly)
192 |
193 | def __sub__(self, other):
194 | return self + (-other)
195 |
196 | def __invert__(self):
197 | '''
198 | Conjugation of g(w) = A(w) + B(w) * iX is ~g(w) = A(w^-1) - B(w) * iX.
199 | '''
200 | return LAlg(~self.IPoly, -self.XPoly)
201 |
202 | def __mul__(self, other):
203 | if isinstance(other, LPoly):
204 | return LAlg(self.IPoly * other, self.XPoly * ~other)
205 | if not isinstance(other, LAlg):
206 | return LAlg(self.IPoly * other, self.XPoly * other)
207 | return LAlg(self.IPoly * other.IPoly - self.XPoly * (~other.XPoly),
208 | self.IPoly * other.XPoly + self.XPoly * (~other.IPoly))
209 |
210 | @property
211 | def pnorm(self):
212 | return (self * (~self)).IPoly
213 |
214 | @property
215 | def unitarity(self):
216 | '''
217 | Unitarity of an element A is defined to be ||Id-a*~a||_2^2.
218 | '''
219 | return (LPoly([1]) - self.pnorm).norm
220 |
221 | @property
222 | def angle(self):
223 | '''
224 | For degree 0 element M, the corresponding angle is defined such that M \propto exp(angle * iX).
225 | '''
226 | assert (self.degree == 0), "deg = {}".format(self.degree)
227 | return numpy.angle(self.IPoly[0]+self.XPoly[0]*1j)
228 |
229 | @property
230 | def left_and_right_angles(self):
231 | '''
232 | For a degree 1 element g(w) = exp(a * iX) * w * exp(b * iX), return the left and right rotation angles [a, b].
233 | Note that g(Id) = exp((a + b) * iX) and g(iZ) = exp((a-b) * iX) * iZ.
234 | '''
235 | assert self.degree == 1
236 | summation = numpy.angle(self.IPoly.eval(0)[0] + 1j * self.XPoly.eval(0)[0])
237 | difference = numpy.angle(self.IPoly.eval(numpy.pi / 2)[0] - 1j * self.XPoly.eval(numpy.pi / 2)[0]) - numpy.pi / 2
238 | res = [(summation + difference) / 2, (summation - difference) / 2]
239 | return res
240 |
241 | @classmethod
242 | def rotation(cls, ang):
243 | '''
244 | Degree 0 rotation with a certain angle. Inverse of `LAlg.angle`.
245 | '''
246 | return LAlg(LPoly([numpy.cos(ang)]), LPoly([numpy.sin(ang)]))
247 |
248 | @property
249 | def curve(self):
250 | i, z = self.IPoly.curve
251 | y, x = self.XPoly.curve
252 | return numpy.angle(1j * i - y) * numpy.sqrt(1 - x**2 - z**2), z, x
253 |
254 | @classmethod
255 | def truncate(cls, g, dmin, dmax):
256 | return LAlg(LPoly.truncate(g.IPoly, dmin, dmax),
257 | LPoly.truncate(g.XPoly, dmin, dmax))
258 |
259 | @classmethod
260 | def generator(cls, ang):
261 | '''
262 | Generator elements of the unitary group; each one is w conjugated by some rotation.
263 | '''
264 | return cls.rotation(ang) * w * cls.rotation(-ang)
265 |
266 | @classmethod
267 | def unitary_from_conjugations(cls, ang):
268 | '''
269 | Generating a special unitary element from the conjugation angles.
270 | '''
271 | res = Id
272 | for i in ang:
273 | res *= cls.generator(i)
274 | return res
275 |
276 | @classmethod
277 | def unitary_from_angles(cls, ang):
278 | '''
279 | Generating a unitary element from the rotation angles sandwiching w.
280 | '''
281 | res = cls.rotation(ang[0])
282 | for i in ang[1:]:
283 | res = res * w * cls.rotation(i)
284 | return res
285 |
286 |
287 |
288 | # Definition of elements
289 |
290 | Id = LPoly([1])
291 | w = LPoly([1], 1)
292 | iX = LAlg(XPoly=LPoly([1]))
293 |
--------------------------------------------------------------------------------