From 2f925ffda1c164352d66ac5023a66ece52cb61a2 Mon Sep 17 00:00:00 2001 From: meuns Date: Sun, 6 Mar 2016 21:48:54 +0100 Subject: [PATCH 01/78] remove and ignore pyc files --- galgebra/.gitignore | 1 + galgebra/ga.pyc | Bin 56118 -> 0 bytes galgebra/lt.pyc | Bin 27352 -> 0 bytes galgebra/metric.pyc | Bin 19980 -> 0 bytes galgebra/mv.pyc | Bin 80905 -> 0 bytes galgebra/printer.pyc | Bin 43206 -> 0 bytes 6 files changed, 1 insertion(+) create mode 100644 galgebra/.gitignore delete mode 100644 galgebra/ga.pyc delete mode 100644 galgebra/lt.pyc delete mode 100644 galgebra/metric.pyc delete mode 100644 galgebra/mv.pyc delete mode 100644 galgebra/printer.pyc diff --git a/galgebra/.gitignore b/galgebra/.gitignore new file mode 100644 index 00000000..0d20b648 --- /dev/null +++ b/galgebra/.gitignore @@ -0,0 +1 @@ +*.pyc diff --git a/galgebra/ga.pyc b/galgebra/ga.pyc deleted file mode 100644 index f17da942f4e9952e9bb33823738a70b1c98fae28..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 56118 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 KLD%7l6$pbqG8e2Msx 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 BR;>U4 diff --git a/galgebra/printer.pyc b/galgebra/printer.pyc deleted file mode 100644 index 7f5cccd6af5942f86cda9e421befec60a152b233..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 43206 zcmeIbdyrgLe&2VxXE4AF2JgoJBuHKYBrza|011-7E(w4S3}3q>2=#CmyTA;48{IvF z277vD`u2c>9ZV!GDebPTO%z48Dw{}_;ws0HR?%9qBH6OzxD;7l%ht<|vyoMm%DYmM z%T6UODOI+W&-Z)o?d}<3wF
>tKp&fI&Rzw`S2e&_c%ci{ipHuA}zocVM$WdFAC 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 From c67ac349bd1d1f8cb1bd2e6f2cb4168769bf8179 Mon Sep 17 00:00:00 2001 From: meuns Date: Sun, 6 Mar 2016 21:49:53 +0100 Subject: [PATCH 02/78] leo dorst book chapter 2 drills --- tests/test_chapter2.py | 79 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 79 insertions(+) create mode 100755 tests/test_chapter2.py diff --git a/tests/test_chapter2.py b/tests/test_chapter2.py new file mode 100755 index 00000000..a881f3a1 --- /dev/null +++ b/tests/test_chapter2.py @@ -0,0 +1,79 @@ +import sys +sys.path.append('../galgebra') + +import unittest + +from sympy import Symbol, solve +from ga import Ga + +class TestChapter2(unittest.TestCase): + + def test_2_12_1_1(self): + """ + Compute the outer products of the following 3-space expressions, + giving the result relative to the basis {1, e_1, e_2, e_3, e_1^e_2, e_1^e_3, e_2^e_3, e_1^e_2^e_3}. + """ + (_g3d, e_1, e_2, e_3) = Ga.build('e*1|2|3') + self.assertTrue((e_1 + e_2) ^ (e_1 + e_3) == (-e_1 ^ e_2) + (e_1 ^ e_3) + (e_2 ^ e_3)) + self.assertTrue((e_1 + e_2 + e_3) ^ (2*e_1) == -2*(e_1 ^ e_2) - 2*(e_1 ^ e_3)) + self.assertTrue((e_1 - e_2) ^ (e_1 - e_3) == (e_1 ^ e_2) - (e_1 ^ e_3) + (e_2 ^ e_3)) + self.assertTrue((e_1 + e_2) ^ (0.5*e_1 + 2*e_2 + 3*e_3) == 1.5*(e_1 ^ e_2) + 3*(e_1 ^ e_3) + 3*(e_2 ^ e_3)) + self.assertTrue((e_1 ^ e_2) ^ (e_1 + e_3) == (e_1 ^ e_2 ^ e_3)) + self.assertTrue((e_1 + e_2) ^ ((e_1 ^ e_2) + (e_2 ^ e_3)) == (e_1 ^ e_2 ^ e_3)) + + + def test2_12_1_2(self): + """ + Given the 2-blade B = e_1 ^ (e_2 - e_3) that represents a plane, + determine if each of the following vectors lies in that plane. + """ + (_g3d, e_1, e_2, e_3) = Ga.build('e*1|2|3') + B = e_1 ^ (e_2 - e_3) + self.assertTrue(e_1 ^ B == 0) + self.assertFalse((e_1 + e_2) ^ B == 0) + self.assertFalse((e_1 + e_2 + e_3) ^ B == 0) + self.assertTrue((2*e_1 - e_2 + e_3) ^ B == 0) + + + def test2_12_1_3(self): + """ + What is the area of the parallelogram spanned by the vectors a = e_1 + 2*e_2 + and b = -e_1 - e_2 (relative to the area of e_1 ^ e_2) ? + """ + (_g3d, e_1, e_2, _e_3) = Ga.build('e*1|2|3') + a = e_1 + 2*e_2 + b = -e_1 - e_2 + B = a ^ b + self.assertTrue(B == 1 * (e_1 ^ e_2)) + + + def test2_12_1_4(self): + """ + Compute the intersection of the non-homogeneous line L with position vector e_1 + and direction vector e_2, and the line M with position vector e_2 and direction vector + (e_1 + e_2), using 2-blades. + """ + (_g2d, e_1, e_2) = Ga.build('e*1|2') + + # x is defined in the basis {e_1, e_2} + a = Symbol('a') + b = Symbol('b') + x = a * e_1 + b * e_2 + + # x lies on L and M (eq. L == 0 and M == 0) + L = (x ^ e_2) - (e_1 ^ e_2) + M = (x ^ (e_1 + e_2)) - (e_2 ^ (e_1 + e_2)) + + # Solve the linear system + R = solve([L, M], a, b) + + # Replace symbols + x = x.subs(R) + + self.assertTrue(x == e_1 + 2*e_2) + + +if __name__ == '__main__': + + unittest.main() + From 5fded260973d7339cf044487925a2c1c8945f63e Mon Sep 17 00:00:00 2001 From: Sylvain Meunier Date: Thu, 10 Mar 2016 13:44:29 +0100 Subject: [PATCH 03/78] ignore eclipe project files --- .gitignore | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 .gitignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..97356bc7 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +.project +.pydevproject From 216731c7a885d7f9113af73cab3eadaff8c2f100 Mon Sep 17 00:00:00 2001 From: Sylvain Meunier Date: Thu, 10 Mar 2016 13:52:02 +0100 Subject: [PATCH 04/78] leo dorst book chapter 2 exercices (in progress) --- tests/test_chapter2.py | 39 ++++++++++++++++++++++++++++++++++++++- 1 file changed, 38 insertions(+), 1 deletion(-) diff --git a/tests/test_chapter2.py b/tests/test_chapter2.py index a881f3a1..cfaba641 100755 --- a/tests/test_chapter2.py +++ b/tests/test_chapter2.py @@ -3,7 +3,7 @@ import unittest -from sympy import Symbol, solve +from sympy import Symbol, Matrix, solve, cos, sin from ga import Ga class TestChapter2(unittest.TestCase): @@ -73,6 +73,43 @@ def test2_12_1_4(self): self.assertTrue(x == e_1 + 2*e_2) + def test2_12_2_1(self): + """ + In R2 with Euclidean metric, choose an orthonormal basis {e_1, e_2} in the plane of a and b such that e1 is parallel to a. + Write x = a * e_1 and y = b * (cos(t) * e_1 + sin(t) * e_2), whete t is the angle from a to b. + Evaluate the outer product. What is the geometrical interpretation ? + """ + (_g2d, e_1, e_2) = Ga.build('e*1|2', g='1 0, 0 1') + + # TODO: use alpha, beta and theta instead of a, b and t (it crashes sympy) + a = Symbol('a') + b = Symbol('b') + t = Symbol('t') + x = a * e_1 + y = b * (cos(t) * e_1 + sin(t) * e_2) + B = x ^ y + self.assertTrue(B == (a * b * sin(t) * (e_1 ^ e_2))) + + # Retrieve the parallelogram area from the 2-vector + area = B.norm() + self.assertTrue(area == (a * b * sin(t))) + + # Compute the parallelogram area using the determinant + x = [a, 0] + y = [b * cos(t), b * sin(t)] + area = Matrix([x, y]).det() + self.assertTrue(area == (a * b * sin(t))) + + + def test2_12_2_2(self): + """ + Consider R4 with basis {e_1, e_2, e_3, e_4}. Show that the 2-vector B = (e_1 ^ e_2) + (e_3 ^ e_4) + is not a 2-blade (i.e., it cannot be written as the outer product of two vectors). + """ + + (g4d, e_1, e_2, e_3, e_4) = Ga.build('e*1|2|3|4') + + if __name__ == '__main__': unittest.main() From cf9c50fe150ae08d53a916c03d9f80aabb08bfc1 Mon Sep 17 00:00:00 2001 From: Sylvain Meunier Date: Fri, 11 Mar 2016 14:06:21 +0100 Subject: [PATCH 05/78] fix and test Mv.blade_coefs method --- galgebra/mv.py | 10 +++---- tests/test_mv.py | 71 ++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 76 insertions(+), 5 deletions(-) create mode 100644 tests/test_mv.py diff --git a/galgebra/mv.py b/galgebra/mv.py index 1b644216..02e5e133 100755 --- a/galgebra/mv.py +++ b/galgebra/mv.py @@ -921,11 +921,11 @@ def is_blade(self): # True is self is blade, otherwise False return self.blade_flg def is_base(self): - (coefs,bases) = linear_expand(self.obj) + (coefs, _bases) = metric.linear_expand(self.obj) if len(coefs) > 1: return False else: - return True + return coefs[0] == ONE def is_versor(self): # Test for versor (geometric product of vectors) """ @@ -986,6 +986,9 @@ def blade_coefs(self, blade_lst): a list (sympy expressions) of the coefficients of each basis blade in blade_lst """ + for blade in blade_lst: + if not blade.is_base() or not blade.is_blade(): + raise ValueError("%s expression isn't a basis blade" % blade) blade_lst = [x.obj for x in blade_lst] (coefs, bases) = metric.linear_expand(self.obj) coef_lst = [] @@ -994,9 +997,6 @@ def blade_coefs(self, blade_lst): coef_lst.append(coefs[bases.index(blade)]) else: coef_lst.append(ZERO) - for (coef, base) in zip(coefs, bases): - if base in bases_lst: - coef_lst.append(coef) return coef_lst def proj(self, bases_lst): diff --git a/tests/test_mv.py b/tests/test_mv.py new file mode 100644 index 00000000..ff589a1d --- /dev/null +++ b/tests/test_mv.py @@ -0,0 +1,71 @@ +import sys +sys.path.append('../galgebra') + +import unittest + +from sympy import Symbol +from ga import Ga + +class TestMv(unittest.TestCase): + + def test_is_base(self): + """ + Various tests on several multivectors. + """ + (_g3d, e_1, e_2, e_3) = Ga.build('e*1|2|3') + + self.assertTrue((e_1).is_base()) + self.assertTrue((e_2).is_base()) + self.assertTrue((e_3).is_base()) + self.assertTrue((e_1 ^ e_2).is_base()) + self.assertTrue((e_2 ^ e_3).is_base()) + self.assertTrue((e_1 ^ e_3).is_base()) + self.assertTrue((e_1 ^ e_2 ^ e_3).is_base()) + + self.assertFalse((2*e_1).is_base()) + self.assertFalse((e_1 + e_2).is_base()) + self.assertFalse((e_3 * 4).is_base()) + self.assertFalse(((3 * e_1) ^ e_2).is_base()) + self.assertFalse((2 * (e_2 ^ e_3)).is_base()) + self.assertFalse((e_3 ^ e_1).is_base()) + self.assertFalse((e_2 ^ e_1 ^ e_3).is_base()) + + + def test_blade_coefs(self): + """ + Various tests on several multivectors. + """ + (_g3d, e_1, e_2, e_3) = Ga.build('e*1|2|3') + + m0 = 2 * e_1 + e_2 - e_3 + 3 * (e_1 ^ e_3) + (e_1 ^ e_3) + (e_2 ^ (3 * e_3)) + self.assertTrue(m0.blade_coefs([e_1]) == [2]) + self.assertTrue(m0.blade_coefs([e_2]) == [1]) + self.assertTrue(m0.blade_coefs([e_1, e_2]) == [2, 1]) + self.assertTrue(m0.blade_coefs([e_1 ^ e_3]) == [4]) + self.assertTrue(m0.blade_coefs([e_1 ^ e_3, e_2 ^ e_3]) == [4, 3]) + self.assertTrue(m0.blade_coefs([e_2 ^ e_3, e_1 ^ e_3]) == [3, 4]) + self.assertTrue(m0.blade_coefs([e_1, e_2 ^ e_3]) == [2, 3]) + + a = Symbol('a') + b = Symbol('b') + m1 = a * e_1 + e_2 - e_3 + b * (e_1 ^ e_2) + self.assertTrue(m1.blade_coefs([e_1]) == [a]) + self.assertTrue(m1.blade_coefs([e_2]) == [1]) + self.assertTrue(m1.blade_coefs([e_3]) == [-1]) + self.assertTrue(m1.blade_coefs([e_1 ^ e_2]) == [b]) + self.assertTrue(m1.blade_coefs([e_2 ^ e_3]) == [0]) + self.assertTrue(m1.blade_coefs([e_1 ^ e_3]) == [0]) + self.assertTrue(m1.blade_coefs([e_1 ^ e_2 ^ e_3]) == [0]) + + # Invalid parameters + self.assertRaises(ValueError, lambda: m1.blade_coefs([e_1 + e_2])) + self.assertRaises(ValueError, lambda: m1.blade_coefs([e_2 ^ e_1])) + self.assertRaises(ValueError, lambda: m1.blade_coefs([e_1, e_2 ^ e_1])) + self.assertRaises(ValueError, lambda: m1.blade_coefs([a * e_1])) + self.assertRaises(ValueError, lambda: m1.blade_coefs([3 * e_3])) + + +if __name__ == '__main__': + + unittest.main() + From 50509fff1ec64bb53296657d4b2c08625be914f2 Mon Sep 17 00:00:00 2001 From: Sylvain Meunier Date: Thu, 17 Mar 2016 13:33:04 +0100 Subject: [PATCH 06/78] leo dorst book chapter 2 exercices (in progress) --- tests/test_chapter2.py | 43 ++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 41 insertions(+), 2 deletions(-) diff --git a/tests/test_chapter2.py b/tests/test_chapter2.py index cfaba641..9ee8e745 100755 --- a/tests/test_chapter2.py +++ b/tests/test_chapter2.py @@ -3,7 +3,7 @@ import unittest -from sympy import Symbol, Matrix, solve, cos, sin +from sympy import Symbol, Matrix, solve, solve_poly_system, cos, sin from ga import Ga class TestChapter2(unittest.TestCase): @@ -107,7 +107,46 @@ def test2_12_2_2(self): is not a 2-blade (i.e., it cannot be written as the outer product of two vectors). """ - (g4d, e_1, e_2, e_3, e_4) = Ga.build('e*1|2|3|4') + (_g4d, e_1, e_2, e_3, e_4) = Ga.build('e*1|2|3|4') + + # B + B = (e_1 ^ e_2) + (e_3 ^ e_4) + + # C is the product of a and b vectors + a_1 = Symbol('a_1') + a_2 = Symbol('a_2') + a_3 = Symbol('a_3') + a_4 = Symbol('a_4') + a = a_1 * e_1 + a_2 * e_2 + a_3 * e_3 + a_4 * e_4 + + b_1 = Symbol('b_1') + b_2 = Symbol('b_2') + b_3 = Symbol('b_3') + b_4 = Symbol('b_4') + b = b_1 * e_1 + b_2 * e_2 + b_3 * e_3 + b_4 * e_4 + + C = a ^ b + + # other coefficients are null + blades = [ + e_1 ^ e_2, e_1 ^ e_3, e_1 ^ e_4, e_2 ^ e_3, e_2 ^ e_4, e_3 ^ e_4, + ] + + C_coefs = C.blade_coefs(blades) + B_coefs = B.blade_coefs(blades) + + # try to solve the system and show there is no solution + system = [ + (C_coef) - (B_coef) for C_coef, B_coef in zip(C_coefs, B_coefs) + ] + + unknowns = [ + a_1, a_2, a_3, a_4, b_1, b_2, b_3, b_4 + ] + + # TODO: use solve if sympy fix it + result = solve_poly_system(system, unknowns) + self.assertTrue(result is None) if __name__ == '__main__': From dc779f214d2507e1f91f6bda9ce1ee0109daacab Mon Sep 17 00:00:00 2001 From: meuns Date: Sun, 6 Mar 2016 21:48:54 +0100 Subject: [PATCH 07/78] remove and ignore pyc files --- galgebra/.gitignore | 1 + galgebra/ga.pyc | Bin 56118 -> 0 bytes galgebra/lt.pyc | Bin 27352 -> 0 bytes galgebra/metric.pyc | Bin 19980 -> 0 bytes galgebra/mv.pyc | Bin 81380 -> 0 bytes galgebra/printer.pyc | Bin 43206 -> 0 bytes 6 files changed, 1 insertion(+) create mode 100644 galgebra/.gitignore delete mode 100644 galgebra/ga.pyc delete mode 100644 galgebra/lt.pyc delete mode 100644 galgebra/metric.pyc delete mode 100644 galgebra/mv.pyc delete mode 100644 galgebra/printer.pyc diff --git a/galgebra/.gitignore b/galgebra/.gitignore new file mode 100644 index 00000000..0d20b648 --- /dev/null +++ b/galgebra/.gitignore @@ -0,0 +1 @@ +*.pyc diff --git a/galgebra/ga.pyc b/galgebra/ga.pyc deleted file mode 100644 index f17da942f4e9952e9bb33823738a70b1c98fae28..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 56118 zcmch=37A~hb>I1_y3s&48vD+*2rk_XG(ZBBNC^Tdf&dA@CPBJD88k?+Sm-K%1$0+; zS5*TIk%(ncvMg)y?k`@V$fMCnUSwzNII%NvY)4jXkH?>rc%0bzyeRhAU#!VYCh<2j zndP(R_y3>wUcGKKM2cXBMBTpczPsOZ&OP^>bI%+2t92v4^Sh6I^Hh@lH^AS=_;o*8 zN|FZuO38GRoXO**X~$;_7I%$8(kYjS35GP5l?vn`p~o}Ag9l+?30Qap zEyJ0N<;Rl7x}lE#LlIpNfedFq~|xi@KSN-mX?&g-SN=l|6BW)`INH~e>Ud@Ir3AVH&b z{(Nr@k!roynr+vodqX^&x;k@icDl#fC8v5?%cmEn{rB0~EBaP2czMb#% zqTO>{zicneoNIQvp8Ndtlv`vbM?!_$5<%)W}xx zJIb&7Ji%*8a?#6ht(3gRs+5vl*UAa=@LE|**C=a7=S~+=N=lhMtqefYu8T_^2GSCz z#x>O^mvzC3o|$^*Lbo>6p7Ic6&vj;5!uxwObN64UPhV)B>(uwn)VsZACx0fgf9`5;BV}moT)o@u*5*31 zjfJV+HqyERgrqcD8YwwXeW5p7TbOIqd(B#dX`a$+{ij|$&aZnnK@+%Uq6j@9%%H#* zv?v>i$aQ^eat2x~4*=R<|-$2Q(WMK+^dyHLq3{x+L(bJ=>$2W@YxgUo{t7-CnDGp+cUn zYkE!%EU+N7S?M;X&+k8<4t<_x7g?g*Ke`d)3ttY_Pd^`0Txp~t?p5N-8%`u2~ea)r0hsZ$fOh&8&YA+I~kxVnvzYL zfxOl3_Ui4aW=|Mr2#SnC4NY-X4OA-xHwn;cIh%reiKl=(TpBJ-X*3#Dj`d&W;p*0; z2jY8dj1by=>U8@jl0oq6(n!+zm84WEUAL@06k`6OogJXj4(QAd+Wb%{A@}p` zQu4g#Uh{aeJ3_cSoA>eV1|BDN8{iww^Q|`E*A?B?(gQl zOs(c+`^M(gCpw+kj#tK6Pl7sLtTcZ&&#%V68Dwa>M9}$O$ID#Zt(G+PG;0tzUsDH2 zcd+YpO z^4e%24zvy@udPkCK1}WNpDASgKa$t-mlVk}TF8AjB?d3wJtd5wPq$)(jv z=kbz|Yn_Hc3X#KbI)(C1+m$tPdj7-7Y2KbPJa#LP0#mxDz>0dO65NaabPjmaQ0$+pBw#@+X>#yuFxf&10pm+NNVcnWl{SQ}!+!cD=J~mNE|D z2D4yJ(OlcOHWky17MyBqWYsN76Jx>gHVs}88Vf)BLj z##PvK(Xf=q4w~ee7f}~x7N)&*y5fK~M`JSjsR#S1QG;rAr7Cp5F_np!(hlZn2djH& zsZaLa3dJ)AnKFyOWv#mat)(@kZTvr4x~+6ud1q-8F+FW7 zkCsOv-*=SEhiG3Cxjf3R`&R_uy?Al<;k(R>my%B;CQPvvlYzaKv(J7-bb=949TWxY zobbC<$+dx`4V6lg3zQkqIzlcNzh9K(*x65ra6%4M$Ezu6e3dPf>@H zi%=;a@gRG>c;qBV^|a$1{L)a;`M2f>pxkSf$lG0zSrK0{nczh$rp9TLr+J-HtyLM< ze5%>3w9Xswy{GStO35#pMElT;7aFa+YLBgfPztE2?;|*@527h{f)` zLRGUQ1ThsEYR|U!6>!FVa?ka0dX_>t`BQQbC~w$Z4w=2$fs^Ap?8UbIPQ?x?ctC;o zeSwaSSVpA}G&{4^?W7pqpR7-Jn^k29hTuHMrkeALLu|Dh&Bbv+U-eOC5w|3)@x+?@ z@l#A$^$BIsu`(Lss5Rrqsp@Sc8mZg^VVff=e3NJ!3g zO4)4b1&?d}xFvfW_2X8x(b$%J*avYWK8@`@P_9oJI~+j@LWZ3_Zf;B(yPUWwY24z- z=A?0}BU_TjZH{bB8c!#U+ns;A<99f|BWdnT8h1LrD``}m`Ie+{mm`RQb~}O$=x#?4 z0Nvxr9ZBO}NA64-_c>BY8e@*!l{Cg3*_|{d9JxDb>~Z8Ck8ZExnhIn-OvPmOi2P@t zvmyW4@5p%4xZjb9r15|wdy>WhN09g&bYwDVJm|>2r11_%_9u;ZI&y!~cqnN+?9n}t zG~VUJ14-lEjvP!Hha7n@X}rggcO;F&j=VEzJmLt&A9dv6r14%y-jy^SbL8Dgl#qfR`mrZu<6w1{UONg7W${n4cHq$BT58pj=ZENQ&Ykt0dtgd>k9jc-UA z-{=aDCXJ_@cucZ{lLFcA-jVWhuq~C#$fF<}-MKm>N?b{+J6VZ6c(Q_!sCC(_li4Z1 zO-pojtT*w=PD80Ea~1r$0WF^x35+@&H05EqCM5pW?bfVQ}#7ou53-s9z)S* zC7qdCn1*X?q?7i<;#704w*uRnag=OR8vK+NTa321rP)sJmDvjti@GIFx_Y++YT%ov zE7?@I)5U&{vq!g^O~gLTNhf&Ne%~sv#_jaIi_T38Xz5BWj*$dxJm3F%3v(=6Og|@+ zbIp+N6spN~>kAU1O|u2h(K=$Fxs03l^rc`|gcUJ}*-2uS-J3BrFSlk{SRUel!O*>Z zPt7*i{KW#~z?)A1d5X0f0+-E-S4kqc{zSN*g*Ia|93j%;oCCltFu7N=3CdPz0S2bs zFU^3`HGZZ|=D^3EP7GeqLJdP>b4Zzar^WFf*JuIN`y0jQ5(0 ztT}uL^({_RuC!R~sX9_M*VP~1jc}|e#$>seH1S_kTAJzlxuztPY44BKd-cjU*E#DL zHTQ))Pp8WhZH|VBWEvH*CzYT<=^zT(l=Z?3lV=ZQIpEZ~bbUNnGmfvF)?)w6>g-4g zFT2PI4z-;C{UA1#>q@se%SyN5=?+y+64s|%AE0}UR^7;wlH0rr!LQ~xI+Dunm-b|8 zcDB>#zCh=XDkiBtBr%^2qd>okR5!4+3XWdJ*F%-(I$~%uL>0rC<1stJ?CkW?f+@n* z&QD+Hm+fP*&Zy#1#W}82+~hz#e_VS_PRB!i5y-2aFJp7l$;gMev`d{@Iz-i5ZBDg7 zFwkWQ=}`@6X;S*|bvMyIR5{j~0YtQg(7)pTbN@`e-8w%z-KdO%2+a%4&eHC+Yn^)g zLi14NsdQyM2S7J;s~MZ9z)Sx%nlGHyAdaoH^X8k6$kvrv04VJcLlw~MuRPi6bbBTb zQC&ZjegJguQt147L?<9@?7;>l;s5^1sV2+VFb+DaySZX*F?zc3O$&8u>WS`(2wg8s zxQ=mHvA%R|QruRr4IPu|3epkn%-Z8(>dsBqwS%KwsdKIp%dh}Bt}H`uA4r-;lM_0d zc#^!StxR;&T47`^bf_P4rVT`^o$e%>cO|Vj20EQ-Cenr>eH;_hU55?G$xAk?IGtsy zopgfUxI<6g zs1HTjvZMpvAgrU|;OvQ5IyVN>8okx*SOmFDVij;KLKuhmCABk0H|`Q*fjSCUa}tI5 zdGJ_eaZj_hxc4gI)x94e{J_#a#dOqA^rZ`KVbiPE*i!0qOCdF1{eYy(c+Rq1omg6F zRu#3T^($*uX@F2#iFg_l(QI9ASg9xU4LT|ulK^)~6K}4cD!OFsOX}!%(@osC%DHZH zp)q^nQ00_KwJ|l)QhDUpMg=A^2mGg(F!7aMa0zcfFTPO%AowQns7pDNtb*}9lP^ua zHhFjwHr-6u^p!eg0u>MJiN6O$Cd3>E?^2N8fI^qrvsc=AH-&KNxe3y1?rB^amkARc z>gd4pT!zF3>4H5eEOCFds)i2NJWQeO@mx+_WHpxn3uu-x8ZTHHeZZ38gOimfWSNld zbAp`o&BWyX{rme8`X!C#tN-R&W)y51p`m`Y(8Qq|HmEt$6~&Bldhf5^}HwDqv4gZfmwiHz!ir6%Lra% zLRw2nPjOs@T8etI{S$pM(`QQGi1T_o*w{WFx-!e^Gmkf#rJ#}xYDI1~1F|UhR`}QK z9I>>~hDb(NyL16ES^K`IrT}8;c73JQdj-%$<}lS0l=eB8%B$mr2lq8+g~ggp9J&E` z_M7Vio?GK6ext?=k(Ke2vlanhSCi^A05m99My0jeA%(a>y9MBZPd|t4%-1{#M3+*G zSjPW6+nAlJwHi90^sXMNJX`N@QkgCuTc-O`uPk3>Jd`AGFdWYcy~zc3K?bBM&6BQc zn3K^8OE~wndH}+`QOih5C4Xzyr(P*6@eL;O)x|@V7t*$2d+I&f(@a}F0Np|IA8c9x=Q=S_FbBH8QuR%|qf#Ou0N7{N+Q-5*qRu8SHeIkDG&_MY&c zeevhSzOIyOdru&Ha!irEj_q5Lwy!HY#Kp<0lOH(r!huQtKO27^IBT`}?1K$!k~@<~ zZhQ5})!DP>U?Jln38@~DKq$1|SEnf?WnNZj?v<7*eg1_9&Z=bkcMoQ$(0GGWwNz!O-;sh0B{DiC2corLPRKDiyVnuip?4YAbE1_{yet9_%BPu57|QPfXJ+ ze7M)4IFI}{8`YTmTpqNERAeJ`PRni*rBMu+G$-WAM z5TOjwh{;V-o+8D4)2nCrb!8cl_wQ4Q3^ZvQa}wFxSp_Rw5bQPpf;Ai=Z~nEwrW^BU%QvlMG>w zS@H@uZOtP?s2pJKAY0He>~BTw#zpPIVuz`R>}dsp-UUe{_pXcB7}yX%tA(YGN#L%= zpv-t)FO{&=kwqh^tCC$5mlb3@55+U~j3pcXV6I?8AB-6u!pa7NhKFQZG@Lv{O|oIY zXcRMshr*(V8r7%KgWc&lbw*QCe@Kq zrN|^`ZE|@2UnZxa5mLe(nd{r zsxrHEbvDAOs*|qGTdkH?X7SbIM5<`#!}yji*K`A9Tp$n@Jk6f;Y|;1G*3qT13gh^Dh}_GDI>F_ zl-=&h-AU)yJbU+e9<>bj`ZdfX4Uah!wnT+~)h3eOo}{%~b`z-)+OQT;x@ zz@BK(m&=Zo)gwyY@AQ4jwoUmS@Uz3(futP`7V@oKo^h>89kAUbyTFIbDQq3g`mc6! zQ230S^+f1)o8CQ`bpG2i^#1t!NB{y<-8-@s!g|R!FKS^=j}elM*RTgwHY}M$XT|HB zG`)GvIA%KP=e?%BPb{{2qP^MOlJSWkm;;o1{+Z`ZBx3I#e-}wrG4f{6P~i09N)85* zy{xO)QxwKe4*;IPxuk|H;AycigB1!dS0#jzmL(I=E5sY|Np4Cw_z4C`pz}ZicJxrxV zlf(~2x?(2qKK^>NeZiwgThQzK23z#5uh=(Xb-U9&&!~v?)0f?9Yxdm5DqxgUHOi{E zUgr!EgR?Yi-FeTVBDL;?c6F0-K~R&HJJynQg>zuOgbCABy1gk!-L=(YI2u?(5K#Rv zF<(|Osdp`w!^LK&e4g;?v&7ue;?3;@2ABQX1D>PCWc5zWuGe8Tx*hI*gk5c1oN~-2HC=CLKDZ*{B6l;;bYmGCZx+5Q!%lG# zV{Myqmt+*aR-kZkG=V^9&w_Q#=_pjKH2<08v@&o{#a7($vcX_9P*W~<_pn-A@4DSR z)9Z8~X4vDxsA-X*Wol*)ydT$@FRYt|m(@CfQE{ezsaX?1RexMbg9NgdHsV!3ljY(z z3lxECDuKv9s&sAm+_Y%68`baEvzF`_Soa0~`sPD09(t_)v@!@$>H*}g%Qegyc@3J? zpHfMFZ=FY}{tTsO1({-c67F-Y<9fg#f7ilkZ7vUC*F03Zlhm=&y+Gv@SuIG>U&EOI zPE#z}uEdiL&0a-J72jMZO@nJS3^r=DZ&zP6G0B_n3-Fh`ZGF)gpZM5Y=*8#W#$Hrs zsO%HVIw5!xjCon!OC**B^k)J9COrnm`;Nx3v%#vF#}vw!gH>a{H=<*TK=gUM`(7`+m}>9Gv{i zWUc;ffp>y0x<+jZ?d#T`^c!VQ=#*f@$0p?RSjGHPHf{Ie*VNq2`{D??u_d`pjh8&+l zvLf2ZSqM7*RWqY>gr(T5n1u28M1@IQhILxSyp zq0r7Bl+JTfiRtE)A8;U zAdt(QPQ*nvo65=(EfrAJm#UjkYsl(%vN9%F=h$Rk{5mODUnUiM77&(HS_DaVg5ZQD z7c-Z~LoH*0w2YJrlIire(mmuZq*i)Y=g@=6c6MeCu^aZW*s5H>N=2Aw+1g1vwCKdg!~nmXR?qVw;tYEVmO=n_`w;*&#TK3k z?@LMNWhV_Lqr_la0e58?0>p3eEqGS~B=1u4>SUXa-tQGkog#NMp$D1?TG{;uMw5eD zHMmZPcgg=akOxCblU-+RWw7FAHpi7|E~zb7N_Ld5R?Os_My6e%(JO2q;0{mI7CL zREW(Ms453kaeBZTL-6y#LGTW8ec8;0_cU$LB&OHhb7q5Gj~4?`&1qMYi4qLS)Eot& z&YU&`!XMPm#1vknJ+Ok!z&Y6RX`nW;!Yg(k4ySPrNX1|59rVWc+vA#NFbZb1Eh?>k ziQ!j&RDsr``eg+oL*~1iHLr+i*bnN>%L;x@!4E624Dm;Zjf*G-EB{4eAwWov=)bN6 zt;k&CW{;GjqyJosx|vAAA@DvQ1>ae|yL@Zuu3(PUl741IdeKx`>>H3cluAmoISx7QvN;y(o=?sV=Qy>4 z>2U*~%WS4z5o8duWIpqX-iS>Y7m`>1t^z@-&z94n!g{J#{qK|{5ey=lRFI{q;c4;h zUxDgxQT1Qs^k|jj7x$FYMNqRj$llJw5|GW|RiX6?URPr%H7 z(rk*K5HN2D{eosvzoY!R-$MYq%KQP#Iwwr3Yd|s7h(wrzrA7~IL6IyRSe1woe@=9e za|671pW%k@6yUE;7Wat?E4NQ0X1jNp?M5Z%#nL-7+=u7qLe6ALdD1$Y;96y@k=~06 zC}dl=bDg*x)T`8Od%WM)AlDU%8&5J|E7X%)DRu8*3Tz3$O-BqfFr4cbaBIE|)!jd? zU9vCvF4h_UNTmZCc)T@Tvm9{=vAg*xoH9rEx8}nD{2wTpjEW@&D1F&XV%No>)B3sn1+f1 z9QAw?KX>lu*gK1zgA?H5xoiXgGBh>;Z3D^TQv%OpXOCGwMj3-^klh`?9`KwBHzC2$ z=8$hv$spt#Fph+W%N)Vy_i}=b`Ea=g4&GYB)lT#AyEOk}427%uUe}m3q_R6Sq7Cjm^$+@NfKD%+%S~BibF%nM zrqBalsTzrcM*A*f@rV@bO5NlJiH}&MoOW%qQyJ#kEOq|7I4W;Vi!cK9Vq21?Y}S~R zvOUus?U3#WcSnowb`ee8e)%Diw0a#6aD}uPip(9M(%)7-O#-8Rm~!)Lc%pkdlkOi5 zq+{EajqR3X@zbtW$W=9?IYzIuNyOsw3&6XL4w!qmcsLVC;GX|X*Osm z&DSCg{|D*v+?w_Lwk&e{b<1wVTb|pp#keC| zp4+pOJG14fWXp5dt?XK1d1y11r=n&K&;R{_;_`fc;B+dxx=T(abjn>Alstz%BYsLP zM(`xTCW1#zL`EkDiCnx}Da`gYE1V;@PrDl>@uDFC5`HJx!0r@E-91R2+C9nq4-W-IsKJW>s%2 zxriz`PFxHlrjG46&=R^6vk#6V%%YKP@&}>_em4UB!Lb06dz05U21x;3@~T7ak2j9Z zE(SD)dLQL^;4v;YLVaIJULT>$KeLJ@Wid~U?}UJNKLhd3*|6v0$c;cLc&#M@sLFWG zu5uqL{AT)I6nST4qsIm8ye1h<7VhlX9zH$L%WOMN_aT=^-BC!&rW~O~gfGkMdP2t3 zY)9PL>mrD-u?ad~Uoi<^1hka9{R%_#tO@mP`AtzBTlAj<1+`ZXT)P&LgV(NMejV}7 zwQK$M!%5czmBSThkG}`6@t6GZ)VXGngz|JMhaag}43ZBZzq*_C>bilal_L^S;r+f* z)|>66z7^EjKHHf`W_|mKGf>`AUP9uua!>PEldflX6Z_u9) zCf1I{(29gma^+=PBEm^UP=*(oY87QUh>Ls`8JVO8l#VTL(2Jh>xd?}xT%ucc`ByWf z|MDe$DPPl`!dmcV3PVanG<12hquNV)I(;CQgIOL~;GwmBv4hkHKlo zF*>xOnY-7#7EeQAuBsW~S!+B_0D*XUzzd4p zNa$U@cJ>Uz$s@0)4gk|1#HmmEWMA-%g13OakTQkmCF`Uq&nt>HP`D_f0FdB z#^J7cIwqnTE!A~xZzE^1ZD-Q8rOZYr$`o2Y8HYBhSmK!UYl{6LfvsS&%49RH%0s6i z{YCYUlt*W_bVO27=wNtX50O1xfS#%VII|tDHOLI@k#jr=xSmbTG08|z9BV{5t&-@IGPyUH6a z9Nvu`?JcBk=4r^Xf=#9MwlR{$kyKaxGB+-a&7A^Sf2?$q9+4NmA3`F#UORa@L8XI?a(S5nv)T*iI}h_R=o zJ&IyxNG>$-D~5GrBlYN>*MetvZX0>uOI@({IWSe*-}#p9U;RDUyg|Ol#2yx0^wKWU zF)<9@kC!?~6W3$s1NkYTKabg1JgnwEwy`Pse~%kH?%`Y=w>AB}jh#L7w*V9CS@G_g z%?fue!-reOdE{;7lajU>P8w)2oSUUnCujJcmGHP4PVY4bYHjH{r#;K-Jt7V%d`YN4 z@MmVV`)32q#S3^o(&mQe1pPTUc|LufjN}PB7pR=gE3SSBB4OAy)j_7j97EF9ZsI?XR4NGi``t*&fP&(V1HI}c4j(U@N?kJ zqVOF1`Y@Ht8rE~+F62*xv|{Osx=TAeF>u^C5_t zw3+<}B$)jN^rHO-gophHi~0){l=9lBp#MMx{Rb+D2a(2dMl2CB$huFP^vUYKA*}ws z0)d|S!@~|A4Y{|pOJVZ6ni$I{Z-U-H@tb|1boTK&hVaPbRYA09mrt_l8XR;k2Vt z$E4C_+wx`)nIMU0{sBU}-Yp612m;+~!0CYIOR3Cn*b+0(g)_5J^G24$OXeC`RS4%} z+vU-c`D^Pf{w8sKvQ_iFK~iues9KWzrV4jTA_XCqg{!9wmIJJG`q38w!nkH*wQ{cP#b*N6ob!=tmFs&j-OF%`mQmwzra!YiFo9#e9p` zHtqZmAUWhO)RysOA&^#>fqUJ!hiDBFiV^9C!0!RUwqP`p@=VN>QgBp$ryMvlow9tl z(QJ3Fv^Z1(o|rPFSa^Hv?0>Q+b24!gvU^1~A81bmykHP*>3$^i1`|i)Fl)zS=N}uN zUFh(9tE3u@K*B9v50rR>7u}_9p|n`p%eSms9p4Jl#%O2WU>^VQ^ZkVS=%#ip^=6KjI_oRQyOxDfGL zW3m3P>)R7TYLh8BfQJgLN~YAc;pE|g!}FhX4q6OCCaT6m5Gr1tR)IBMIxCR7+6ej# zQFBfoNYY%UYkEZ|LKI<-y<$!O}l zSj?JlfyyDL_`j8$Jpbf>1%u+D=rvVKyaM26ff0J+lpnEk&V8{b!yf1z;}?Z zs`86IT{o^EVh`xO&|XeBLl7Y|CLe0!vgz(ThlCO0$}}6WPq1D-(dsFj9l1At%dLo@ zD5jizVN9p&cJm6%U{-cRF(t}ZWqf1;Y@sD!sY?$)AaJCb_?e=7bO}WPuB|<~gdOH> zVtiyq2_;#4NhrfRL?4|G`MNUOyZ8yC-Y)%-wKXXHFS9f;7#XQbx6%)%?<$d>x_8{U z12V>48ljijL0*A>yrK^X0HRqQcdGKASD&CU6o6DuT8EPJ9Bma7#Ynpa zTVkGww53g3ldU=;V_HmgZ56lTTGA4`AeTGR3|Z6+QVc;u^%onW=bf7FsiWXddkT%D zKJy~fabC9VF8nZ)JAW@M3{!0@r0(Zgq8zCNb(tJD9N(r+RJ)ZItX+|VqDf+%YiGgL zRM+!{XFnazIL`SvovRvZucF^Uy|afZ_h4hnlQaOL0m~>$-n}l;`6=`QoI~|-1s#AT znp~l8CNk3%u08VUD7;A1Wf`g|u*YYYQ-nA^ABk>fwwIt0xB&BY_sUGj)7$==i z>v%M)LY2$WG!llqw!BB!FM_BA{)<}?ik(4l?vtfIFHVPXf&Lc_nJ`ou)F>L{~V;A{=;GAQuC`_l= zxavffPuUKD;oJ>7kdy8Mx>lh{y15pW{Oib5t z(WjIuamy-E;#&pFIc8)E0t{{F+W){d$D-tB}G%t9tDreLwe>lO=rVM&%8PVnFibkOuW)w^2k9~4~l9ZSKFn3_|Doyfcz>%D8MZyor=rY@fJoQA!8eohPa z=688*cRbaHG>cYT)JgNz@i463su$Z7Y*(;D!A=67g5s=n(5vADi=l4%Xe0aS5NM^$ zV#-g(u?9#=t?&lYLk$ebVEUOQFN!1*`J&v)9K7{K@xg;x*8VK4N7GGrrwq+-YryitzWjzUqCJ`A4fg}T;q_btQ?o)|COrcaoUvfP zhPXl@H^vAt;QEhZ za33yFAr?dL3g<7Xn)1(;gv_Zpf{~MbFqd6Kg%$mABjQVjl6joVM6Gn~pXgMl58oNE zXLemw z3FR&ei@WtHOOwZi};Y!RwJ!lPkaq2 zk~gdgaiY#0Ir2Wph~FIYYIQTB%8nOIIqI!g?AXGftW+i}1!~C;)QL6+M)M%r4Bqhu zd$K_9pRoABe6!IT{Nf+CMKPQEtdVs@*YsB>r`pisC>dz~$HGL|?VSQ={zaL8?J{e# zGT)SyfhYj8o71mFE-kaRB$hodgOe3om%@=mgA8Z~=&Y>-zfz|A0;d8{8=heZ_>`(y zj`)Bo#w7OBrmJ1y$c}HfEY(K{Ak3hfjS7vuO#*!4ln3h23jiNdaa6YmLbvb`dg3L< zBB2tuD&P>=s!x)w?|LaddA3_=%eI`yl=irSJ5{@IF|8dVg6exz)nUERR-C?=?OA9T zPZ{fellUPXWQEyT30=M@bdhKv864%nyRN*syn+8EfRji~pq?Sjl33gm)7s^twtvP$ z97+jUZs<=Mfz`WW0Fb3E=FsJ2aVmd)zAt#&n(`jNKGXp=0$vmN)Q#o+|Woq zG7=2OCavXW1^+X{?&(g-vKgl&<|65wcP((SN1A0dR=29Wf@Lp49W~91zEhIwU-MXf zOo5?nuVR0T;N}3PD(4OVN|0~|btMowDWv3EYXn|J_X4G_-+*W~y+tCx*4iKC*Zlzd8|5!Vgg%rqD8iQ8tNLSWmkL6QlHdc!hp>TqeArq>ria-lV(b7ZjSSkQuO|t7@ zkwO-6P4Y;jJc2%9@m<>4Vte-KVJs6b0xaYY!h<@CIt(cKChh8E{$oG}lrUC0xK0fy z3|Hio#d}?t%lw&M)CuB%X2chM7Z&1#vi!V)KUDA(L2#X6Drs@oO|H=iTKsofqhRz$ zOFPTk%0m`@uV+)!<>EU7E43}H=I7PR{+siiMABocs%l2luMt0T0Xp3!R7G$Eu+Au8XLW^fA(JPhLd9#5D)IE?aqeE|-8Z&4@Q#L}BjRyG@Z0 zQEO(st>QvUSq74|+Vt}zi?Gc|X0>RLCP3t-xfLz;TWr2%mY+;iPm-$yEc2$pU zb84<4GA4*FD8X#j*WFIyxfM3X$|K!vdIi}EA#c8ARvsDGK3V+>1%Ir-T&4A8W*-gx zFYqEH4;je}&rOp4E^E&;FL3K|;gN`(>1g%ORf$+Ce>HWjist=kWOifA`&ZbHPpZ0I z3jVX|bjQq@enAxrDUPH9Y;9Zh+UeSn>a)uJtg`PRc9U3IqKf}lSo$ax>Nq|$pxajx zo1oGvZ!PUuwV#+gxZ=mDv|(_lyw`8Hv*(J^-mX;trnC#`M|FYAV?+YNI~++$9Jvty zgReRi5ctLrv{1{3U`W(jzrEwwNR1CK8;2%eW5~yGkO#@GP2sGUw&etP zt?siLgscMugLLwlXl8>^Eq!%yHRmDz3ca5Q-A#`ZI6h<~*+2zXdKU+VcljMN;7Lf|ksa%h7=HgnLo2B!3PvI^+M{&L z@=VK5s`()mMt-H~sD2BT+$5lC$j3Do+9DJ1P8_am8toicy%fpxWW19Iqln31&t)^p zCx%Gs?u=9o*gFkt|GGXFEtE|KS*o$B>Z%n6Tm2hiAur4^&ntbCqtZk^t5K=*3G0h5 zQMIh!f2r}jTjLoZ_%AS?_fzfr^YIMi;~8c=YxrqGyYjg5wbmzqw{sv}ACNKiOOf-a zIW%8$9#|OD(e(Vyqjz#xCh?92^c2UD$%LyV@LVD?3{!t3g?l&*9gch6{NZL?qB& zvbv}e`$?5TPsP{RrOwL=(y+2Vj#}=yq8FDd&B`m-zbyUs$#{M5k+F%x5yuhXPOdtZ`Oh3o|B^7>*+|~CfFlpUPTBz~3 z9#NmhIjN>7sFOZzZ`*^sKQ)v91v!GtQ^Lr@U8zo;~AR?*rQh{2Gs;mlct zbIuQsha)H*g>RD5eKY-dQIugnZ6w2@444aH-KVdsZ$SBU2j|so#CG!kXnAcOTOR^D zQIMP>3PfWgr>`R3ATAr5&l#^nM-_t`Ar=Gx#RX6fJxCo{LKwoLC&y~mN6%u=VhtH*f@f?IO^P{<+6Mln$7h(6qrRIO5M-r!Wd>*pp3!-u`(5YAinb2^o zSk18_updVZNcjHDh`c5%$os7_n;}1ic@Ubf;R0?eF1FhaG#hkjaeu!TzMW3Zv8}n3 z?0d5ETT7>9#NyEarHT`HCuh>c^i7vUCfjH(#vNSMgA$rzymagduzO-Nd{u z^wmc~li!)uN5zHSg?e4lmDvTplgdT72aG{{D-Vo~oQa;oDN4X3y}LgA{D3k8E;`Co`Er#8D&t|6r~;wr(n zj0;I~Lv^4OsLDf322olAIoT#c#IBDmVdonZ(Y^6+FoELOaigr=g)HlWv%)x-LdoJW zZ4UD~nnOrLL)87tia?K)L$oSl@B!)jKUwiT$Ar{4r^B#w&^m?T9rFAB?jrnXV|7;q zO6^LZpEm7f@S4}-ENuSSRZH_0?A&<+&A=MZNfff^VVhO@fsA@CAX?ZB&@7+RPC|7H50;Uygd@`dMzA3tjc>#zHh^fO&ezh*=mlMAEk$2Kvzf2uO|GC?{#C7@g|UNLL0!6= z@ahegS4#%c_v!fcuHKD^04sST%w9$WZn%0{IjvxE^|G`^uKEayF7aEqJbGEN&k)!S zw8%cb2~2wXXa5|t|F84ehv)|WtT6k-^f{k>sq)0u7&*ksnF(U_5fAxz$kW(+ggW>& zF%y`c9y6QpMYI%>4$tdc2ZM(W_H`RF`HqO0HC0K(*439VB=|A0OcIbos^wz$mrT4l z%|$BNOY^^!%s*ubcVu}g&4M`CubgG3g z+g1k~`d5M_^O3?4#JW4LPoj-kUwNda6vB5q{e0kMiptV4Ojp z1py`5Xh?$KofFU!Q0+5NSw7fCdtw0V98#oz>tOK*0>VC-K^P$P5s}$+)fW}vkq*ep zUz|SbddmSwJq#z1gXN0|8mwv)FoqgNNZ?aJ1_%L4zpYAvfy_x zv-lkaNJcp9JLdXg^=U=xL=roWlTePXO6HI0-)Rz;2s4F@RgvBTpRR2_6gfG97d$g- zDJ%!*1c&~U$}Qb!aOlqxyU7(0eb-kNjK}wD4K|V@$!>VOLnjKK?l~5kY6wQtb<5CH zScnDxotk)s;Np^v!03rIVA)2%oU#KfLd>-t{CrAqRg)R*&Z^Xu>|t`vuPV(ytC!^qnImu+Y!hc!9XK+MZBH)lkT*WP3a4S)e6=&GP%2aWY6&-H)@5&uneGJ* zghL?%4#@Dgx0VjxrLID_z^GfnB|c)t=9D{rfF8c0cgN1YoY~Ns!l@9ZmE6?{KW3a{ zsr3%iKk!HoS*K$;SR2N`ZlzbW1$Bs9XCU8N+MEBy_%s+j|L0{bRweVkc30+ojc;e6 zuXXX)hv=sb4QjjPf$jMF`yxW!oz95JREtaCadrY~1 zyo~QQeO=67HQaIhee>NkMWZ7pWj7|<7zqF3_38PkvL$%D3aBF!AP3efa$gG0wK2OE z9CsBHX^)tP%=6m&`8iwOOoT3NWMb()=1c|B-nTV}*gm z&$^(h`vKkGUHnA;{B6m#jeg)V)88Lh0x>X7*#F|)Z`Z~BX~XFf#TZq6p6BrVg*Q_= ztzb#9)AOh8moWXX8ZH%q84y2Pw#9##_x7hUglCFCaQqQ2{$T#{ykr$dva$tt%n(w>gLfv8}!pijzY!k{z<)nPoP9Nv;p`R5NK5-k2PM`TM28`GbD# z21IL$c@OjQ^!zsb26{!(55xhme=&$#Kcm`xfoiFn0bCt!P}6oZu(kNWo$US_FuMQ1 z2NnAdRB)hJLH~gY`VUmlf1rYRU`}cWMaEJWg1^aLl*^`HNGr$Y?gcG7HK_&IZGQ?ZSUCs?swp1E;GK377gyYmyen^wmwAet|} zO4|0Psl9;m4^rsM33LL;nCY*+Vyby=SH@kTJ_D|)S6}pDQ~P`2gS`mqNzL!@6}}RI z{Jv21kxIOd zX}$T9f?re6Q~*DaRDVN(Y;$aZ2G6a=CH5cFi;pSzP6Z!Va7lqDqN~_v6=*0(L6YRj z)TvQOEMyrI-x*ZD>iCZ_IOX64k@x|)zstPW!CRH(nwi~Y=)#K0V@qbhBV+y2Y ztx7`~=Hh#J^i8eo%ZQqX_>jaq^-9Z$r#gEUD|{%f`ginF186j>N$C1}tWaO!Y5 zyr}4rdljwA<6=}pwh2!f_so1Q1vccw%)NJMeM-C=*PjFpdh}gwe_|X08m=t%(4t#! zw&Kr4ceL56$-cgM(bJf9+aeSXuW4n?Vl4_?JXaN1dPX}W`p#SSguhz;b6}6{_rEeb z)4cy2ZV}JT-rt*cUk#wUb zju7K9?HXfz6JEEK-7<;J3m(fJByBM71|Mh;!B=@cujSADjESnNCoaE2;pN`aOpNKo zDI;-$qIs=&Rk7Y$VVQKy^Q}<=y(e)B*Qvj4{;}0icxL=mvO!1zZSuW z`y1zpez^`G~SC)VRaF@db)jxLonnvj&H)A`l(yKD;u!Uc_h6J6QIm z#2`Em_A_EAl4C*S&wMX{YHoVEoN-u)?fbr|Prba7K1XKc&sx&QTDlcm zs$jk`CxZ@@(%`Ww>X*r@iA@{Ce=c|N+~NBn*lz?SbFs9Cu(@e3U>Dixu=1G7E6O?` z8V8>|wa~fT%1t-8IT;@xQBOowX8784#&hOXWM8i-^3p-;YFX{GZB*shy2a^*5u$*1 zd-Z}rP5O<&{o#+T@TtK}$*w{5YYd8AN&RJ;r0c-cpDfL2Te9)Ir-e21NbE^^wmsD> z=z5_EMfv03@LyQ>OX_GI1ABqs@lKFZl^nNlC84WF)5E6TepP|4<`;BL)+>p49oa;6Qbv zv=3qA!P4z89#fhVby&=WWUSpS1W-<~2)50D?7UvO(RWV2pUed-`Ez>Y_tqIO?;>-2 zD{B=TPgfmFLcqR`SWeyl1-)KE-9MtK4Ms=e6g=d-Z2Bj;*ZZyI%1DXtbwf-cX{ibo zVhzJIo-9t-7S}@WV`q03Rzh-)RI}&_*UPQpkhqV!Oc^;$q zmrC=S^zZciZ~H@&;t5UP8E~H`Jc@_)1%xp&$-#$hP9bm>1CcKao;RaC!(ViHU)g7zB`E%r;6AV*GGvxaQpXn!s-P{*!f_z+ zI6TPl_kmN#!JXsp6J(>=w9R}8at^mbCMb#&7F;vLHAo)Ev%3|)r#yv{G6LJwg73gE)_NTV0 z=LM*VlTJ$OJxBnLhBd{Byao&^xV0DUMkYN_9rxXZqhKyQB8gno<&Pfi0*zhO{J)1R z{X}BXJDlq0t^A?&&zBcpHtA6xt0mEgs5JWMYkdQnLnboq8iRuzZY+bpF_oP!%0ffs zf_R{8=nsURr9)AEOgkL?KgKT}TY4_BKj<)8mddH$sRoP~dY$Fd@H zsXtj!a-}ZKW%=pS*UleZqOBM*UAjSj`ollZxa?PS`sn^NK~8v%)~CXmb7gE?8sy3z z9mH&#JfS$>V1ZV_d5s-OAFs>Sw>b=NL1rUtwjb!qh<>YmHI;KEE#{F7d}pg zt5K0a%W#g(&OJFh7l+6vl}CI=uvr#ziGBo0wRB1hNZJ8N<+L0u%b%}>K9+#>HWza6 zmE|mhSu>HxWt7o^sBB1{I^XuHeweK0`WU|dG6QkB+9E8*Wm%VZ3dBsGQ?GAV8EsUJ zgGXvNDQxm7H|y6S5zwcoI2qa{@6GURL#U1i;o^L4(|@H>z767fJ6~Rn5AR9vB+H?D z(x1K$oyGTt=tyK$vPGYZ|92u_Dku&?W#5Mg3bP3@+|q}rL}TL~!H}(f`N%T}07ZDh zFHP9fIOPAw}zWAJ|Awcq7%Lhrn&3hiPGc5oRnOM6^#a1_%#k?qUQ4W#RZ*Ibn_- z8F!)6Rg3E~OBjk2sH8{8PFS3h`k#mze?s5mY;vyZF3hB#N~H`&0X-S$hbm~#_%TVa zGa(GBmgV9X9cylsX-`4bRN2GnC%ujk*#27I%iLk({-dx*bImr#x(k(sxl{sWL{%eE zmu@okC2ILhEIIz>sd26uWy(SrZQM2MW4`3eOlbAj6?{ZleuG#a@r)xK>J8VTT#S(Z zp;5^?Y)9*(LT<7sD0#ZuS^1)0c$Id?6=Q!A> z;Y`FN;QX7LwIjUVYK{SP106?`T_<@p|98O?j6FEvP!q4hC4ij)l;%IkOH49Btiay+ z87IIZ?9!+5a8o7!3T(0KlC7X4RcNRvTtRv85^X=?Cnd3EZjk7T93^-&6z=NyreyIU zz3?{{G4?Md#m*BnYMs&H^jbkCxIr@e#xJM6!5a zf9ic`qmQVe=`iL4+ah{+{`+ww&OB^Vmt(6w+4^fv|5j`n&<<4pD^}S2e~U=sl@E{; zZtD37$;M<-_i#OA^~CG=uW`kQ`yarxB>0u3-QBmQP_L<)6>1erQvfVSEmSW zZUU-bprb#h5pybG#3#p}P{sQc>{oEVg69>SR&ZRwClq`^!90PLb=@8+hbhE#ugo^8 zXY}eN1?LrftAcM+aJPb=QSc8H{DOiR1y>b}DfmSNFDvLO_-6`!NWp)vK$B7Z7Ycq) z!M{@QDFxrH;L{2!45+$a!95BdQs7(Hk16JRorz*66qFQLC1B-%HJgIIIE$L?vS#v_ z^7#sk&$hPa#AQpW-_&YVFDbB+&6~!@^!S|$%mn_BVxLy`J23pI;l0BfhwmQVzv2E38-_OzKRi4^xOMnJ@{9}*k!{uRYSQi=-Zi{^_%{Bp z43CWN8lKy5>+m?`4-P*xe9P!W^>^ruK-2$;GCa<&`+kCElAHnN3Hctzs-$UEAM6ZR z@DO%FkmQH3%z`F9q*wM!j?vvi4XlXPCJ&uKvOk)9n9}Sm>l|5~R8NhIYwkV@5PO3^ zRroYH!c>_Xr1InZJAsWR&VgZzU?u8sIp;EAu=t@cLD81}Mg_A{A<$k@PwLn_YscRh zT=HeSr4sU-5p%Oyy_x#%26+qpjt^7%5~eIWQl<}Ce00FmqFM??boKkth~LPJWt&0Gf;SnK|yUw$8ir{i+2Z3w%h5ubui^h77H}OyNj1 zV+))zU*In6_`<}VsoC!M<%!AkNq=}!;=;sP?`f5 z{x_Sd<@GRens;x4xy@ETkxxR^+pY7n(~W?~<;CTW{(Nhy1sk7Fq`-{{fOXCq&0iJf zczI3f5DuKZMoTA(7ih0ED{)C!D&04Nl6qx#jCgn6a~n$48x4PX5~*n6)+@V9*0<&T z$;O>sMlL8!Sez;P6g~@5t&6@bxu`E^wfeS7DWo)K&NXQr2v6rVuUa4&lV6zU{!)9k zy4m6~Gt1G(cpyMrhB}i{2rVB_2CQQ*EKFX`LF!Z!^}oJ(R_jjB_PS$MCGnZgF02$% zV=BhHO82m(-+zwIjvcQ{v(K2&gQXTcZfdMMGnP%(LKiv+rO=rV>#de71ef)(a=BHn zjL$xZzw}Ur32aP-K+Z=h zG_t=tGyY~U4U9w6*DW+X-dvb$Mw@w^`)4kXFKJ6s3H7R= zX>;fHHTJl6pyT=0btH^?Y%gZ!QWAV|=VmSk*=2*dL{8kfH;Z#_gQtU^hF`8>8bBt0 zn@A@cWE=h;K(Q(mU=el5LpDpfL+*>1{ra0F zw5B_LIyk49>SX-Im{_ z`M*N4{GO6}?K3WiJ)Op0F3A!ACIgfCe6VT;H_Pu+)?;TM({V6u`JNcUZ8pJnvuLI` z5$c2Lsfhi9w9S|78Clx!3pJnLhj!fSjg9gswqRvs*&uv-Ez zUnvK$Y5gmDrN~{%-AM*u3Hu&MB@!$gp^Prwcavyzx}FnEt#aA@IRu{13=s+#yFc)G zf-2bR3NNq$n8BtswNn6z`x`^|ZgPj+?L6j!BCks_r<~w^BYV)6iO4~~OUO?BN*HE} z289CfWyL;2;6rxPgN?`^qzRj9~MH_ z=>DJ6n<)i9r6R8IgFL$B3znlf+K#BY+~!-i=e!1eHQ>1eb~$L3;JJg4H)IcsjfLvB zQmK#13$-x6q$u8c_^xFb#JaPeBX=kbC6jg+FFBm>4GNLOfsfn#U4TcFPV;avlqbTy}44UiU%4dsnd5J@+?j2v!!P?E%a6ACdR zRli;qVXuv>@QMQ>B>DAnSs6lG7u&_Cs8QxA?}LoD`qKm5yR^~6_7V0wb&vCK`B{5AAcvpWBVCxUv zvA2XPB31uDxU!MR&~9OYBo`>z%frtBA}n`JzR4hhlWNzN0v{Kmi#ouBVYEO5QI3ML zf&m303Nm+2Z4j8;XSasZ#C?9!dje z_tUDY#p$lcr@^}MrSgxd#DfZ6R`6R2`v^anIx0()YCYPA8iwlGp=Wvj@`T_^)ybCY5RxUX{T=EWHQs| zA5GdM?(g^e?%rKMkgNzAXDV_J&pr1%zxVmhcdmN=*T~?v{^0DpD=zzQ7ydnqUvkKE zu8O~&t2?)l=RLRJwaW``q0r9vxP>0&3$EVlR(H9DU2e6{E%dq7ez(x?RtMa|fLkrP zg`!&>bPI!Sb;vCYxz%B}Fzi-G+`@?SI9`vdkGj=Sw=inY7=O$yjJ3xZcMId~{BF0f zyPe(zwKbRUT@V&6{ajKv7sL zA$3ZU)5x8^-dv4NUyGXn!s&E%?eub3UyiQD;e)GTl16d)$PKj=MC+uX?S2>1i5Y+;)ziwmD(}+ssiT-|$@gg7?b#HumI; zzyi3CY!N@{z2+j0c(1vLBi@zuUHI7|`m~6DQfLj>BEGxrB7WBv@msIGh!cf$ko->U zjOKIiAfR!`#@paB)+1`mGyri z0Hi|{MkFgS>J$e1G0!z79bXJ)B!$B41xA`L4T13I#0+I|UAY|WMUg~A9EEkgN&@mo zjm%ub&m{+tJk#(^+|(*%6Gr}Ovl{tN`pG@pzpgZ!aWz4O(l9CjcIYmtHIg)JR3eR7 zuO+EOT6%M>9!VV3qlP%S6^D)GD4?VY$j<>;SB);mH=}?uCm> zY^xOiW~H1_RF77%2o0FjmK!@1Yh2J4!0$s*0`)5SRxI>+`@G@o&xkh;i8t=;f#f?> zI93?e6HA7@eTD5xSw4dL!`>L${7~U!VY)EsRVb3V8m?NB@>h`oH9$Lja-A*~CIBjAFvKR7in)UnK^u%L5XV&jR&Rum^PY$Z zJ5iqSr32**d-r+MNJqR0{ARog`GD+71{2=Di43tUQwMecI0b+v_CX0ilCT9z0Ist< zQ2J9u@9g3yh*ILnFga~%bET~AqsS8y=f2*NCefT|CR}95FE*8%Zn@PkAWI`K@BAAxeiu^#~abDMCc73!dnR) z38B>pMpq>rL1Deth{Cv>S!YDDI@8pn;-2Zd5L@CP zVy0{ly+TtNVh+zJg%s>{E0BN4!@frzG81`Ne2|OCfLXMGD34NMvjjY1CEyXu>}ELx z>dGGOV7+uN1#mi{ea`Q@*5w|_? zI_=PdHN?ILO#su@-!5#|)x5eIpsK;h+=xA-mVl*!BpdR8>J7QOfEW}JU_0z49z*^0 zU-7Q6!~jynB!1kx(m32++yO1_c)`|CHc032KIkT>EbrO6MDzwUHNM?O#l3FjK6Ui# zh4c`|0$Q^keP&h;6LBm*qM4u&^Eu)sSfuUC%BWlUAy$gNQs6s0-rsK1u8=;!jUYrb z_p4toDno3#yE*tJnB%Y>(|xsX+eb49va3(XI#%i>y;C#js8JnoRD(UD<=uS{p`QDw<@p;Wp<|5BR^TIHDH0lP>7=h z8wtsIpKr_t*`fjRZeY$PX)}(hewg?nOxw1>=f7A>uluq&{SCO8&^ZkrpJuQ<=Y4E& zBZYpV`qE52mC@)YO$;8U=n^A@?H)qx>%#ZmuHB8wNI1R5VBQTE#y(2n@pd);#HL=~ zqh^aUoi-ORtv9fIOe!C?vwU_GZA1-!^=3V-WsBCTf_yzt&9?zVU)^mCfn2Fcwx%X@n9dK535+CT$R5ZSi=A1hp#&&VoVBHlt}JF9>$F<^VC)tTond8oypjPx?)V)%Q6mSnO+R#sQ^!#JH%`?nk*=4X;#l` zH*z7Y!{(xCV{%A)lDTHX%Hr$IaW2P#b|au!t|7azhe ziIKSJeT6}6^G5KesLfx|E5c-DI#L)z>Ha+3>mBhX3j2`u;kO6*G1S}Z9fTK&Eyj=^ zL-{^$tbjk>32(Z$&pU`eM^&$c_kN*5kkIK!B7GXaWEQxDF$~ieD_t?q`s32JFlt~x zpimh&l;X!p2r$y1Wod%Q-hrtR|HjrHl4jN$Eow6%sGB`(ezI0j@xTpig1@V=pp?P>#RtP4;l3a<_* zEPP&`wYlVNps8mX6q{i4I{&2_17F^x6BZhCDS}2Vjzw*2U@fe`?{XIHYZNag z{430HZ&{i|VuI^E%w1)235giB0e^rAlXtOvo=L!Dkx7{edkQd7lQJ!+?m7kxF0=SY znNU(mKwYbcRmgP?XjwXIo{guZ-mB~1sU12{pgf|I9pM*YnB_h9sN^cO6@7sdNcfKA(?*D#> z9O_{AH86P=)9%6~50m0hd#)cKGK1TgMSumlHxGYUNx6LQ_V~+AhRB4uukm|$$h9GO zBmSDuX|67?k22)TM=3nJ`4Tdh?D&YVxC@L}%FdL)b>JxQ4`dgZC(m>t23{LH%`P6N zW`z~XS8$bq!{Lb_hB0iB3~w*atlx51AS&?$eQ58=me~}Js)$p7dsJ2ktH|dzJ<;yr zced?8%7~I_E}>lNA_;9i*Xp%Oz889(EM7rB(o7|qO{Rs KLD%7l6$pbqG8e2Msx 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
>tKp&fI&Rzw`S2e&_c%ci{ipHuA}zocVM$WdFAC z_XU2_-x~;_#{WWShH$z2TnLv7d3-TkF6Pez;qri=i=jCf<~N1Qo5K81xI7f*H;2oc z!~B+Tc}tid4wr|+{7ASw66Uvt%UeT1Z489wXqev?E^mwPgQ2-S%b?-?xV517SW1my~IvrNr{dVN2H&t5(mYKKF8N0?>)k91?7 z3blvyQXdNCr$g;wKX3B$BYFHYp>|ZymrsS-c&P7k6~`3gicg2yqfY)-lStDqh5Ae3`uR{h72anIC-c|SetkLAJ{B&&l1F~rk*|gt z^Zr_Bgiv|i;U~fz&!5y9pq|>ZOeEcYjz4EY?JV>4MyQ<&wdW1So1ylC!MG4=FZ$uF zPs!=p3NFdV>K#Lt}fJPXUmPWRiBHo>cER>x=?*!rLyGMdaO)+?$%?#w-Ym0?<_3SmDzS>zMi@- z)#Y}(-s(7pnFohH&zOunG(B0bHY@GA8hLB6-mY{O+N;xVx7Swr9l3q#i?!=(s|rS@ zTlM#<3-j}pR&90qDwS8)uAH1%oz5UlJ2#v4X+^Idd2(&_$jP-0MVgCitJBT;Y$qAt zkkVF4yD@jIlN{TSvD3L5v}0oI>GZ~;mnipMy*5|hP-?L~?;%j%k8Vh- z)jDx_VvlRev$jg)hI}RJu2tsd>RMy{YpA9zyLP45?V7tqHLFy1a_zRAoNHHV^}cC2 zlP?M{pN&%(=fpGT<#Y0O%cp*$(u{NG)p>kfjkn5P$`6fnwBy?IMkw_gjaIdu3JE0V zk`uN1jT5cqX45;^4?;bUIBT=fT3$J6oK&kfJH&@nD2{R$KJ|uo|9g$rQzu<^VUcaC z)LNxYY*o4SPl)h5zv;sSH%9~26u^m7ms-2SAb`C)652^96biI6^%Q@&Q z-X(*p$ub@H%Y}%`w$etq{QAYuzWv%;7s_S#ij zz~;QNt>M;Y0ovYF;9+Xz(yrT>)R?e@*2vyjX-TD9b8eWhMq?o_Te>qnDDOG_%LHQMzmCVuiWNmyo|ixK_Jwh+PM1=p#v2Q%b7X*d zGeG?t9mi&UaS4Ir2Su98alZOW#|@`9Q%Ax%bY>U8Mq1m=h4kn(qcyJ8 zZ|Toszb%?8o4wnuyo0K=Y|1?t zjp`fq@~OcN88d93WlGukN#t9g@k*t)^QB@M8g*Y?8I>ui!_ zT$6F4v&S7n3)0JXqm5N!8 z*pV-*Yq8f1kx<)3m0^^Wd#O{@%Ya{2?p4OgnLR>_>V8q^Qr%ywgiFfD5P;&w)XX1f zOa#b~0r?jnYUY>R%us(bo4;K%f3Mffme|Y(c{AWPU>_|4QTxvq1k7-(?T-Y-^)14( zlQV5$OM%W5MbIPI2Wunl`x`E)nWPHUzO*Y#D;oxn{0Y>b8uUu??=@95L4&rEp z=d=CreDd4C^ZOsJciZjZS^8R`I|bY0*j&rk0{EM;`CO5?h%*hP61*8iOzYt7r}bmS zZk;<~oqJWVyPy~~whA?|RmOL+0x!g~hW6h0u?ZF-e|@q`MTw71q)NFm6H~4@a%Lh` zwh?EZFiuS#KbQD*)#0!#$+!G+x~0zq`oYi`(Z?qvUf9 z#k~d7vpD{qCR6j$Bn_6%D^Sy=mleFC;57xWEBGYAxW=pWhJrT&!ZPWKXo!5vQx(e1r(clXkTZNnn?K1<#h7X{U5X ziTuXk&IZ-Zi`3>Yq<@79eT)f(p~4|3>E6P@IIP;T`t`6LVemTEQ1y_owdSxaF3vR2end)A$>_b)O&41fhHOB?!*zDD0{p#{uk_*Wa zSmYjeb$hl7%*{GYk0+J(T$-PA7iLv-J{j*cI?ei%6EV}16Mj9Jy`G#ne*E~c1aVkw zFnxB!FzCD;%Oz)%tmOKHlih@q-3022#b2LIZZ0e*3oSUr_ZsOnF@Ek)Qi;_ju!C8> z)T1Lt%9oqk0k+D{13Ig#Ri}rgWcg zEE@6qWxldfnj$4qVqrRsoblMZ@9D8e4?GH~cRu5g8jhCHq*F{ioZ4Ef)Lc2`?-oCZ z{a^5a{g*tYCketR$A#ih@d%XTUdZUq!fx z_C65tz%EA~VG@R>3oQwnE` zd8U(&nrqkVtvuJ${S|hL)A%V@SiM>4W<6=j*ljznguG9Bv9{~Ayw+3wwRYz02~hujYwVxDQF;yGeIo7>1|%OT@~& z@t$z)Eznw}*^^`-Wh|Yh8h3uVh0i)7`p|y203}^x z9YI3P5s1P*WSTaV$U?|rE3re4`CePub@sjNwo9)?9Y+Y<%0@vE<~j|JX{L&SEF zF!psXYIBT==`q|w6q}x$oFv)#$~+Xt*i~byo&kt)(7rs54IAS)-R5zCNO2vo#JgQ6 z-5@M2E6}8s-cztb;N^1fl_%!Y1H8T5x~6kcExDL!oh0C0XgzC2`XvNBCthkIMMnV9 z6oE{WbT_4Pby{EaUZQ)7wY~(=mJuJx?!DsGH9xL>S~{UX65Wys%5|R10-sk)=XHkK zLR2oY+t7&;izq7NUT@v#mWW!xL6XvXb9O@!_%*P7q$xGgen-4Db+<%BP?pQq+?D^e znzl3w9$80_VU8XNu=~U)DaAX5~npuJF**(sKPDRXdu2g^y$CF|f2~vtIxDO*!J{nyxChjSP z+jS(nNHjJq1vTZNHdaQ5R=(>sp>9nfa9=#CZVAPQ;y(BxgJoV3(qOc5m6a1?wT?t1 zHbiR>lv#hF92@eki_NpDbrY8jnY-~9#>dEFY*F?}8O_oNF=a_&mYel*x%7EnqdcOl ztm*=aJotgqCzQf$#HFwiWvCaJ5Y$Sm%3(Z}=;^S6?~Mfa77aBTYdLkv&JwXE?OKN2hu!lZ*<^iq|}|1R9j$5KRoT>67*T zu_?F(_KcK`7IC5a*bn19Wdw7<+jjin^q6VDL=>q$oc0cK5(|~If;=cKALhJ2wAMNWfrzO^5VR6XcEqUDT1Sksh=93XP-*DFLD; z6uJw34LLf;`&}T_Lue645eC4i<=awQ)7x@C53C))WOp%;{MY6zGNOX~f@zjzAeNYH zSoTIrSY)AxXy0wmboR@77ER)iIpng=-4XH}dPA-a#RVDJ8HAAp12P zDAe7ku_>iI#$LR^Q%O~N9lUwNozQ!4e($zV+NOD*ZoH%ZnE7Cq2ID?fa~fy&Nc%kY zNPuVKuT--J7i$ZP9|4FqX|eF9tQHu^jBV}>`JVy6%|JQ%k-(=x$rt>>A_gjV0^SxX znfl1bPc49f?*u?Ci#Osn`p8z`8=d((15T6sX7i(fu5WT_V6g|@xR^ZCa4}J-NS(Q< zJgSpNVHu;%E%Yl!1OwVuOcK`;qhL;A6b~o6PuEg)D`TiEUFtGau2IdZMln=bL??^L z+JvEUZDy#s#Y8x9^N}V zI6O2w5Pzx^F!t1zF$R@p9%=^q6encKYmP^~t;E~qgCcQJJUNS;_j++h9q{mrjpN}$KyIl zz47qVolTdRje3+EM&hy}c(n6EVRNA)0pzQdelz*1lJrmN;g|SL{~;yd=%QneTyhZl zTkN4TD9@Zh2|0rj!u%3$33WViIDHnSnb0%mPJA6WbPficIMFenZy)Q*KYn@CE#u|L$uLj8ar_NYF^?9;;-o+b5zJQa9R zL-&OGy?v^YLw@b5M(*<~9w$*#g6ikE58Oa>!VigDH}E+5fT{oszY~TX;BYkI0TA&o z;Q>H#DB%HcqeBTmpK=VKpZ3EsIgTg~5CYQ2^(+V<(+U1LP3!N2Gp(q68|z8CLqv@v z7QzURWmt)<88hPd8XeS?v@SABzRvyH;=46X$hn{neXCldY(V(=t}gDXJETOp2XpHx){V)pM(pPPi9tEU(K_ zDEp1{^7m@1El>|8p~-SeukdYs$T3=VFCxkSz6FfVQ^s04=gjSbx7!c`{!D zJfX+&aXC6+5&CULKhK|cGaAj#x(5_Yl+FUc$b4bBSxern*B7mOpIv5g)Z<9Cs?Ft; z(UaY8ZJ`<`B=$st`#6lTeAcyf)yGQa=NUZYJI$ND!D4mbBtmA?e96M1XUMI0=YJmi zU`#G$e~gO16_+qZ!Ho1xm37^ST|Ktl(d;Xg%-83yvf8`rx^FSRc>I<5&bV|F6R-_u zGb4es(#x+~!7(v8IU#G~r!ZT_>T|x)Vtui~*)(l;msQH*Nc!7;kx@5++HlMCQ5;l zxq2(JUL`+v6Xew3&-d?l*Z0^_j1P`Jrc?2Y>jtKQ1#FsmCe$d;Chh`I$i*wh9>6mS5D^#7JIL#5)hig(*jcD}U-R@b1uCLqoS!|jX z5OmKa)oWOzSLGpr$YhZi8qqRb#2|?w?^_*5H6KT1(|B~t{OSlc@s+jYsDYN9$k9iS z>W`B9BH!soPG9QJe5)5wk|th^Sh_At=4%=o_LtT`oWcxcDITMKIiPrm(xkp3@49TZ$=0MEv+_49 zoA_-({}wUJApQy{pwYr1uhD{L)mzndsjHRwR9oyrk+W>k^h2>J-kcwbyqd0Wag|om z1$mG4pjgekL^f1ujrRJCE4PWeI=?>8)pmWUMHGr@X2}IsWmB=X+Mc_*w)({Jk3Tcf z4akQD7S_aboon{glX$d+8{rd+9n$`-kL7oxkLADhjZFSj-$*$*e%P`0&*B-m-ly|| zDI9*RC|I?*7n-ZnEyuAn`kg=P?5qAshchOx(v%Mk zmM(K*{=k7kSUJg}sl-Lys-IS6f3mjv+S=;;O(89;s%fLvq=IGPyVZ+!hFlvNnZ8<| zYqVB#E_H1rl^SXq8}0tM#}wD!rG95qyE(@}&HS3JNvSj!;co^hd9@Ce1=)tV3NF=t zl7DE$qL}((+Gv6>mGSe+IKV?*fW8u=q{c#4$Gec#ibeKO++uah{BqQ~ESG+fysmd) zzEQ<7&hd-Ui=lmO!HuK81Pry)sVqD5at4Ir7O|?+-)i=kB$vW%6LqV)LBq8O!W)+O zIFd&qm0W4o7o#$lj$bpVHR5>m7&PYTyE};&t^bmuE?&EeAGM@%*V1MM&9TzvuPv0` z;%`;*4@pK;#d&_yA!3j!d|(d%L<)$zWZciC1t{S1XVeK%3^qL=y$;$yJYtmDzMiQ4 z5U8WABgEZnDys#RV5(wBTZrL!j3G4-C?@oI!CV?E?xuo@91zuYdwjFRob5n0m1ZdSU77Cz_!LW z{aGY@Q}Q!RDoNapiH~k@c?(4888z@SM})MBDF{xKI$5U6SF_jNksT)WZT8q~qpgD| zbCPegb=9Vrh_%uNXKJ`$nz+g#LzPxL%ax{?5plcxJF2FSPH<8XOHF3nIt|C;8z4BI zyj-bX>xYrULVryh_OW!M2A|t1$>Dbn_p#?NhSYqxTt3{bDC=81{lv2U^^u)Q%6*l@ zgCZzK=SvI%5O=^M%FrY`cfx78gV}gSXV%zHJF`a6J~YgnZSeEU6Z0X@SlX=kxpU_b zJ1op=_7-m*H!}z`eVk#wp<^JUo!|z6@j_xh(Xv}1yxQL<)v_UZ{Ai`T^3Ib-kM-vx zk~z6@^vR<$`m=)TY=81?tASfqb5EAlw4P3GtdktBmGoGzvJ-REnQ5ZihBvfi^4j^b zF;9OnZfNYNYWCF7F6wIUKT%-UNK0#Z)w_7-*A&CH9AkG9t4+oGKp1fWV4@{_@z~cOBbyYocNEy|GM7cNS;YH8h6UX$-ums6 zeOU%JX0gvl`%^Ye*jNiV6|pH*exaatFM;tf96*dJYy-$(B!sEJhZ)LWDe#O<$@|bd zlK77+2IdZApc`A2zo5Ych1$+xb^!b%HE&ce`*dNK1k)WXx4urA6at}~N_718HO@`x zHH(f~I!Gqhpl9{g@pK=lPWLQcey zK7ih&MdKWcE=hR7whPwwl$O~w*_{NXpH@*34=7(X5~aEJ0(8$52YCW#T6;^legs`n zi-a)Fwk^&Tx3x2{o}-bI8#WCkYZ-#yeO^whQ7c7!OZ91`3!Rp33YGx^YUzpd?@W@v zCstG5u;=$f4bpR@g{~55AMSu-c+<(DS}uhOckn*Mlbo*i=D)?AgG$|v0flGoQBPvD zgm{!1J;HHT{zR|COs@y2RM1nAz$1;|38bgR5rNCqbfO3HbBhyaisaWV%E`Fe2kta4 zcRM!Xfq{{gIb{jAhU@GtvJLB%HZq$H zV@bx-?HfyeI~CvWlKCUhpnX>3M%C+edjEeygvu#G6n&f$^;@0yg0tvSdf}1=&s!3r zg{ru0%YfoB6GlIw)5Y?D zN!4bhmhI{aHRP@W6=(4dY6**$@rnn8m!Y1w%0iYUNs?EkhBIDz;lVELBBHd9pl{e2 z=ja@;U-9Z5_g0yUigveqREFPB+O4O(3V7?=?sqYoo2dw$5aR#(RSo8D)y!ZHV*sXb zhj)6Fq#($)^XsAfQDQ{(O}rUU$R*TIM2{~S^Hx1e zu}g;x0P9dKn#}{+t#BgDkveZ{w^-zVUI%Rta&u6WaMo?1{V`o1K`q>dsBz{swy`~2 z-=SvqQuXzn!pPz-VIv1af~Gjj>-Ps@y3BA@cvl7;1=~|a{OdGj`@|)+uNF~C>lHqX zD>YnU4>T{e-m4*Xue{&(vOX`A{)jMgQiD86AJAj8Y`ZfpQ<~L>(={_0c*U*QGi+M6 zH>(fV)|3=ul<~^LwHe2V|L$I;rqZG=oYU|cN9R#q)MYbiDqE!dHn-&AluOw%F~>Vp zazZtW1A92_zRJ2FR!gE9@|6STB_20+^VM)zC&!wOso+|A&!5)Jn$4AB8*Ag*)TLig zAU2_tC^)R(CkSlK(@4uD3(zZ3#=!kf#FCL!8Og4c4ryM1tNpAT&fjw*l9G;pdo3XKudvj-}iX8iw_kCu)ZEGJWxC^bO?{AU8IR&;up(oBUE)=_*e8P{Z|A~kT2so zYdi}2JjL;hopkEUzN_bp-o2cEe$dA~cAa(sXE;opWtM%R@14YhnNce!mhmBi;bKR( zZ+hcx3Zpzhr)f{L3iK3>yZ^Z!IO`k)IcodH_YvN=_Pt|>GQxR{dO3x`zZqRiR1^^x z57fZc9s?td36&8un#FKoN-Q{~1n`CtQ+7>?eSS(KOraNWym&A1Uak_^y|ZkOhU^qE zBDx+4TASvcF_R(c7cf!Ul?$U*{!pYxZTckkzHS8jre#W-bqv+v6I2s>vN?PSytH7t zB{xLfSmv`U=hHb(eqgDucvvZc@j2++-iVw=v(%r>Q+?e4bu-9JfHE^<_$*+-{@?kK z?8Bx`vQFABOZ69#L&pEkyICGX7#!F^UZ@GKP?`TzNYi=E<4YGlI*!Y0cq&4Oj0WrC$;) zuw?Kc92urJ(*)>yxhjbtWOtF`guw*?NlI`O=LVm zel3gUcpofx+|S##qK`wdWOhjn;D_TLwvyxrJ<@&5JV&Yl$gXxxY|cMz`-I4bWSl_~ z>D*zmmX2sVJQU)yOIlT>M--Rn=~05XVr7LIeThDYgditkhzC{T zj3l2Q@VX_8io%b5;p;khfetAFQis41qY~CKh{4@tDm4nmhOUBbg>%8`P!_TPAJ?IQ z_qg1Jq;x^MzGR71VV4ehF6nG*~JY5jX!(r)k zfihy25rE990B3GB&2eG7THMbVF13nqEABEd&lSYHw)asVXVH-)O-v>QRSw2V!4%rnjLqV$kz9(SO)A+ z5o=Oj1#Ih@n8MpM+Jl0522;fS=o+Epq3F0l4SS003jgK>p=o4~(xE&U+DU z1N1kFU~eYp%`DSsJLbC2xw7f_!(tC)o*9DsH3ZOOdP(bf_tWA) zY-+Uq+`xu)*M4|_f+EbUIM3yCiomY+Iz!+E>Oo^aB>6B*E5~)LbrZRribBTrq>7UW zO!(tM6y8`HQjSb`>jng?F^4<{C2&V)fp}!Wqp{0q$MX?Mq4c8){#^w>rr<{i%;8tA zVhVCY7KSx*-m|*ZSDU@ku9~*R@o4qs+}IDA5Pb!S+Vc=sf#=!XNk{DLW^YH%b&dd{sUSc z;d_BBM%SD(#^;X3p75Nb^11G@F8W*i4Edvfs5p9TkC1G)Jr@Xb^c)>RUsg7s#Eu9# zYsR9KUUyR<_SWGix1{V&PDK4jl~+CUU|V7rse`Dn<7^0Q+FKIC5=G8EI-$c}$#9yy zT+?es!SjI9*~3#KUn-u;auwsZjywpfl~&nnCO?<0Us%4*Hqsq3-x;;B56K^iz%*3{ z#CFIRMm#p4+ljUnjum$gNW3R8pWGEk3PZf_fK-bt>*zJgQEWf1iwBGQOse-26W_=~ zEyIPKq;KQ!wTr(F$+s^$)`?(=yD-5z3>Yw!qBwnmEm1C?%ycZOiGIP>wqLcJ3kt$! zc7Bl=5URKI?-U?1A}_Z|Y|R&-YS2Fr01_d*UXcj(=YxE$EasKk5BGVK3aAB_Gy}sg z7=4KChU5PaL<)5@;3PLBJP!=gi_<`nnqrnq9QeRaLN6c>9zI+o>(*QWumES}*Oh5< zreyUNsyHeDXTkjg!1?UTwycLfl+4m~quK3FboO=W8J=9)Ubxh{9#$^J`ro=;{XgX@ z=ujNN{Lmx9;}k}M`uh?^-rRVra7n^FSd77(=CKzwK^E4E-6$3%;_IItAHYl@k>7H% zu_iC8-X$5*EEzUn%5wDVTww{0bX7V!2-8G9`VcEB3U=~K?lJ7ivLP8-i;TE1avlrU z-B@!CzBMV2CUdION&ar;T#?cdu5ZCfmLeso_AXK*UESW39vR7j^)m1FhMY2f?s3^pw|9Ed+E$bOCLKe#i< zgi7T-{854S>(u|n4WMl%PQT~*P5)DZxYkh9=)p2Dwm6iNV}ZmQO$Cu7WJVz}|CtX z(fvgqdadr5qDbWUg|;uGV}wa$Os^!Ffo7#`8*R()tKL`K6HT{6@<`J7H%M?S3rnV_ zlg7W&o6JcPn{mdH8T&kwSkhx`-sb8(KVSqNjX4m5&DBe! zomTKSNHuw&BLAhHyTokfPBR%rR7az#uKy7IG{YXG!UTou?yStcaCI^gcOrzZobPLSI9}+F)9}G)|Lg=>)S# z+91b977-E*AJEj`g;b-I&r*}CaUNasT#PU?afYU+t$P}Y(-E^oq0f1W#;Zuh^xJO1 z!*7$uc$e@N5^_yVl&guH-sL#Ek*{i41$-C~y&Qsq9)b_QFg`(4&*3~CK+9MNW6t#V zmGlh-zoFnCDENm8ep7*uKURq!)B>?yT<~MKh@mWpRpEX4_eGma^9b`5%yiSz1r>N* zfo9OhAIoM-Cy4Va7a7&t9xUtBIlH2qn!#^~F_kM_Lb@rmDArDB`Pi0*nP=oA=_J}hT2U$*o! zimPmf$SW^0DW6eXWnFjgtfv~XzpN#eF#$PjL`B%dC&yjNfPAH(%O?vk1InI0O>j{j zT50k|3K+-F(Dvq>_wyy(I1`GQu=HShQ5l44P zq{%3bpyVl6ThUOLTU|9#l<1=+b)B&5uW@5FSn=I5I@q`N?^|PN&F0};^ZZG?sD{B zgygfy;Dxa@uv!SK1#ltcK-{kj-mProjvtHJ5kd{`H# ztjqI|E>r2oFbbNB7Bb`R3~tHhw|_)5fOMD%<)Fg+sRG_vxs1mN58-yLcp^70tkVPe zydTZmLV`?%Yygg{Tf)*i9Bc5kB4*sXg1X6oEz8#iQ0!?#VO5C$oSR?NE{aMl1b4YP zl#M*^)^mXiU%2^x>=vd#d_8z6@ ztp=`o;Xz4HIO$&uaL)@jyv)&R{9Yk|JSDA$@S*KJsV!7?5nRyS<35k9!$EjCtOF_YB_a5FWG3G(=6~|UOrO{_-m}}vsAE` z3J3%N>d>v5NY(JN@6KV@$@+*1?++oFXrJBGIT)_rqsmdWj;Yn>Lo!Bnh1P@NwrNs! zzra=Q%A@BO8Nj}xwU?-d=<{S*!?1Io`V-smtv`hh7wSw3V0|d1{ku5;6nS|=O|SF- z;&aInD2#$)W#1c0CthG;eD|LG2 zHQ&1E8{-?CWI;dXAnv=(--^iuE~!s+0{1On_|Y+xAWjGh%?|Z zZr7>R#EZuBu^a+jPw$L8k>t0IaQi`a{mcaS9&jH49J}tK!S~Q?!$f{fj&5T}bq!2w zE>>3K6EEb<-0fa$&xz&!wA}jiP;zGHBdnm40s-6A(*e5iJN_q98&om(kt%hh_KjQOCUa+s_0 z+o-<#?b>h`F4eQ^RU$jzm`!{O&@uTyRt*>loEale!YvWHR^lq(yU0^@B@5o4c8m5` zVzY~0vMV#P@6Y%G2{BkBQ8T2o=8+@O^Gnf3zz@>Zh@IIW8?t&RdR^zYTs(>_Pr98b zu1*w9?*^mbNZ(u|4uMF3Ia$*K-H=#O#^#?R6JFGb6s;JPP{yDjiAsTbFNs@|Lf`dUh=tF^i0Bj4O6*xW?5S*_Bazz&uT_ydWm{ zg^AfF_jb&z!m}gm!cG4*`t)bZ+IQ8!l^az=D13{$H8-<5SI4Ej)8+yapFik$E#<6R zH~T$D_56GA1xu2r^SrBzWZdwi=Drgf*MoRxr`QN54tD#G3H*nbuB%Zd7NzQt@-}BJ~q~a7$cm9GaHWwqEy1V=Y^3h3iJBwm9#T8rtt*@oqD;Xk$IJQr)T zCoh7k#_y-74{cMQohqMyb257WJV9(cxLFt~XQ0wO3M`C#RH=s*ys98k@Phj zp_Y`cbe|$6-DdUP@B_r6=6zaiE~NEn&{|bixQ{5?bn1I;{2)qaRk03nQBKOoAM5j; z?snxpKKcO}i7nRW7jD$EM;Z2vYivCknjOWsQcu63&``h{APkmW;2)X_G5^ypcF0Mw(f+(&N zy`f%pF@wgJ^0-98V&hv?2frIV>d`(gVw zj8r)7j-s@doEH<9!;nJJBTd$2!HP_bOoLgP@LyCeTINzIjd0Yrqv{H>TGLH%?c51 z5^~ffgv*2N4V&W?pH_R7en`vr&nfs}1xSRkTT`G#Rr>ez)Fy~x>Yvc7_D>XAxUBQ3 zg73)m)owJt$UGVYgu;`(4r_V4!_t$`8TwrAlkjG;TzVcYS)%Dqt*7S}rr99Tv8tpb zsm(?ijC3e>+Z~Bh-oVapB{DsKPQB6=usV%J$@k-F#fk%MO84yr9+<|8r?k!C3;3D7 zXimfwACwTqhciY1;miBvMI*|y^2Fle+^X6$rS;PBduV%AqXWCh)AAub<@(?73!r#_p*A zpzDVE_$8}LkM#3N*{6G@G>hIul2P2(FOe>WJy`^buVn zF@5XSnOo<$PGV!qF|)MvNw`GS^(V zimjkrgzVJe#U?u@?s?E+7zW$kS~iqwtPnjNYYB z6GD(85z#b49qPLiB@*p~$Vjx+h<7I{-g+D7_$SxBoxIzw+R7QeUpJ+~KE^iwk@~C& z2!(N(%(2IVPU9LEFOtuYfnLzf)0N1=_#!Q~Ac&h`ki+{4J#b-G{67W42FEM3c?d?h zbI4+`Ci*{=Z*t~;%p9H}mlAn?pShpx!`dt}NC5vS6uE>Jqivr%<=rp8aA48s{=Aka z929kq!9t0&Kroe^N(MxIWtxmN@5)(4P0l=%e>52vI;v59|EJAk@h(-XltEH=Q8ifA zU^m0RYg?tPzS#K3qWG09!4@D&q=^SK*Xjh0Fc&D}0gw~rrwanp7QP$6^yp~qygjG^ z+TtBYe~3py3oMf}_=cmV@YP>r5qL{C3zt@o#tuDTSwBKpC?0)sCwRn=+Btu`AOjs~ zaQ-9R)1zUdv6&+E@p_P6#3vFC9Fzc^(b^VPKBqoS&U{9dD{6aKd0rCo$(hpuF}833 z5`8N_+E3?|p}lbP$lU=_Lx_Sz!?8UqZ7yCS*>Bsz(*F%q2IgZxw)CroOC$)!pRr)p zM&Li0uOFcKfe6i-p+N-f+bU>d3_1Q7Z`9TQhQ38>j9XP1AnB9X>;~}GKOz*CUMhd(z4?Evz?C8KnP1?gV{)R(SwQ(hBI0p`g*s{ zNs%41V`1qG=qCyf<@x_+!*|JYx!XmqS8gGK>Zm9XBzDWus<p#UIo9Ik_@?F?Tsx(MUTju$QA8lSi>l#RS5$9i5(*df zWk|Akv-rVRR++RyG}Vkx@_NEP?Y&k^iJ3%{8-9#Q%GviSNIMJfGSZd~#BWiSIU{xM zmh`CMz5)I{5uL9R7m6dr1CZIwu!|>oz37lW%Dq(hzYwEq1qS&p;t0<>_^T8h#m9)> zR=AI+9i(k{Ugh6`M@;;VqCWbim?t1@`y7|Y8hWPTSd-j55cO6}rFUO82!)W(yd zjV6RY%meEK=n{dkjYRp0E{w7lb%aX@9F7Bj^#iO)G79gUt7p9A|GU9vzD(oxbj z*FnUmGyI`2%jzJU&KYq#n;_M=Sbd+l9lv3xJOA zkM~HpF91Ac%j!(r(RCe(!!?cD*e7 z#(0sO7ncR4!ZJd+5y7UVUsG|f?_bxG7YJGI<~*v2Mm-tV!WpfFM^qyyBjIXTlBeGnfAC>R zPd|JI^UDfgF0}+6jAyok*%DxC#invSli*(XZ!8LQQscHY>(N?FaOV z_^C+1#?_>8O7hbiY6#mt4Vu z;v?~H$~dix1txVvM-KT17{y~m)<@H3ZQGr_%&{ZQti_1OXGZI;+#LA zliWhjd?Wk#O3TmgmGX00TVF*JQ9X?TEb7vCVqtFk^abFL#^w#uZ9u4EtBM9bUK8H; zshs@C-?hDsI_xNE{%@JM$aXPprJvO*)KZ97pa}PKLulK)SE`A<+a)RvBQBA;mZw&1 z7}4THk`))DuntLaF&+SxxENV;EI1H77pk$n+rM4ZUXNsgcrRG;DCs4W2xtdb0RH09 zJT(^lobe2)oc2iNB#2|ISr1;>{TY|?LpG*ET&ts(u*8Twg|>3k!Y$~ZsO8C--xQX~ znvRwKme^Eq4#bPI#h6}zEPk`rHx6N>m1lr%fvXUgb6DXVA)M8X;j7adVvxXhW1@^1 zZi*B4XJxCSO86`_=0p9J_%mSZV*E+47^71OkU%WQWCpbO^bbKGuRdbUw|Hfl**L{N2Np2qd^my^k`^1?mvYDSQsyEyrtU1)JRY=hJ z{F(dOdd^46$gX^^Ghz&+4EK{!*Bw2OWyF$;TCjUlViF^DuG{vYf)1_A#XSPw@E#Xvs<5sTYiCq`=5;1i|#{>Mz1>S7bda-$K^CL`Ht>B>D*kDc@&>_>qMjO^K>CPf4!oo*A=|# zI3_IWBL2Q!r9ZZjwxz@fGfUqf#%JlbsndsbpU92J)r?R2r8iX0HPn?~sJF>?7x3lQ zW805WF)ut{t#kc{@NrbX>(x%(cZ>1C;DIi)4MKXI=U~235@%gT>ihEuanFV?*-?CO zP?~K0=!)ju9A$SGN6>p)Q_cku>(bCt-$%*&;BU55n=X*5;nY>g=JcM{LtM);>j6*|HQkHlNqNo#Q746OfK9eOe zw;f54UA}i^JtVWF#yfjIoaW&zsh)WG$DVGMDtERX&;4UcV(>dQR`GTCPVDj_X-QIx zlFpsjl!ownG*&vEeNE1enRvukC9)ADfKYjlUBptqz4K{;2YbxMM#B)Bb} z;YLV}#rzHbX+~oaSq(L+rFn)h7G0E$M_T$mjj6Cw>8OIU3VP?ADZO4+Fr%QMKntSu zDFyaZSWpGOqgSu_*Yt|8I+T_a{Dgv^RPYxSSWRqw-+!XlpHblBlz36^4wNUjM{t{s zR|0Q*eF0^lR^$R~jTSq9xd)RQZ?2VoSrv!@irl-I`LFBsuPLzD%Ur(L`gmIV$9fV2 zRr>E0{7(wLOr^aoG+cq=uH4euU3F#5`9@&5%KRS10m From d6ec574fa93a0226f8ffb0e8aac3dd04c0a8447a Mon Sep 17 00:00:00 2001 From: meuns Date: Sun, 6 Mar 2016 21:49:53 +0100 Subject: [PATCH 08/78] leo dorst book chapter 2 drills --- tests/test_chapter2.py | 79 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 79 insertions(+) create mode 100755 tests/test_chapter2.py diff --git a/tests/test_chapter2.py b/tests/test_chapter2.py new file mode 100755 index 00000000..a881f3a1 --- /dev/null +++ b/tests/test_chapter2.py @@ -0,0 +1,79 @@ +import sys +sys.path.append('../galgebra') + +import unittest + +from sympy import Symbol, solve +from ga import Ga + +class TestChapter2(unittest.TestCase): + + def test_2_12_1_1(self): + """ + Compute the outer products of the following 3-space expressions, + giving the result relative to the basis {1, e_1, e_2, e_3, e_1^e_2, e_1^e_3, e_2^e_3, e_1^e_2^e_3}. + """ + (_g3d, e_1, e_2, e_3) = Ga.build('e*1|2|3') + self.assertTrue((e_1 + e_2) ^ (e_1 + e_3) == (-e_1 ^ e_2) + (e_1 ^ e_3) + (e_2 ^ e_3)) + self.assertTrue((e_1 + e_2 + e_3) ^ (2*e_1) == -2*(e_1 ^ e_2) - 2*(e_1 ^ e_3)) + self.assertTrue((e_1 - e_2) ^ (e_1 - e_3) == (e_1 ^ e_2) - (e_1 ^ e_3) + (e_2 ^ e_3)) + self.assertTrue((e_1 + e_2) ^ (0.5*e_1 + 2*e_2 + 3*e_3) == 1.5*(e_1 ^ e_2) + 3*(e_1 ^ e_3) + 3*(e_2 ^ e_3)) + self.assertTrue((e_1 ^ e_2) ^ (e_1 + e_3) == (e_1 ^ e_2 ^ e_3)) + self.assertTrue((e_1 + e_2) ^ ((e_1 ^ e_2) + (e_2 ^ e_3)) == (e_1 ^ e_2 ^ e_3)) + + + def test2_12_1_2(self): + """ + Given the 2-blade B = e_1 ^ (e_2 - e_3) that represents a plane, + determine if each of the following vectors lies in that plane. + """ + (_g3d, e_1, e_2, e_3) = Ga.build('e*1|2|3') + B = e_1 ^ (e_2 - e_3) + self.assertTrue(e_1 ^ B == 0) + self.assertFalse((e_1 + e_2) ^ B == 0) + self.assertFalse((e_1 + e_2 + e_3) ^ B == 0) + self.assertTrue((2*e_1 - e_2 + e_3) ^ B == 0) + + + def test2_12_1_3(self): + """ + What is the area of the parallelogram spanned by the vectors a = e_1 + 2*e_2 + and b = -e_1 - e_2 (relative to the area of e_1 ^ e_2) ? + """ + (_g3d, e_1, e_2, _e_3) = Ga.build('e*1|2|3') + a = e_1 + 2*e_2 + b = -e_1 - e_2 + B = a ^ b + self.assertTrue(B == 1 * (e_1 ^ e_2)) + + + def test2_12_1_4(self): + """ + Compute the intersection of the non-homogeneous line L with position vector e_1 + and direction vector e_2, and the line M with position vector e_2 and direction vector + (e_1 + e_2), using 2-blades. + """ + (_g2d, e_1, e_2) = Ga.build('e*1|2') + + # x is defined in the basis {e_1, e_2} + a = Symbol('a') + b = Symbol('b') + x = a * e_1 + b * e_2 + + # x lies on L and M (eq. L == 0 and M == 0) + L = (x ^ e_2) - (e_1 ^ e_2) + M = (x ^ (e_1 + e_2)) - (e_2 ^ (e_1 + e_2)) + + # Solve the linear system + R = solve([L, M], a, b) + + # Replace symbols + x = x.subs(R) + + self.assertTrue(x == e_1 + 2*e_2) + + +if __name__ == '__main__': + + unittest.main() + From 0b9b78f2580aedec46af1d4674961d57d8c4a5d8 Mon Sep 17 00:00:00 2001 From: Sylvain Meunier Date: Thu, 10 Mar 2016 13:44:29 +0100 Subject: [PATCH 09/78] ignore eclipe project files --- .gitignore | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 .gitignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..97356bc7 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +.project +.pydevproject From 8cd317ba8b78567d9af6c51266b2dd51d799a741 Mon Sep 17 00:00:00 2001 From: Sylvain Meunier Date: Thu, 10 Mar 2016 13:52:02 +0100 Subject: [PATCH 10/78] leo dorst book chapter 2 exercices (in progress) --- tests/test_chapter2.py | 39 ++++++++++++++++++++++++++++++++++++++- 1 file changed, 38 insertions(+), 1 deletion(-) diff --git a/tests/test_chapter2.py b/tests/test_chapter2.py index a881f3a1..cfaba641 100755 --- a/tests/test_chapter2.py +++ b/tests/test_chapter2.py @@ -3,7 +3,7 @@ import unittest -from sympy import Symbol, solve +from sympy import Symbol, Matrix, solve, cos, sin from ga import Ga class TestChapter2(unittest.TestCase): @@ -73,6 +73,43 @@ def test2_12_1_4(self): self.assertTrue(x == e_1 + 2*e_2) + def test2_12_2_1(self): + """ + In R2 with Euclidean metric, choose an orthonormal basis {e_1, e_2} in the plane of a and b such that e1 is parallel to a. + Write x = a * e_1 and y = b * (cos(t) * e_1 + sin(t) * e_2), whete t is the angle from a to b. + Evaluate the outer product. What is the geometrical interpretation ? + """ + (_g2d, e_1, e_2) = Ga.build('e*1|2', g='1 0, 0 1') + + # TODO: use alpha, beta and theta instead of a, b and t (it crashes sympy) + a = Symbol('a') + b = Symbol('b') + t = Symbol('t') + x = a * e_1 + y = b * (cos(t) * e_1 + sin(t) * e_2) + B = x ^ y + self.assertTrue(B == (a * b * sin(t) * (e_1 ^ e_2))) + + # Retrieve the parallelogram area from the 2-vector + area = B.norm() + self.assertTrue(area == (a * b * sin(t))) + + # Compute the parallelogram area using the determinant + x = [a, 0] + y = [b * cos(t), b * sin(t)] + area = Matrix([x, y]).det() + self.assertTrue(area == (a * b * sin(t))) + + + def test2_12_2_2(self): + """ + Consider R4 with basis {e_1, e_2, e_3, e_4}. Show that the 2-vector B = (e_1 ^ e_2) + (e_3 ^ e_4) + is not a 2-blade (i.e., it cannot be written as the outer product of two vectors). + """ + + (g4d, e_1, e_2, e_3, e_4) = Ga.build('e*1|2|3|4') + + if __name__ == '__main__': unittest.main() From d9f0d7bd50d7d5157e9d29f47aee7d27d79636de Mon Sep 17 00:00:00 2001 From: Sylvain Meunier Date: Thu, 17 Mar 2016 13:33:04 +0100 Subject: [PATCH 11/78] leo dorst book chapter 2 exercices (in progress) --- tests/test_chapter2.py | 43 ++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 41 insertions(+), 2 deletions(-) diff --git a/tests/test_chapter2.py b/tests/test_chapter2.py index cfaba641..9ee8e745 100755 --- a/tests/test_chapter2.py +++ b/tests/test_chapter2.py @@ -3,7 +3,7 @@ import unittest -from sympy import Symbol, Matrix, solve, cos, sin +from sympy import Symbol, Matrix, solve, solve_poly_system, cos, sin from ga import Ga class TestChapter2(unittest.TestCase): @@ -107,7 +107,46 @@ def test2_12_2_2(self): is not a 2-blade (i.e., it cannot be written as the outer product of two vectors). """ - (g4d, e_1, e_2, e_3, e_4) = Ga.build('e*1|2|3|4') + (_g4d, e_1, e_2, e_3, e_4) = Ga.build('e*1|2|3|4') + + # B + B = (e_1 ^ e_2) + (e_3 ^ e_4) + + # C is the product of a and b vectors + a_1 = Symbol('a_1') + a_2 = Symbol('a_2') + a_3 = Symbol('a_3') + a_4 = Symbol('a_4') + a = a_1 * e_1 + a_2 * e_2 + a_3 * e_3 + a_4 * e_4 + + b_1 = Symbol('b_1') + b_2 = Symbol('b_2') + b_3 = Symbol('b_3') + b_4 = Symbol('b_4') + b = b_1 * e_1 + b_2 * e_2 + b_3 * e_3 + b_4 * e_4 + + C = a ^ b + + # other coefficients are null + blades = [ + e_1 ^ e_2, e_1 ^ e_3, e_1 ^ e_4, e_2 ^ e_3, e_2 ^ e_4, e_3 ^ e_4, + ] + + C_coefs = C.blade_coefs(blades) + B_coefs = B.blade_coefs(blades) + + # try to solve the system and show there is no solution + system = [ + (C_coef) - (B_coef) for C_coef, B_coef in zip(C_coefs, B_coefs) + ] + + unknowns = [ + a_1, a_2, a_3, a_4, b_1, b_2, b_3, b_4 + ] + + # TODO: use solve if sympy fix it + result = solve_poly_system(system, unknowns) + self.assertTrue(result is None) if __name__ == '__main__': From 1b1672a87a642b9f72217b512ddd07534d40176c Mon Sep 17 00:00:00 2001 From: Sylvain Meunier Date: Wed, 23 Mar 2016 12:31:07 +0100 Subject: [PATCH 12/78] leo dorst book chapter 2 exercices (in progress) --- tests/test_chapter2.py | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/tests/test_chapter2.py b/tests/test_chapter2.py index 9ee8e745..06b5b025 100755 --- a/tests/test_chapter2.py +++ b/tests/test_chapter2.py @@ -2,6 +2,7 @@ sys.path.append('../galgebra') import unittest +import itertools from sympy import Symbol, Matrix, solve, solve_poly_system, cos, sin from ga import Ga @@ -149,6 +150,34 @@ def test2_12_2_2(self): self.assertTrue(result is None) + def test2_12_2_9(self): + """ + Prove Ak ^ Bl = (-1**kl) Bl ^ Ak. + """ + + # very slow if bigger + dimension = 5 + + # create the vector space + # e*1|2|3|4|5 if dimension == 5 + # TODO: don't fix the vector space dimension + ga = Ga('e*%s' % '|'.join(str(i) for i in range(1, dimension+1))) + + # an helper for building multivectors + # Ak = a1 ^ a2 ^ ... ^ ak if _build('a', k) + def _build(name, grade): + M = ga.mv('%s1' % name, 'vector') + for k in range(2, grade+1): + M = M ^ ga.mv('%s%d' % (name, k), 'vector') + return M + + # prove for k in [1, 5] and l in [1, 5] + for k, l in itertools.product(range(1, dimension+1), range(1, dimension+1)): + Ak = _build('a', k) + Bl = _build('b', l) + self.assertTrue((Ak ^ Bl) == (-1)**(k*l) * (Bl ^ Ak)) + + if __name__ == '__main__': unittest.main() From 2f4aa45b006d9a189f4cb6fa92d5d8353136a177 Mon Sep 17 00:00:00 2001 From: Sylvain Meunier Date: Sun, 27 Mar 2016 23:21:31 +0200 Subject: [PATCH 13/78] cleanup path hack --- tests/test_chapter2.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/tests/test_chapter2.py b/tests/test_chapter2.py index 06b5b025..51e3f3ed 100755 --- a/tests/test_chapter2.py +++ b/tests/test_chapter2.py @@ -1,6 +1,3 @@ -import sys -sys.path.append('../galgebra') - import unittest import itertools From 539ef47eaecd29c96079f98db159e5e71e31872a Mon Sep 17 00:00:00 2001 From: Sylvain Meunier Date: Sun, 27 Mar 2016 23:25:51 +0200 Subject: [PATCH 14/78] leo dorst book chapter 3 exercices (in progress) --- tests/test_chapter3.py | 101 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 101 insertions(+) create mode 100644 tests/test_chapter3.py diff --git a/tests/test_chapter3.py b/tests/test_chapter3.py new file mode 100644 index 00000000..6962afd8 --- /dev/null +++ b/tests/test_chapter3.py @@ -0,0 +1,101 @@ +import unittest + +from itertools import product, izip, count +from sympy import Symbol +from ga import Ga + +class TestChapter3(unittest.TestCase): + + def test_3_2_2(self): + """ + Computing the contraction explicitly. + """ + + (_g3d, e_1, e_2, e_3) = Ga.build('e*1|2|3') + + A_s = Symbol('A_s') + A_1 = Symbol('A_1') + A_2 = Symbol('A_2') + A_3 = Symbol('A_3') + A_12 = Symbol('A_12') + A_13 = Symbol('A_13') + A_23 = Symbol('A_23') + A_123 = Symbol('A_123') + B_s = Symbol('B_s') + B_1 = Symbol('B_1') + B_2 = Symbol('B_2') + B_3 = Symbol('B_3') + B_12 = Symbol('B_12') + B_13 = Symbol('B_13') + B_23 = Symbol('B_23') + B_123 = Symbol('B_123') + C_s = Symbol('C_s') + C_1 = Symbol('C_1') + C_2 = Symbol('C_2') + C_3 = Symbol('C_3') + C_12 = Symbol('C_12') + C_13 = Symbol('C_13') + C_23 = Symbol('C_23') + C_123 = Symbol('C_123') + + A_by_grades = [ + _g3d.mv(A_s), + A_1 * e_1 + A_2 * e_2 + A_3 * e_3, + A_12 * (e_1 ^ e_2) + A_13 * (e_1 ^ e_3) + A_23 * (e_2 ^ e_3), + A_123 * (e_1 ^ e_2 ^ e_3), + ] + + B_by_grades = [ + _g3d.mv(B_s), + B_1 * e_1 + B_2 * e_2 + B_3 * e_3, + B_12 * (e_1 ^ e_2) + B_13 * (e_1 ^ e_3) + B_23 * (e_2 ^ e_3), + B_123 * (e_1 ^ e_2 ^ e_3), + ] + + C_by_grades = [ + _g3d.mv(C_s), + C_1 * e_1 + C_2 * e_2 + C_3 * e_3, + C_12 * (e_1 ^ e_2) + C_13 * (e_1 ^ e_3) + C_23 * (e_2 ^ e_3), + C_123 * (e_1 ^ e_2 ^ e_3), + ] + + self.assertTrue(A_by_grades[1].is_vector()) + self.assertTrue(B_by_grades[1].is_vector()) + self.assertTrue(C_by_grades[1].is_vector()) + + for grade, A in izip(count(), A_by_grades): + self.assertTrue(A.is_blade() and A.pure_grade() == grade) + + for grade, B in izip(count(), B_by_grades): + self.assertTrue(B.is_blade() and B.pure_grade() == grade) + + for grade, C in izip(count(), C_by_grades): + self.assertTrue(C.is_blade() and C.pure_grade() == grade) + + # scalar and blades of various grades + A = A_by_grades[0] + for B in B_by_grades: + self.assertTrue((A < B) == (A * B)) + + A = A_by_grades[0] + for B in B_by_grades: + self.assertTrue((B < A) == 0 if B.pure_grade() > 0 else B_s) + + # vectors + A = A_by_grades[1] + B = B_by_grades[1] + self.assertTrue((A < B) == (A | B)) + + # vector and the outer product of 2 blades of various grades (scalars, vectors, 2-vectors...) + A = A_by_grades[1] + for B, C in product(B_by_grades, C_by_grades): + self.assertTrue((A < (B ^ C)) == (((A < B) ^ C) + (-1)**B.pure_grade() * (B ^ (A < C)))) + + # vector and the outer product of 2 blades of various grades (scalars, vectors, 2-vectors...) + for A, B, C in product(A_by_grades, B_by_grades, C_by_grades): + self.assertTrue(((A ^ B) < C) == (A < (B < C))) + + +if __name__ == '__main__': + + unittest.main() From 4791c4db544915236dc0c5535f2d0d1e0e2f22d2 Mon Sep 17 00:00:00 2001 From: meuns Date: Sun, 6 Mar 2016 21:48:54 +0100 Subject: [PATCH 15/78] remove and ignore pyc files --- galgebra/.gitignore | 1 + galgebra/ga.pyc | Bin 56118 -> 0 bytes galgebra/lt.pyc | Bin 27352 -> 0 bytes galgebra/metric.pyc | Bin 19980 -> 0 bytes galgebra/mv.pyc | Bin 81380 -> 0 bytes galgebra/printer.pyc | Bin 43206 -> 0 bytes 6 files changed, 1 insertion(+) create mode 100644 galgebra/.gitignore delete mode 100644 galgebra/ga.pyc delete mode 100644 galgebra/lt.pyc delete mode 100644 galgebra/metric.pyc delete mode 100644 galgebra/mv.pyc delete mode 100644 galgebra/printer.pyc diff --git a/galgebra/.gitignore b/galgebra/.gitignore new file mode 100644 index 00000000..0d20b648 --- /dev/null +++ b/galgebra/.gitignore @@ -0,0 +1 @@ +*.pyc diff --git a/galgebra/ga.pyc b/galgebra/ga.pyc deleted file mode 100644 index f17da942f4e9952e9bb33823738a70b1c98fae28..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 56118 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 KLD%7l6$pbqG8e2Msx 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
>tKp&fI&Rzw`S2e&_c%ci{ipHuA}zocVM$WdFAC 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 From 053cead0e7e2552c5cc4ab16df3d335175f9fd7a Mon Sep 17 00:00:00 2001 From: meuns Date: Sun, 6 Mar 2016 21:49:53 +0100 Subject: [PATCH 16/78] leo dorst book chapter 2 drills --- tests/test_chapter2.py | 79 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 79 insertions(+) create mode 100755 tests/test_chapter2.py diff --git a/tests/test_chapter2.py b/tests/test_chapter2.py new file mode 100755 index 00000000..a881f3a1 --- /dev/null +++ b/tests/test_chapter2.py @@ -0,0 +1,79 @@ +import sys +sys.path.append('../galgebra') + +import unittest + +from sympy import Symbol, solve +from ga import Ga + +class TestChapter2(unittest.TestCase): + + def test_2_12_1_1(self): + """ + Compute the outer products of the following 3-space expressions, + giving the result relative to the basis {1, e_1, e_2, e_3, e_1^e_2, e_1^e_3, e_2^e_3, e_1^e_2^e_3}. + """ + (_g3d, e_1, e_2, e_3) = Ga.build('e*1|2|3') + self.assertTrue((e_1 + e_2) ^ (e_1 + e_3) == (-e_1 ^ e_2) + (e_1 ^ e_3) + (e_2 ^ e_3)) + self.assertTrue((e_1 + e_2 + e_3) ^ (2*e_1) == -2*(e_1 ^ e_2) - 2*(e_1 ^ e_3)) + self.assertTrue((e_1 - e_2) ^ (e_1 - e_3) == (e_1 ^ e_2) - (e_1 ^ e_3) + (e_2 ^ e_3)) + self.assertTrue((e_1 + e_2) ^ (0.5*e_1 + 2*e_2 + 3*e_3) == 1.5*(e_1 ^ e_2) + 3*(e_1 ^ e_3) + 3*(e_2 ^ e_3)) + self.assertTrue((e_1 ^ e_2) ^ (e_1 + e_3) == (e_1 ^ e_2 ^ e_3)) + self.assertTrue((e_1 + e_2) ^ ((e_1 ^ e_2) + (e_2 ^ e_3)) == (e_1 ^ e_2 ^ e_3)) + + + def test2_12_1_2(self): + """ + Given the 2-blade B = e_1 ^ (e_2 - e_3) that represents a plane, + determine if each of the following vectors lies in that plane. + """ + (_g3d, e_1, e_2, e_3) = Ga.build('e*1|2|3') + B = e_1 ^ (e_2 - e_3) + self.assertTrue(e_1 ^ B == 0) + self.assertFalse((e_1 + e_2) ^ B == 0) + self.assertFalse((e_1 + e_2 + e_3) ^ B == 0) + self.assertTrue((2*e_1 - e_2 + e_3) ^ B == 0) + + + def test2_12_1_3(self): + """ + What is the area of the parallelogram spanned by the vectors a = e_1 + 2*e_2 + and b = -e_1 - e_2 (relative to the area of e_1 ^ e_2) ? + """ + (_g3d, e_1, e_2, _e_3) = Ga.build('e*1|2|3') + a = e_1 + 2*e_2 + b = -e_1 - e_2 + B = a ^ b + self.assertTrue(B == 1 * (e_1 ^ e_2)) + + + def test2_12_1_4(self): + """ + Compute the intersection of the non-homogeneous line L with position vector e_1 + and direction vector e_2, and the line M with position vector e_2 and direction vector + (e_1 + e_2), using 2-blades. + """ + (_g2d, e_1, e_2) = Ga.build('e*1|2') + + # x is defined in the basis {e_1, e_2} + a = Symbol('a') + b = Symbol('b') + x = a * e_1 + b * e_2 + + # x lies on L and M (eq. L == 0 and M == 0) + L = (x ^ e_2) - (e_1 ^ e_2) + M = (x ^ (e_1 + e_2)) - (e_2 ^ (e_1 + e_2)) + + # Solve the linear system + R = solve([L, M], a, b) + + # Replace symbols + x = x.subs(R) + + self.assertTrue(x == e_1 + 2*e_2) + + +if __name__ == '__main__': + + unittest.main() + From c10a4c959ae551474ea1d9f926bad58905a296aa Mon Sep 17 00:00:00 2001 From: Sylvain Meunier Date: Thu, 10 Mar 2016 13:44:29 +0100 Subject: [PATCH 17/78] ignore eclipe project files --- .gitignore | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 .gitignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..97356bc7 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +.project +.pydevproject From f908814a53fb6eb52e0355c92ef30f5f1468ce06 Mon Sep 17 00:00:00 2001 From: Sylvain Meunier Date: Thu, 10 Mar 2016 13:52:02 +0100 Subject: [PATCH 18/78] leo dorst book chapter 2 exercices (in progress) --- tests/test_chapter2.py | 39 ++++++++++++++++++++++++++++++++++++++- 1 file changed, 38 insertions(+), 1 deletion(-) diff --git a/tests/test_chapter2.py b/tests/test_chapter2.py index a881f3a1..cfaba641 100755 --- a/tests/test_chapter2.py +++ b/tests/test_chapter2.py @@ -3,7 +3,7 @@ import unittest -from sympy import Symbol, solve +from sympy import Symbol, Matrix, solve, cos, sin from ga import Ga class TestChapter2(unittest.TestCase): @@ -73,6 +73,43 @@ def test2_12_1_4(self): self.assertTrue(x == e_1 + 2*e_2) + def test2_12_2_1(self): + """ + In R2 with Euclidean metric, choose an orthonormal basis {e_1, e_2} in the plane of a and b such that e1 is parallel to a. + Write x = a * e_1 and y = b * (cos(t) * e_1 + sin(t) * e_2), whete t is the angle from a to b. + Evaluate the outer product. What is the geometrical interpretation ? + """ + (_g2d, e_1, e_2) = Ga.build('e*1|2', g='1 0, 0 1') + + # TODO: use alpha, beta and theta instead of a, b and t (it crashes sympy) + a = Symbol('a') + b = Symbol('b') + t = Symbol('t') + x = a * e_1 + y = b * (cos(t) * e_1 + sin(t) * e_2) + B = x ^ y + self.assertTrue(B == (a * b * sin(t) * (e_1 ^ e_2))) + + # Retrieve the parallelogram area from the 2-vector + area = B.norm() + self.assertTrue(area == (a * b * sin(t))) + + # Compute the parallelogram area using the determinant + x = [a, 0] + y = [b * cos(t), b * sin(t)] + area = Matrix([x, y]).det() + self.assertTrue(area == (a * b * sin(t))) + + + def test2_12_2_2(self): + """ + Consider R4 with basis {e_1, e_2, e_3, e_4}. Show that the 2-vector B = (e_1 ^ e_2) + (e_3 ^ e_4) + is not a 2-blade (i.e., it cannot be written as the outer product of two vectors). + """ + + (g4d, e_1, e_2, e_3, e_4) = Ga.build('e*1|2|3|4') + + if __name__ == '__main__': unittest.main() From 93b9a835a8c9d567fed290fe56fc3a4010db2c24 Mon Sep 17 00:00:00 2001 From: Sylvain Meunier Date: Fri, 11 Mar 2016 14:06:21 +0100 Subject: [PATCH 19/78] fix and test Mv.blade_coefs method --- galgebra/mv.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/galgebra/mv.py b/galgebra/mv.py index 77e3cf68..9258583f 100755 --- a/galgebra/mv.py +++ b/galgebra/mv.py @@ -987,13 +987,12 @@ def blade_coefs(self, blade_lst=None): a list (sympy expressions) of the coefficients of each basis blade in blade_lst """ - if blade_lst is None: blade_lst = [self.Ga.mv(ONE)] + self.Ga.mv_blades_lst - - for blade in blade_lst: - if not blade.is_base() or not blade.is_blade(): - raise ValueError("%s expression isn't a basis blade" % blade) + else: + for blade in blade_lst: + if not blade.is_base() or not blade.is_blade(): + raise ValueError("%s expression isn't a basis blade" % blade) blade_lst = [x.obj for x in blade_lst] (coefs, bases) = metric.linear_expand(self.obj) coef_lst = [] From ff08a83dd4569d81ce2ca1a3ec389d5ce3bf6114 Mon Sep 17 00:00:00 2001 From: Sylvain Meunier Date: Thu, 17 Mar 2016 13:33:04 +0100 Subject: [PATCH 20/78] leo dorst book chapter 2 exercices (in progress) --- tests/test_chapter2.py | 43 ++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 41 insertions(+), 2 deletions(-) diff --git a/tests/test_chapter2.py b/tests/test_chapter2.py index cfaba641..9ee8e745 100755 --- a/tests/test_chapter2.py +++ b/tests/test_chapter2.py @@ -3,7 +3,7 @@ import unittest -from sympy import Symbol, Matrix, solve, cos, sin +from sympy import Symbol, Matrix, solve, solve_poly_system, cos, sin from ga import Ga class TestChapter2(unittest.TestCase): @@ -107,7 +107,46 @@ def test2_12_2_2(self): is not a 2-blade (i.e., it cannot be written as the outer product of two vectors). """ - (g4d, e_1, e_2, e_3, e_4) = Ga.build('e*1|2|3|4') + (_g4d, e_1, e_2, e_3, e_4) = Ga.build('e*1|2|3|4') + + # B + B = (e_1 ^ e_2) + (e_3 ^ e_4) + + # C is the product of a and b vectors + a_1 = Symbol('a_1') + a_2 = Symbol('a_2') + a_3 = Symbol('a_3') + a_4 = Symbol('a_4') + a = a_1 * e_1 + a_2 * e_2 + a_3 * e_3 + a_4 * e_4 + + b_1 = Symbol('b_1') + b_2 = Symbol('b_2') + b_3 = Symbol('b_3') + b_4 = Symbol('b_4') + b = b_1 * e_1 + b_2 * e_2 + b_3 * e_3 + b_4 * e_4 + + C = a ^ b + + # other coefficients are null + blades = [ + e_1 ^ e_2, e_1 ^ e_3, e_1 ^ e_4, e_2 ^ e_3, e_2 ^ e_4, e_3 ^ e_4, + ] + + C_coefs = C.blade_coefs(blades) + B_coefs = B.blade_coefs(blades) + + # try to solve the system and show there is no solution + system = [ + (C_coef) - (B_coef) for C_coef, B_coef in zip(C_coefs, B_coefs) + ] + + unknowns = [ + a_1, a_2, a_3, a_4, b_1, b_2, b_3, b_4 + ] + + # TODO: use solve if sympy fix it + result = solve_poly_system(system, unknowns) + self.assertTrue(result is None) if __name__ == '__main__': From a9341b4ada9b2f68f47a311844181303eccd4f02 Mon Sep 17 00:00:00 2001 From: Sylvain Meunier Date: Thu, 10 Mar 2016 13:52:02 +0100 Subject: [PATCH 21/78] leo dorst book chapter 2 exercices (in progress) --- tests/test_chapter2.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/test_chapter2.py b/tests/test_chapter2.py index 9ee8e745..6961e805 100755 --- a/tests/test_chapter2.py +++ b/tests/test_chapter2.py @@ -106,7 +106,6 @@ def test2_12_2_2(self): Consider R4 with basis {e_1, e_2, e_3, e_4}. Show that the 2-vector B = (e_1 ^ e_2) + (e_3 ^ e_4) is not a 2-blade (i.e., it cannot be written as the outer product of two vectors). """ - (_g4d, e_1, e_2, e_3, e_4) = Ga.build('e*1|2|3|4') # B From e46619ff93c2b1dc4cb92786c4d603d2b924a8f0 Mon Sep 17 00:00:00 2001 From: Sylvain Meunier Date: Wed, 23 Mar 2016 12:31:07 +0100 Subject: [PATCH 22/78] leo dorst book chapter 2 exercices (in progress) --- tests/test_chapter2.py | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/tests/test_chapter2.py b/tests/test_chapter2.py index 6961e805..ac11a079 100755 --- a/tests/test_chapter2.py +++ b/tests/test_chapter2.py @@ -2,6 +2,7 @@ sys.path.append('../galgebra') import unittest +import itertools from sympy import Symbol, Matrix, solve, solve_poly_system, cos, sin from ga import Ga @@ -148,6 +149,34 @@ def test2_12_2_2(self): self.assertTrue(result is None) + def test2_12_2_9(self): + """ + Prove Ak ^ Bl = (-1**kl) Bl ^ Ak. + """ + + # very slow if bigger + dimension = 5 + + # create the vector space + # e*1|2|3|4|5 if dimension == 5 + # TODO: don't fix the vector space dimension + ga = Ga('e*%s' % '|'.join(str(i) for i in range(1, dimension+1))) + + # an helper for building multivectors + # Ak = a1 ^ a2 ^ ... ^ ak if _build('a', k) + def _build(name, grade): + M = ga.mv('%s1' % name, 'vector') + for k in range(2, grade+1): + M = M ^ ga.mv('%s%d' % (name, k), 'vector') + return M + + # prove for k in [1, 5] and l in [1, 5] + for k, l in itertools.product(range(1, dimension+1), range(1, dimension+1)): + Ak = _build('a', k) + Bl = _build('b', l) + self.assertTrue((Ak ^ Bl) == (-1)**(k*l) * (Bl ^ Ak)) + + if __name__ == '__main__': unittest.main() From 96b43a180bbb6c0788fb4e295a1d476701f54731 Mon Sep 17 00:00:00 2001 From: Sylvain Meunier Date: Sun, 27 Mar 2016 23:21:31 +0200 Subject: [PATCH 23/78] cleanup path hack --- tests/test_chapter2.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/tests/test_chapter2.py b/tests/test_chapter2.py index ac11a079..21e9c7a9 100755 --- a/tests/test_chapter2.py +++ b/tests/test_chapter2.py @@ -1,6 +1,3 @@ -import sys -sys.path.append('../galgebra') - import unittest import itertools From 7923202723689edd53f5092853c15a653d236219 Mon Sep 17 00:00:00 2001 From: Sylvain Meunier Date: Sun, 27 Mar 2016 23:25:51 +0200 Subject: [PATCH 24/78] leo dorst book chapter 3 exercices (in progress) --- tests/test_chapter3.py | 101 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 101 insertions(+) create mode 100644 tests/test_chapter3.py diff --git a/tests/test_chapter3.py b/tests/test_chapter3.py new file mode 100644 index 00000000..6962afd8 --- /dev/null +++ b/tests/test_chapter3.py @@ -0,0 +1,101 @@ +import unittest + +from itertools import product, izip, count +from sympy import Symbol +from ga import Ga + +class TestChapter3(unittest.TestCase): + + def test_3_2_2(self): + """ + Computing the contraction explicitly. + """ + + (_g3d, e_1, e_2, e_3) = Ga.build('e*1|2|3') + + A_s = Symbol('A_s') + A_1 = Symbol('A_1') + A_2 = Symbol('A_2') + A_3 = Symbol('A_3') + A_12 = Symbol('A_12') + A_13 = Symbol('A_13') + A_23 = Symbol('A_23') + A_123 = Symbol('A_123') + B_s = Symbol('B_s') + B_1 = Symbol('B_1') + B_2 = Symbol('B_2') + B_3 = Symbol('B_3') + B_12 = Symbol('B_12') + B_13 = Symbol('B_13') + B_23 = Symbol('B_23') + B_123 = Symbol('B_123') + C_s = Symbol('C_s') + C_1 = Symbol('C_1') + C_2 = Symbol('C_2') + C_3 = Symbol('C_3') + C_12 = Symbol('C_12') + C_13 = Symbol('C_13') + C_23 = Symbol('C_23') + C_123 = Symbol('C_123') + + A_by_grades = [ + _g3d.mv(A_s), + A_1 * e_1 + A_2 * e_2 + A_3 * e_3, + A_12 * (e_1 ^ e_2) + A_13 * (e_1 ^ e_3) + A_23 * (e_2 ^ e_3), + A_123 * (e_1 ^ e_2 ^ e_3), + ] + + B_by_grades = [ + _g3d.mv(B_s), + B_1 * e_1 + B_2 * e_2 + B_3 * e_3, + B_12 * (e_1 ^ e_2) + B_13 * (e_1 ^ e_3) + B_23 * (e_2 ^ e_3), + B_123 * (e_1 ^ e_2 ^ e_3), + ] + + C_by_grades = [ + _g3d.mv(C_s), + C_1 * e_1 + C_2 * e_2 + C_3 * e_3, + C_12 * (e_1 ^ e_2) + C_13 * (e_1 ^ e_3) + C_23 * (e_2 ^ e_3), + C_123 * (e_1 ^ e_2 ^ e_3), + ] + + self.assertTrue(A_by_grades[1].is_vector()) + self.assertTrue(B_by_grades[1].is_vector()) + self.assertTrue(C_by_grades[1].is_vector()) + + for grade, A in izip(count(), A_by_grades): + self.assertTrue(A.is_blade() and A.pure_grade() == grade) + + for grade, B in izip(count(), B_by_grades): + self.assertTrue(B.is_blade() and B.pure_grade() == grade) + + for grade, C in izip(count(), C_by_grades): + self.assertTrue(C.is_blade() and C.pure_grade() == grade) + + # scalar and blades of various grades + A = A_by_grades[0] + for B in B_by_grades: + self.assertTrue((A < B) == (A * B)) + + A = A_by_grades[0] + for B in B_by_grades: + self.assertTrue((B < A) == 0 if B.pure_grade() > 0 else B_s) + + # vectors + A = A_by_grades[1] + B = B_by_grades[1] + self.assertTrue((A < B) == (A | B)) + + # vector and the outer product of 2 blades of various grades (scalars, vectors, 2-vectors...) + A = A_by_grades[1] + for B, C in product(B_by_grades, C_by_grades): + self.assertTrue((A < (B ^ C)) == (((A < B) ^ C) + (-1)**B.pure_grade() * (B ^ (A < C)))) + + # vector and the outer product of 2 blades of various grades (scalars, vectors, 2-vectors...) + for A, B, C in product(A_by_grades, B_by_grades, C_by_grades): + self.assertTrue(((A ^ B) < C) == (A < (B < C))) + + +if __name__ == '__main__': + + unittest.main() From 47564c4ca96760defe95acfcc3cf17fb171e929d Mon Sep 17 00:00:00 2001 From: Sylvain Meunier Date: Sat, 9 Apr 2016 23:35:03 +0200 Subject: [PATCH 25/78] add I_inv method to Ga class --- galgebra/ga.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/galgebra/ga.py b/galgebra/ga.py index d77a4538..bdb7fd60 100755 --- a/galgebra/ga.py +++ b/galgebra/ga.py @@ -397,6 +397,9 @@ def E(self): def I(self): return self.i + + def I_inv(self): + return self.i_inv def X(self): return self.mv(sum([coord*base for (coord, base) in zip(self.coords, self.basis)])) From 569a40c002009e09452192c42eeb863311f88fc8 Mon Sep 17 00:00:00 2001 From: Sylvain Meunier Date: Sat, 9 Apr 2016 23:39:23 +0200 Subject: [PATCH 26/78] remove Abs from Mv.norm2 method --- galgebra/mv.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/galgebra/mv.py b/galgebra/mv.py index 9258583f..63668a29 100755 --- a/galgebra/mv.py +++ b/galgebra/mv.py @@ -1211,7 +1211,7 @@ def norm2(self): reverse = self.rev() product = self * reverse if product.is_scalar(): - return Abs(product.scalar()) + return product.scalar() else: raise TypeError('"(' + str(product) + ')**2" is not a scalar in norm2.') From 5edaa640078711275e440ed83d736e79ef394749 Mon Sep 17 00:00:00 2001 From: Sylvain Meunier Date: Sat, 9 Apr 2016 23:40:18 +0200 Subject: [PATCH 27/78] leo dorst book chapter 3 exercices (in progress) --- tests/test_chapter3.py | 201 +++++++++++++++++++++++++---------------- 1 file changed, 123 insertions(+), 78 deletions(-) diff --git a/tests/test_chapter3.py b/tests/test_chapter3.py index 6962afd8..84711921 100644 --- a/tests/test_chapter3.py +++ b/tests/test_chapter3.py @@ -1,100 +1,145 @@ import unittest -from itertools import product, izip, count -from sympy import Symbol +from itertools import product +from sympy import simplify from ga import Ga +from mv import Mv + class TestChapter3(unittest.TestCase): + def assertEquals(self, first, second, msg=None): + """ + Compare two expressions are equals. + """ + + if isinstance(first, Mv): + first = first.obj + + if isinstance(second, Mv): + second = second.obj + + self.assertTrue(simplify(first - second) == 0, "%s == %s" % (first, second)) + + def test_3_2_2(self): """ Computing the contraction explicitly. """ - (_g3d, e_1, e_2, e_3) = Ga.build('e*1|2|3') - - A_s = Symbol('A_s') - A_1 = Symbol('A_1') - A_2 = Symbol('A_2') - A_3 = Symbol('A_3') - A_12 = Symbol('A_12') - A_13 = Symbol('A_13') - A_23 = Symbol('A_23') - A_123 = Symbol('A_123') - B_s = Symbol('B_s') - B_1 = Symbol('B_1') - B_2 = Symbol('B_2') - B_3 = Symbol('B_3') - B_12 = Symbol('B_12') - B_13 = Symbol('B_13') - B_23 = Symbol('B_23') - B_123 = Symbol('B_123') - C_s = Symbol('C_s') - C_1 = Symbol('C_1') - C_2 = Symbol('C_2') - C_3 = Symbol('C_3') - C_12 = Symbol('C_12') - C_13 = Symbol('C_13') - C_23 = Symbol('C_23') - C_123 = Symbol('C_123') - - A_by_grades = [ - _g3d.mv(A_s), - A_1 * e_1 + A_2 * e_2 + A_3 * e_3, - A_12 * (e_1 ^ e_2) + A_13 * (e_1 ^ e_3) + A_23 * (e_2 ^ e_3), - A_123 * (e_1 ^ e_2 ^ e_3), - ] - - B_by_grades = [ - _g3d.mv(B_s), - B_1 * e_1 + B_2 * e_2 + B_3 * e_3, - B_12 * (e_1 ^ e_2) + B_13 * (e_1 ^ e_3) + B_23 * (e_2 ^ e_3), - B_123 * (e_1 ^ e_2 ^ e_3), - ] - - C_by_grades = [ - _g3d.mv(C_s), - C_1 * e_1 + C_2 * e_2 + C_3 * e_3, - C_12 * (e_1 ^ e_2) + C_13 * (e_1 ^ e_3) + C_23 * (e_2 ^ e_3), - C_123 * (e_1 ^ e_2 ^ e_3), - ] - - self.assertTrue(A_by_grades[1].is_vector()) - self.assertTrue(B_by_grades[1].is_vector()) - self.assertTrue(C_by_grades[1].is_vector()) - - for grade, A in izip(count(), A_by_grades): - self.assertTrue(A.is_blade() and A.pure_grade() == grade) - - for grade, B in izip(count(), B_by_grades): - self.assertTrue(B.is_blade() and B.pure_grade() == grade) - - for grade, C in izip(count(), C_by_grades): - self.assertTrue(C.is_blade() and C.pure_grade() == grade) + Ga.dual_mode("Iinv+") + + R = Ga('e*1|2|3') + A_blades = [Mv('A', i, 'grade', ga=R) for i in range(R.n + 1)] + B_blades = [Mv('B', i, 'grade', ga=R) for i in range(R.n + 1)] + C_blades = [Mv('C', i, 'grade', ga=R) for i in range(R.n + 1)] # scalar and blades of various grades - A = A_by_grades[0] - for B in B_by_grades: - self.assertTrue((A < B) == (A * B)) - - A = A_by_grades[0] - for B in B_by_grades: - self.assertTrue((B < A) == 0 if B.pure_grade() > 0 else B_s) + A = A_blades[0] + for B in B_blades: + self.assertEquals(A < B, A * B) + + A = A_blades[0] + for B in B_blades: + self.assertEquals(B < A, 0 if B.pure_grade() > 0 else A * B) # vectors - A = A_by_grades[1] - B = B_by_grades[1] - self.assertTrue((A < B) == (A | B)) + A = A_blades[1] + B = B_blades[1] + self.assertEquals(A < B, A | B) # vector and the outer product of 2 blades of various grades (scalars, vectors, 2-vectors...) - A = A_by_grades[1] - for B, C in product(B_by_grades, C_by_grades): - self.assertTrue((A < (B ^ C)) == (((A < B) ^ C) + (-1)**B.pure_grade() * (B ^ (A < C)))) - + A = A_blades[1] + for B, C in product(B_blades, C_blades): + self.assertTrue(A < (B ^ C), ((A < B) ^ C) + (-1)**B.pure_grade() * (B ^ (A < C))) + # vector and the outer product of 2 blades of various grades (scalars, vectors, 2-vectors...) - for A, B, C in product(A_by_grades, B_by_grades, C_by_grades): - self.assertTrue(((A ^ B) < C) == (A < (B < C))) + for A, B, C in product(A_blades, B_blades, C_blades): + self.assertTrue((A ^ B) < C, A < (B < C)) + + + def test_3_5_2(self): + """ + The inverse of a blade. + """ + + Ga.dual_mode("Iinv+") + + R = Ga('e*1|2|3') + A_blades = [Mv('A', i, 'grade', ga=R) for i in range(R.n + 1)] + + for A in A_blades: + self.assertEquals(A.inv(), ((-1) ** (A.pure_grade() * (A.pure_grade() - 1) / 2)) * (A / A.norm2())) + + for A in A_blades: + self.assertEquals(A < A.inv(), 1) + + A = A_blades[1] + self.assertEquals(A.inv(), A / A.norm2()) + + def test_3_5_3(self): + """ + Orthogonal complement and duality. + """ + + Ga.dual_mode("Iinv+") + + # some blades by grades for each space + spaces = [([Mv('A', i, 'grade', ga=R) for i in range(R.n + 1)], R) for R in [Ga('e*1|2'), Ga('e*1|2|3'), Ga('e*1|2|3|4')]] + + # dualization + for blades, R in spaces: + for A in blades: + self.assertEquals(A.dual(), A < R.I_inv()) + + # dualization sign + for blades, R in spaces: + for A in blades: + self.assertEquals(A.dual().dual(), ((-1) ** (R.n * (R.n - 1) / 2)) * A) + + # undualization + for blades, R in spaces: + for A in blades: + self.assertEquals(A, A.dual() < R.I()) + + + def test_3_5_4(self): + """ + The duality relationships. + """ + + Ga.dual_mode("Iinv+") + + R = Ga('e*1|2|3') + A_blades = [Mv('A', i, 'grade', ga=R) for i in range(R.n + 1)] + B_blades = [Mv('B', i, 'grade', ga=R) for i in range(R.n + 1)] + + for A, B in product(A_blades, B_blades): + self.assertEquals((A ^ B).dual(), A < B.dual()) + + for A, B in product(A_blades, B_blades): + self.assertEquals((A < B).dual(), A ^ B.dual()) + + + def test_3_6(self): + """ + Orthogonal projection of subspaces. + """ + + Ga.dual_mode("Iinv+") + + R = Ga('e*1|2|3') + A_blades = [Mv('A', i, 'grade', ga=R) for i in range(R.n + 1)] + B_blades = [Mv('B', i, 'grade', ga=R) for i in range(R.n + 1)] + + def P(A, B): + return (A < B) < B.inv() + + # a projection should be idempotent + for A, B in product(A_blades, B_blades): + self.assertEquals(P(A, B), P(P(A, B), B)) + if __name__ == '__main__': From 53f7af8b8c428a13b37bf2c82fd31a9fb546655f Mon Sep 17 00:00:00 2001 From: Sylvain Meunier Date: Mon, 11 Apr 2016 14:11:19 +0200 Subject: [PATCH 28/78] leo dorst book chapter 3 exercices (in progress) --- tests/test_chapter3.py | 147 ++++++++++++++++++++++++++--------------- 1 file changed, 94 insertions(+), 53 deletions(-) diff --git a/tests/test_chapter3.py b/tests/test_chapter3.py index 84711921..ec0706ca 100644 --- a/tests/test_chapter3.py +++ b/tests/test_chapter3.py @@ -1,145 +1,186 @@ import unittest from itertools import product -from sympy import simplify +from sympy import simplify, Symbol from ga import Ga from mv import Mv class TestChapter3(unittest.TestCase): - + def assertEquals(self, first, second, msg=None): """ Compare two expressions are equals. """ - + if isinstance(first, Mv): - first = first.obj - + first = first.obj + if isinstance(second, Mv): second = second.obj - + self.assertTrue(simplify(first - second) == 0, "%s == %s" % (first, second)) - - + + def test_3_2_2(self): """ Computing the contraction explicitly. """ - + Ga.dual_mode("Iinv+") - + R = Ga('e*1|2|3') A_blades = [Mv('A', i, 'grade', ga=R) for i in range(R.n + 1)] B_blades = [Mv('B', i, 'grade', ga=R) for i in range(R.n + 1)] C_blades = [Mv('C', i, 'grade', ga=R) for i in range(R.n + 1)] - + # scalar and blades of various grades A = A_blades[0] for B in B_blades: self.assertEquals(A < B, A * B) - + A = A_blades[0] for B in B_blades: self.assertEquals(B < A, 0 if B.pure_grade() > 0 else A * B) - + # vectors A = A_blades[1] B = B_blades[1] self.assertEquals(A < B, A | B) - + # vector and the outer product of 2 blades of various grades (scalars, vectors, 2-vectors...) A = A_blades[1] for B, C in product(B_blades, C_blades): - self.assertTrue(A < (B ^ C), ((A < B) ^ C) + (-1)**B.pure_grade() * (B ^ (A < C))) - + self.assertEquals(A < (B ^ C), ((A < B) ^ C) + (-1)**B.pure_grade() * (B ^ (A < C))) + # vector and the outer product of 2 blades of various grades (scalars, vectors, 2-vectors...) for A, B, C in product(A_blades, B_blades, C_blades): - self.assertTrue((A ^ B) < C, A < (B < C)) - - + self.assertEquals((A ^ B) < C, A < (B < C)) + + # distributive properties + for A, B, C in product(A_blades, B_blades, C_blades): + self.assertEquals((A + B) < C, (A < C) + (B < C)) + + for A, B, C in product(A_blades, B_blades, C_blades): + self.assertEquals(A < (B + C), (A < B) + (A < C)) + + alpha = Symbol("alpha") + for A, B in product(A_blades, B_blades): + self.assertEquals((alpha * A) < B, alpha * (A < B)) + self.assertEquals((alpha * A) < B, A < (alpha * B)) + + a = Mv('a', 1, 'grade', ga=R) + for A_minus1, B in product(A_blades[:-1], B_blades): + A = A_minus1 ^ a + self.assertEquals(A < B, (A_minus1 ^ a) < B) + self.assertEquals(A < B, A_minus1 < (a < B)) + + + def test_3_4(self): + """ + The other contraction. + """ + + Ga.dual_mode("Iinv+") + + R = Ga('e*1|2|3') + A_blades = [Mv('A', i, 'grade', ga=R) for i in range(R.n + 1)] + B_blades = [Mv('B', i, 'grade', ga=R) for i in range(R.n + 1)] + + for A, B in product(A_blades, B_blades): + self.assertEquals(B > A, ((-1) ** (A.pure_grade() * (B.pure_grade() - 1))) * (A < B)) + + #for A, B in product(A_blades, B_blades): + # self.assertEquals((B > A).pure_grade(), B.pure_grade() - A.pure_grade()) + + def test_3_5_2(self): """ The inverse of a blade. """ - + Ga.dual_mode("Iinv+") - + R = Ga('e*1|2|3') A_blades = [Mv('A', i, 'grade', ga=R) for i in range(R.n + 1)] - + for A in A_blades: self.assertEquals(A.inv(), ((-1) ** (A.pure_grade() * (A.pure_grade() - 1) / 2)) * (A / A.norm2())) - + for A in A_blades: self.assertEquals(A < A.inv(), 1) - + A = A_blades[1] self.assertEquals(A.inv(), A / A.norm2()) - - + + def test_3_5_3(self): """ Orthogonal complement and duality. """ - + Ga.dual_mode("Iinv+") - + # some blades by grades for each space spaces = [([Mv('A', i, 'grade', ga=R) for i in range(R.n + 1)], R) for R in [Ga('e*1|2'), Ga('e*1|2|3'), Ga('e*1|2|3|4')]] - + # dualization - for blades, R in spaces: + for blades, R in spaces: for A in blades: self.assertEquals(A.dual(), A < R.I_inv()) - + # dualization sign for blades, R in spaces: - for A in blades: + for A in blades: self.assertEquals(A.dual().dual(), ((-1) ** (R.n * (R.n - 1) / 2)) * A) - + # undualization for blades, R in spaces: for A in blades: self.assertEquals(A, A.dual() < R.I()) - - + + def test_3_5_4(self): """ The duality relationships. """ - + Ga.dual_mode("Iinv+") - + R = Ga('e*1|2|3') A_blades = [Mv('A', i, 'grade', ga=R) for i in range(R.n + 1)] B_blades = [Mv('B', i, 'grade', ga=R) for i in range(R.n + 1)] - - for A, B in product(A_blades, B_blades): + + for A, B in product(A_blades, B_blades): self.assertEquals((A ^ B).dual(), A < B.dual()) - - for A, B in product(A_blades, B_blades): + + for A, B in product(A_blades, B_blades): self.assertEquals((A < B).dual(), A ^ B.dual()) - - + + def test_3_6(self): """ Orthogonal projection of subspaces. """ - + Ga.dual_mode("Iinv+") - + R = Ga('e*1|2|3') - A_blades = [Mv('A', i, 'grade', ga=R) for i in range(R.n + 1)] + X_blades = [Mv('X', i, 'grade', ga=R) for i in range(R.n + 1)] B_blades = [Mv('B', i, 'grade', ga=R) for i in range(R.n + 1)] - - def P(A, B): - return (A < B) < B.inv() - + + # projection of X on B + def P(X, B): + return (X < B.inv()) < B + # a projection should be idempotent - for A, B in product(A_blades, B_blades): - self.assertEquals(P(A, B), P(P(A, B), B)) - + for X, B in product(X_blades, B_blades): + self.assertEquals(P(X, B), P(P(X, B), B)) + + # with the contraction + for X, B in product(X_blades, B_blades): + self.assertEquals(X < B, P(X, B) < B) + if __name__ == '__main__': From 0c33701530c1c216f5bdfd61fb7a57f930ff762f Mon Sep 17 00:00:00 2001 From: Sylvain Meunier Date: Tue, 12 Apr 2016 21:02:36 +0200 Subject: [PATCH 29/78] cleanup chapter 3 exercices --- tests/test_chapter3.py | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/tests/test_chapter3.py b/tests/test_chapter3.py index ec0706ca..6db9485c 100644 --- a/tests/test_chapter3.py +++ b/tests/test_chapter3.py @@ -30,9 +30,9 @@ def test_3_2_2(self): Ga.dual_mode("Iinv+") R = Ga('e*1|2|3') - A_blades = [Mv('A', i, 'grade', ga=R) for i in range(R.n + 1)] - B_blades = [Mv('B', i, 'grade', ga=R) for i in range(R.n + 1)] - C_blades = [Mv('C', i, 'grade', ga=R) for i in range(R.n + 1)] + A_blades = [R.mv('A', i, 'grade') for i in range(R.n + 1)] + B_blades = [R.mv('B', i, 'grade') for i in range(R.n + 1)] + C_blades = [R.mv('C', i, 'grade') for i in range(R.n + 1)] # scalar and blades of various grades A = A_blades[0] @@ -69,7 +69,7 @@ def test_3_2_2(self): self.assertEquals((alpha * A) < B, alpha * (A < B)) self.assertEquals((alpha * A) < B, A < (alpha * B)) - a = Mv('a', 1, 'grade', ga=R) + a = R.mv('a', 1, 'grade') for A_minus1, B in product(A_blades[:-1], B_blades): A = A_minus1 ^ a self.assertEquals(A < B, (A_minus1 ^ a) < B) @@ -84,8 +84,8 @@ def test_3_4(self): Ga.dual_mode("Iinv+") R = Ga('e*1|2|3') - A_blades = [Mv('A', i, 'grade', ga=R) for i in range(R.n + 1)] - B_blades = [Mv('B', i, 'grade', ga=R) for i in range(R.n + 1)] + A_blades = [R.mv('A', i, 'grade') for i in range(R.n + 1)] + B_blades = [R.mv('B', i, 'grade') for i in range(R.n + 1)] for A, B in product(A_blades, B_blades): self.assertEquals(B > A, ((-1) ** (A.pure_grade() * (B.pure_grade() - 1))) * (A < B)) @@ -102,7 +102,7 @@ def test_3_5_2(self): Ga.dual_mode("Iinv+") R = Ga('e*1|2|3') - A_blades = [Mv('A', i, 'grade', ga=R) for i in range(R.n + 1)] + A_blades = [R.mv('A', i, 'grade') for i in range(R.n + 1)] for A in A_blades: self.assertEquals(A.inv(), ((-1) ** (A.pure_grade() * (A.pure_grade() - 1) / 2)) * (A / A.norm2())) @@ -122,8 +122,8 @@ def test_3_5_3(self): Ga.dual_mode("Iinv+") # some blades by grades for each space - spaces = [([Mv('A', i, 'grade', ga=R) for i in range(R.n + 1)], R) for R in [Ga('e*1|2'), Ga('e*1|2|3'), Ga('e*1|2|3|4')]] - + spaces = [([R.mv('A', i, 'grade') for i in range(R.n + 1)], R) for R in [Ga('e*1|2'), Ga('e*1|2|3'), Ga('e*1|2|3|4')]] + # dualization for blades, R in spaces: for A in blades: @@ -148,8 +148,8 @@ def test_3_5_4(self): Ga.dual_mode("Iinv+") R = Ga('e*1|2|3') - A_blades = [Mv('A', i, 'grade', ga=R) for i in range(R.n + 1)] - B_blades = [Mv('B', i, 'grade', ga=R) for i in range(R.n + 1)] + A_blades = [R.mv('A', i, 'grade') for i in range(R.n + 1)] + B_blades = [R.mv('B', i, 'grade') for i in range(R.n + 1)] for A, B in product(A_blades, B_blades): self.assertEquals((A ^ B).dual(), A < B.dual()) @@ -166,8 +166,8 @@ def test_3_6(self): Ga.dual_mode("Iinv+") R = Ga('e*1|2|3') - X_blades = [Mv('X', i, 'grade', ga=R) for i in range(R.n + 1)] - B_blades = [Mv('B', i, 'grade', ga=R) for i in range(R.n + 1)] + X_blades = [R.mv('X', i, 'grade') for i in range(R.n + 1)] + B_blades = [R.mv('B', i, 'grade') for i in range(R.n + 1)] # projection of X on B def P(X, B): From 99e20e96bad09840e3688d60823596c482712e62 Mon Sep 17 00:00:00 2001 From: Sylvain Meunier Date: Tue, 12 Apr 2016 22:35:03 +0200 Subject: [PATCH 30/78] leo dorst book chapter 3 exercices (in progress) --- tests/test_chapter3.py | 50 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/tests/test_chapter3.py b/tests/test_chapter3.py index 6db9485c..58b917d7 100644 --- a/tests/test_chapter3.py +++ b/tests/test_chapter3.py @@ -180,6 +180,56 @@ def P(X, B): # with the contraction for X, B in product(X_blades, B_blades): self.assertEquals(X < B, P(X, B) < B) + + + def test_3_10_1_1(self): + """ + Drills. + """ + + Ga.dual_mode("Iinv+") + + _R, e_1, e_2, e_3 = Ga.build('e*1|2|3', g='1 0 0, 0 1 0, 0 0 1') + + a = e_1 + e_2 + b = e_2 + e_3 + + # a + self.assertEquals(e_1 < a, (e_1 < e_1) + (e_1 < e_2)) + self.assertEquals(e_1 < e_1, e_1 | e_1) + self.assertEquals(e_1 < e_1, 1) + self.assertEquals(e_1 < e_2, e_1 | e_2) + self.assertEquals(e_1 < e_2, 0) + self.assertEquals(e_1 < a, 1) + + # b + self.assertEquals(e_1 < (a ^ b), ((e_1 < a) ^ b) - (a ^ (e_1 < b))) + self.assertEquals((e_1 < a) ^ b, b) # reusing drill a + self.assertEquals(e_1 < b, (e_1 < e_2) + (e_1 < e_3)) + self.assertEquals(e_1 < b, 0) + self.assertEquals(e_1 < (a ^ b), b) + + # c + self.assertEquals((a ^ b) < e_1, a < (b < e_1)) + self.assertEquals((a ^ b) < e_1, a < ((e_2 < e_1) + (e_3 < e_1))) + self.assertEquals((a ^ b) < e_1, 0) + + # d + self.assertEquals(((2 * a) + b) < (a + b), ((2 * a) < a) + ((2 * a) < b) + (b < a) + (b < b)) + self.assertEquals(((2 * a) + b) < (a + b), ((2 * (a < a)) + (2 * (a < b)) + (b < a) + (b < b))) + self.assertEquals(((2 * a) + b) < (a + b), 4 + 2 + 1 + 2) + + # e + self.assertEquals(a < (e_1 ^ e_2 ^ e_3), a < ((e_1 ^ e_2) ^ e_3)) + self.assertEquals(a < (e_1 ^ e_2 ^ e_3), ((a < (e_1 ^ e_2)) ^ e_3) + ((e_1 ^ e_2) ^ (a < e_3))) + self.assertEquals(a < (e_1 ^ e_2 ^ e_3), (((e_1 < (e_1 ^ e_2)) + (e_2 < (e_1 ^ e_2))) ^ e_3) + ((e_1 ^ e_2) ^ ((e_1 < e_3) + (e_2 < e_3)))) + self.assertEquals(((e_1 < (e_1 ^ e_2)) + (e_2 < (e_1 ^ e_2))) ^ e_3, (((e_1 < e_1) ^ e_2) - (e_1 ^ (e_1 < e_2)) + ((e_2 < e_1) ^ e_2) - (e_1 ^ (e_2 < e_2))) ^ e_3) + self.assertEquals(((e_1 < (e_1 ^ e_2)) + (e_2 < (e_1 ^ e_2))) ^ e_3, (e_2 - e_1) ^ e_3) + self.assertEquals(((e_1 < (e_1 ^ e_2)) + (e_2 < (e_1 ^ e_2))) ^ e_3, (e_2 ^ e_3) - (e_1 ^ e_3)) + self.assertEquals(e_1 < e_3, 0) + self.assertEquals(e_2 < e_3, 0) + self.assertEquals(((e_1 ^ e_2) ^ ((e_1 < e_3) + (e_2 < e_3))), 0) + self.assertEquals(a < (e_1 ^ e_2 ^ e_3), (e_2 ^ e_3) - (e_1 ^ e_3)) if __name__ == '__main__': From dcd1d4535400912c7d75880570bd25103abc937e Mon Sep 17 00:00:00 2001 From: Sylvain Meunier Date: Fri, 15 Apr 2016 20:11:57 +0200 Subject: [PATCH 31/78] leo dorst book chapter 3 exercices (in progress) --- tests/test_chapter3.py | 26 ++++++++++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/tests/test_chapter3.py b/tests/test_chapter3.py index 58b917d7..8b79fe36 100644 --- a/tests/test_chapter3.py +++ b/tests/test_chapter3.py @@ -189,7 +189,7 @@ def test_3_10_1_1(self): Ga.dual_mode("Iinv+") - _R, e_1, e_2, e_3 = Ga.build('e*1|2|3', g='1 0 0, 0 1 0, 0 0 1') + R, e_1, e_2, e_3 = Ga.build('e*1|2|3', g='1 0 0, 0 1 0, 0 0 1') a = e_1 + e_2 b = e_2 + e_3 @@ -208,6 +208,7 @@ def test_3_10_1_1(self): self.assertEquals(e_1 < b, (e_1 < e_2) + (e_1 < e_3)) self.assertEquals(e_1 < b, 0) self.assertEquals(e_1 < (a ^ b), b) + self.assertEquals(e_1 < (a ^ b), e_2 + e_3) # c self.assertEquals((a ^ b) < e_1, a < (b < e_1)) @@ -228,8 +229,29 @@ def test_3_10_1_1(self): self.assertEquals(((e_1 < (e_1 ^ e_2)) + (e_2 < (e_1 ^ e_2))) ^ e_3, (e_2 ^ e_3) - (e_1 ^ e_3)) self.assertEquals(e_1 < e_3, 0) self.assertEquals(e_2 < e_3, 0) - self.assertEquals(((e_1 ^ e_2) ^ ((e_1 < e_3) + (e_2 < e_3))), 0) + self.assertEquals(((e_1 ^ e_2) ^ ((e_1 < e_3) + (e_2 < e_3))), 0) self.assertEquals(a < (e_1 ^ e_2 ^ e_3), (e_2 ^ e_3) - (e_1 ^ e_3)) + + # f + self.assertEquals(a < R.I_inv(), a < (-e_1 ^ e_2 ^ e_3)) # equals because we fixed the metric + self.assertEquals(a < R.I_inv(), a < ((e_1 ^ e_2) ^ (-e_3))) # almost last drill + self.assertEquals(a < R.I_inv(), -(e_2 ^ e_3) + (e_1 ^ e_3)) + + # g + self.assertEquals((a ^ b) < R.I_inv(), -((b ^ a) < R.I_inv())) + self.assertEquals((a ^ b) < R.I_inv(), -(b < (a < R.I_inv()))) + self.assertEquals((a ^ b) < R.I_inv(), -((e_2 + e_3) < (-(e_2 ^ e_3) + (e_1 ^ e_3)))) + self.assertEquals((a ^ b) < R.I_inv(), ((e_2 + e_3) < (e_2 ^ e_3)) - ((e_2 + e_3) < (e_1 ^ e_3))) + self.assertEquals((a ^ b) < R.I_inv(), ((e_2 < (e_2 ^ e_3)) + (e_3 < (e_2 ^ e_3)) - (e_2 < (e_1 ^ e_3)) - (e_3 < (e_1 ^ e_3)))) + self.assertEquals(e_2 < (e_2 ^ e_3), e_3) + self.assertEquals(e_3 < (e_2 ^ e_3), -e_2) + self.assertEquals(e_2 < (e_1 ^ e_3), 0) + self.assertEquals(e_3 < (e_1 ^ e_3), -e_1) + self.assertEquals((a ^ b) < R.I_inv(), e_3 - e_2 + e_1) + + # h + self.assertEquals(a < (b < R.I_inv()), (a ^ b) < R.I_inv()) + self.assertEquals(a < (b < R.I_inv()), e_3 - e_2 + e_1) if __name__ == '__main__': From 7803817ca9d49f6154d1763dccb5bcd796e0fecc Mon Sep 17 00:00:00 2001 From: Sylvain Meunier Date: Sun, 17 Apr 2016 21:51:48 +0200 Subject: [PATCH 32/78] leo dorst book chapter 3 exercices (in progress) --- tests/test_chapter3.py | 84 ++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 80 insertions(+), 4 deletions(-) diff --git a/tests/test_chapter3.py b/tests/test_chapter3.py index 8b79fe36..5b07912b 100644 --- a/tests/test_chapter3.py +++ b/tests/test_chapter3.py @@ -1,7 +1,7 @@ import unittest from itertools import product -from sympy import simplify, Symbol +from sympy import simplify, Symbol, cos, sin, sqrt from ga import Ga from mv import Mv @@ -184,7 +184,9 @@ def P(X, B): def test_3_10_1_1(self): """ - Drills. + Let a = e_1 + e_2 and b = e_2 + e_3 in a 3D Euclidean space R(3,0) with an orthonormal basis {e_1, e_2, e_3}. + Compute the following expressions, giving the results relative to the basis + {1, e_1, e_2, e_3, e_1 ^ e_2, e_2 ^ e_3, e_3 ^ e_1, e_1 ^ e_2 ^ e_3}. """ Ga.dual_mode("Iinv+") @@ -230,12 +232,12 @@ def test_3_10_1_1(self): self.assertEquals(e_1 < e_3, 0) self.assertEquals(e_2 < e_3, 0) self.assertEquals(((e_1 ^ e_2) ^ ((e_1 < e_3) + (e_2 < e_3))), 0) - self.assertEquals(a < (e_1 ^ e_2 ^ e_3), (e_2 ^ e_3) - (e_1 ^ e_3)) + self.assertEquals(a < (e_1 ^ e_2 ^ e_3), (e_2 ^ e_3) + (e_3 ^ e_1)) # f self.assertEquals(a < R.I_inv(), a < (-e_1 ^ e_2 ^ e_3)) # equals because we fixed the metric self.assertEquals(a < R.I_inv(), a < ((e_1 ^ e_2) ^ (-e_3))) # almost last drill - self.assertEquals(a < R.I_inv(), -(e_2 ^ e_3) + (e_1 ^ e_3)) + self.assertEquals(a < R.I_inv(), -(e_2 ^ e_3) - (e_3 ^ e_1)) # g self.assertEquals((a ^ b) < R.I_inv(), -((b ^ a) < R.I_inv())) @@ -252,6 +254,80 @@ def test_3_10_1_1(self): # h self.assertEquals(a < (b < R.I_inv()), (a ^ b) < R.I_inv()) self.assertEquals(a < (b < R.I_inv()), e_3 - e_2 + e_1) + + + def test_3_10_1_2(self): + """ + Compute the cosine of the angle between the following subspaces given on an orthonormal basis of a Euclidean space. + """ + + Ga.dual_mode("Iinv+") + + _R, e_1, e_2, e_3, e_4 = Ga.build('e*1|2|3|4', g='1 0 0 0, 0 1 0 0, 0 0 1 0, 0 0 0 1') + alpha = Symbol('alpha', real=True) + theta = Symbol('theta', real=True) + + # e_1 and alpha * e_1 + A = e_1 + B = alpha * e_1 + cosine = (A | B.rev()) / (A.norm() * B.norm()) + self.assertEquals(A | B.rev(), e_1 | (alpha * e_1)) + self.assertEquals(A.norm() * B.norm(), alpha) + self.assertEquals(cosine, (e_1 | (alpha * e_1)) / alpha) + self.assertEquals(cosine, (alpha * (e_1 | e_1)) / alpha) + self.assertEquals(cosine, 1) + + # (e_1 + e_2) ^ e_3 and e_1 ^ e_3 + A = (e_1 + e_2) ^ e_3 + B = e_1 ^ e_3 + num = A | B.rev() + den = A.norm() * B.norm() + cosine = num / den + self.assertEquals(num, ((e_1 ^ e_3) + (e_2 ^ e_3)) | (e_3 ^ e_1)) + self.assertEquals(num, ((e_1 ^ e_3) | (e_3 ^ e_1)) + ((e_2 ^ e_3) | (e_3 ^ e_1))) + self.assertEquals(((e_1 ^ e_3) | (e_3 ^ e_1)), 1) + self.assertEquals(((e_2 ^ e_3) | (e_3 ^ e_1)), 0) + self.assertEquals(num, 1) + self.assertEquals(den, ((e_1 + e_2) ^ e_3).norm() * (e_1 ^ e_3).norm()) + self.assertEquals(den, ((e_1 ^ e_3) + (e_2 ^ e_3)).norm()) + den2 = ((e_1 ^ e_3) + (e_2 ^ e_3)).norm2() + self.assertEquals(den2, ((e_1 ^ e_3) + (e_2 ^ e_3)) | ((e_1 ^ e_3) + (e_2 ^ e_3)).rev()) + self.assertEquals(den2, ((e_1 ^ e_3) + (e_2 ^ e_3)) | ((e_3 ^ e_1) + (e_3 ^ e_2))) + self.assertEquals(den2, ((e_1 ^ e_3) | (e_3 ^ e_1)) + ((e_1 ^ e_3) | (e_3 ^ e_2)) + ((e_2 ^ e_3) | (e_3 ^ e_1)) + ((e_2 ^ e_3) | (e_3 ^ e_2))) + self.assertEquals(den2, 1 + 0 + 0 + 1) + self.assertEquals(cosine, 1 / sqrt(2)) + + # (cos(theta) * e_1 + sin(theta) * e_2) ^ e_3 and e_2 ^ e_3 + A = (cos(theta) * e_1 + sin(theta) * e_2) ^ e_3 + B = e_2 ^ e_3 + num = A | B.rev() + den = A.norm() * B.norm() + cosine = num / den + self.assertEquals(num, (cos(theta) * (e_1 ^ e_3) + sin(theta) * (e_2 ^ e_3)) | (e_3 ^ e_2)) + self.assertEquals(num, ((cos(theta) * (e_1 ^ e_3)) | (e_3 ^ e_2)) + ((sin(theta) * (e_2 ^ e_3)) | (e_3 ^ e_2))) + self.assertEquals((cos(theta) * (e_1 ^ e_3)) | (e_3 ^ e_2), 0) + self.assertEquals((sin(theta) * (e_2 ^ e_3)) | (e_3 ^ e_2), sin(theta)) + self.assertEquals(num, sin(theta)) + self.assertEquals(den, ((cos(theta) * e_1 + sin(theta) * e_2) ^ e_3).norm() * (e_2 ^ e_3).norm()) + self.assertEquals(den, ((cos(theta) * e_1 + sin(theta) * e_2) ^ e_3).norm()) + den2 = ((cos(theta) * e_1 + sin(theta) * e_2) ^ e_3).norm2() + self.assertEquals(den2, ((cos(theta) * e_1 + sin(theta) * e_2) ^ e_3) | ((cos(theta) * e_1 + sin(theta) * e_2) ^ e_3).rev()) + self.assertEquals(den2, (cos(theta) * (e_1 ^ e_3) + sin(theta) * (e_2 ^ e_3)) | (cos(theta) * (e_1 ^ e_3) + sin(theta) * (e_2 ^ e_3)).rev()) + self.assertEquals(den2, (cos(theta) * (e_1 ^ e_3) + sin(theta) * (e_2 ^ e_3)) | (cos(theta) * (e_3 ^ e_1) + sin(theta) * (e_3 ^ e_2))) + self.assertEquals(den2, ((cos(theta) * (e_1 ^ e_3)) | (cos(theta) * (e_3 ^ e_1))) + ((cos(theta) * (e_1 ^ e_3)) | (sin(theta) * (e_3 ^ e_2))) + ((sin(theta) * (e_2 ^ e_3)) | (cos(theta) * (e_3 ^ e_1))) + ((sin(theta) * (e_2 ^ e_3)) | (sin(theta) * (e_3 ^ e_2)))) + self.assertEquals(den2, cos(theta) * cos(theta) + sin(theta) * sin(theta)) + self.assertEquals(den2, 1) + self.assertEquals(cosine, sin(theta)) + + # e_1 ^ e_2 and e_3 ^ e_4 + A = e_1 ^ e_2 + B = e_3 ^ e_4 + num = A | B.rev() + den = A.norm() * B.norm() + cosine = num / den + self.assertEquals(num, (e_1 ^ e_2) | (e_4 ^ e_3)) + self.assertEquals(num, 0) + self.assertEquals(cosine, 0) if __name__ == '__main__': From ca9c388c0f015c5435f5be6ac147c116b8ccac3e Mon Sep 17 00:00:00 2001 From: Sylvain Meunier Date: Sun, 29 Sep 2019 22:08:04 +0200 Subject: [PATCH 33/78] leo dorst book chapter 2 exercices (in progress) --- tests/test_chapter2.py | 70 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 70 insertions(+) diff --git a/tests/test_chapter2.py b/tests/test_chapter2.py index 21e9c7a9..136a30da 100755 --- a/tests/test_chapter2.py +++ b/tests/test_chapter2.py @@ -3,6 +3,7 @@ from sympy import Symbol, Matrix, solve, solve_poly_system, cos, sin from ga import Ga +from mv import Mv class TestChapter2(unittest.TestCase): @@ -71,6 +72,15 @@ def test2_12_1_4(self): self.assertTrue(x == e_1 + 2*e_2) + def test_2_12_1_5(self): + """ + Compute (2 + 3 * e_3) ^ (e_1 + (e_2 ^ e_3) using the grade based defining equations of section 2.9.4. + """ + (_g3d, e_1, e_2, e_3) = Ga.build('e*1|2|3') + + self.assertTrue((2 + 3 * e_3) ^ (e_1 + (e_2 ^ e_3)) == (2 * e_1 + 2 * (e_2 ^ e_3) + 3 * (e_3 ^ e_1))) + + def test2_12_2_1(self): """ In R2 with Euclidean metric, choose an orthonormal basis {e_1, e_2} in the plane of a and b such that e1 is parallel to a. @@ -100,6 +110,66 @@ def test2_12_2_1(self): def test2_12_2_2(self): + + (_g2d, e_1, e_2) = Ga.build('e*1|2', g='1 0, 0 1') + + a = Symbol('a') + b = Symbol('b') + t = Symbol('t') + x = a * e_1 + y = b * (cos(t) * e_1 + sin(t) * e_2) + + x_1 = x.blade_coefs([e_1])[0] + x_2 = x.blade_coefs([e_2])[0] + y_1 = y.blade_coefs([e_1])[0] + y_2 = y.blade_coefs([e_2])[0] + + cross = x_1 * y_2 - y_1 * x_2 + + self.assertTrue(cross == (a * b * sin(t))) + + + def test2_12_2_4(self): + """ + Solve the linear system of equation using the outer product. + """ + (g3d, e_1, e_2, e_3) = Ga.build('e*1|2|3') + + a_1 = Symbol('a_1') + a_2 = Symbol('a_2') + a_3 = Symbol('a_3') + b_1 = Symbol('b_1') + b_2 = Symbol('b_2') + b_3 = Symbol('b_3') + c_1 = Symbol('c_1') + c_2 = Symbol('c_2') + c_3 = Symbol('c_3') + a = a_1 * e_1 + a_2 * e_2 + a_3 * e_3 + b = b_1 * e_1 + b_2 * e_2 + b_3 * e_3 + c = c_1 * e_1 + c_2 * e_2 + c_3 * e_3 + + x_1 = Symbol('x_1') + x_2 = Symbol('x_2') + x_3 = Symbol('x_3') + x = x_1 * a + x_2 * b + x_3 * c + + # Solve x_1 + self.assertTrue((x ^ a) == (x_2 * (b ^ a) + x_3 * (c ^ a))) + self.assertTrue((x ^ a ^ b) == x_3 * (c ^ a ^ b)) + self.assertTrue((x ^ a ^ b) * (c ^ a ^ b).inv() == Mv(x_3, 'scalar', ga=g3d)) + + # Solve x_2 + self.assertTrue((x ^ b) == (x_1 * (a ^ b) + x_3 * (c ^ b))) + self.assertTrue((x ^ b ^ c) == x_1 * (a ^ b ^ c)) + self.assertTrue((x ^ b ^ c) * (a ^ b ^ c).inv() == Mv(x_1, 'scalar', ga=g3d)) + + # Solve x_3 + self.assertTrue((x ^ c) == (x_1 * (a ^ c) + x_2 * (b ^ c))) + self.assertTrue((x ^ c ^ a) == x_2 * (b ^ c ^ a)) + self.assertTrue((x ^ c ^ a) * (b ^ c ^ a).inv() == Mv(x_2, 'scalar', ga=g3d)) + + + def test2_12_2_5(self): """ Consider R4 with basis {e_1, e_2, e_3, e_4}. Show that the 2-vector B = (e_1 ^ e_2) + (e_3 ^ e_4) is not a 2-blade (i.e., it cannot be written as the outer product of two vectors). From d5028e1a800def6b015d8d250e97525818b7a630 Mon Sep 17 00:00:00 2001 From: Sylvain Meunier Date: Mon, 30 Sep 2019 14:22:04 +0200 Subject: [PATCH 34/78] leo dorst book chapter 2 exercices (in progress) --- tests/test_chapter2.py | 43 ++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 41 insertions(+), 2 deletions(-) diff --git a/tests/test_chapter2.py b/tests/test_chapter2.py index 136a30da..9e302d2e 100755 --- a/tests/test_chapter2.py +++ b/tests/test_chapter2.py @@ -76,9 +76,16 @@ def test_2_12_1_5(self): """ Compute (2 + 3 * e_3) ^ (e_1 + (e_2 ^ e_3) using the grade based defining equations of section 2.9.4. """ - (_g3d, e_1, e_2, e_3) = Ga.build('e*1|2|3') + (g3d, e_1, e_2, e_3) = Ga.build('e*1|2|3') + + A = 2 + 3 * e_3 + B = e_1 + (e_2 ^ e_3) - self.assertTrue((2 + 3 * e_3) ^ (e_1 + (e_2 ^ e_3)) == (2 * e_1 + 2 * (e_2 ^ e_3) + 3 * (e_3 ^ e_1))) + R = Mv(0, 'scalar', ga=g3d) + for k, l in itertools.product(range(3), range(3)): + R += A.get_grade(k) ^ B.get_grade(l) + + self.assertTrue(R == (2 * e_1 + 3 * (e_3 ^ e_1) + 2 * (e_2 ^ e_3))) def test2_12_2_1(self): @@ -216,6 +223,38 @@ def test2_12_2_5(self): self.assertTrue(result is None) + def test2_12_2_6(self): + """ + Show that B = e1 ^ e2 + e3 ^ e4 of the previous exercise doesn't contain any other vector than 0. + """ + (_g4d, e_1, e_2, e_3, e_4) = Ga.build('e*1|2|3|4') + + # B + B = (e_1 ^ e_2) + (e_3 ^ e_4) + + # x + x_1 = Symbol('x_1') + x_2 = Symbol('x_2') + x_3 = Symbol('x_3') + x_4 = Symbol('x_4') + x = x_1 * e_1 + x_2 * e_2 + x_3 * e_3 + x_4 * e_4 + + # Solve x ^ B = 0 + system = (x ^ B).blade_coefs() + + unknowns = [ + x_1, x_2, x_3, x_4 + ] + + # TODO: use solve if sympy fix it + result = solve_poly_system(system, unknowns) + + self.assertTrue(result[0] == 0) + self.assertTrue(result[1] == 0) + self.assertTrue(result[2] == 0) + self.assertTrue(result[3] == 0) + + def test2_12_2_9(self): """ Prove Ak ^ Bl = (-1**kl) Bl ^ Ak. From 0afad0b40c37b76878bd375a484198ca7b20a0ea Mon Sep 17 00:00:00 2001 From: Sylvain Meunier Date: Thu, 3 Oct 2019 18:56:02 +0200 Subject: [PATCH 35/78] compute grades using grade_decomposition instead of pure_grade, leo dorst book chapter 3 exercices (in progress) --- tests/test_chapter2.py | 89 ++++++++++++++++------------- tests/test_chapter3.py | 127 ++++++++++++++++++++++++++++++++++++----- 2 files changed, 162 insertions(+), 54 deletions(-) diff --git a/tests/test_chapter2.py b/tests/test_chapter2.py index 9e302d2e..2333bd1c 100755 --- a/tests/test_chapter2.py +++ b/tests/test_chapter2.py @@ -1,18 +1,41 @@ import unittest -import itertools +from itertools import product from sympy import Symbol, Matrix, solve, solve_poly_system, cos, sin from ga import Ga from mv import Mv class TestChapter2(unittest.TestCase): - def test_2_12_1_1(self): + def test2_9_1(self): + """ + Blades and grades. + """ + R, e_1, e_2, e_3, e_4, e_5 = Ga.build('e*1|2|3|4|5') + + # Check for k in [1, R.n] + for k in range(1, R.n + 1): + Ak = R.mv('a', k, 'grade') + grades = R.grade_decomposition(Ak) + self.assertEquals(len(grades), 1) + self.assertEquals(grades.keys()[0], k) + + # Check for k and l in in [1, R.n] + for k, l in product(range(1, R.n + 1), range(1, R.n + 1)): + Ak = R.mv('a', k, 'grade') + Bl = R.mv('b', l, 'grade') + C = Ak ^ Bl + grades = R.grade_decomposition(C) + self.assertEquals(len(grades), 1) + self.assertEquals(grades.keys()[0], 0 if C == 0 else k + l) + + + def test2_12_1_1(self): """ Compute the outer products of the following 3-space expressions, giving the result relative to the basis {1, e_1, e_2, e_3, e_1^e_2, e_1^e_3, e_2^e_3, e_1^e_2^e_3}. """ - (_g3d, e_1, e_2, e_3) = Ga.build('e*1|2|3') + R, e_1, e_2, e_3 = Ga.build('e*1|2|3') self.assertTrue((e_1 + e_2) ^ (e_1 + e_3) == (-e_1 ^ e_2) + (e_1 ^ e_3) + (e_2 ^ e_3)) self.assertTrue((e_1 + e_2 + e_3) ^ (2*e_1) == -2*(e_1 ^ e_2) - 2*(e_1 ^ e_3)) self.assertTrue((e_1 - e_2) ^ (e_1 - e_3) == (e_1 ^ e_2) - (e_1 ^ e_3) + (e_2 ^ e_3)) @@ -26,7 +49,7 @@ def test2_12_1_2(self): Given the 2-blade B = e_1 ^ (e_2 - e_3) that represents a plane, determine if each of the following vectors lies in that plane. """ - (_g3d, e_1, e_2, e_3) = Ga.build('e*1|2|3') + R, e_1, e_2, e_3 = Ga.build('e*1|2|3') B = e_1 ^ (e_2 - e_3) self.assertTrue(e_1 ^ B == 0) self.assertFalse((e_1 + e_2) ^ B == 0) @@ -39,7 +62,7 @@ def test2_12_1_3(self): What is the area of the parallelogram spanned by the vectors a = e_1 + 2*e_2 and b = -e_1 - e_2 (relative to the area of e_1 ^ e_2) ? """ - (_g3d, e_1, e_2, _e_3) = Ga.build('e*1|2|3') + R, e_1, e_2, e_3 = Ga.build('e*1|2|3') a = e_1 + 2*e_2 b = -e_1 - e_2 B = a ^ b @@ -52,7 +75,7 @@ def test2_12_1_4(self): and direction vector e_2, and the line M with position vector e_2 and direction vector (e_1 + e_2), using 2-blades. """ - (_g2d, e_1, e_2) = Ga.build('e*1|2') + R, e_1, e_2 = Ga.build('e*1|2') # x is defined in the basis {e_1, e_2} a = Symbol('a') @@ -72,20 +95,20 @@ def test2_12_1_4(self): self.assertTrue(x == e_1 + 2*e_2) - def test_2_12_1_5(self): + def test2_12_1_5(self): """ Compute (2 + 3 * e_3) ^ (e_1 + (e_2 ^ e_3) using the grade based defining equations of section 2.9.4. """ - (g3d, e_1, e_2, e_3) = Ga.build('e*1|2|3') + R, e_1, e_2, e_3 = Ga.build('e*1|2|3') A = 2 + 3 * e_3 B = e_1 + (e_2 ^ e_3) - R = Mv(0, 'scalar', ga=g3d) - for k, l in itertools.product(range(3), range(3)): - R += A.get_grade(k) ^ B.get_grade(l) + C = R.mv(0, 'scalar') + for k, l in product(range(R.n + 1), range(R.n + 1)): + C += A.get_grade(k) ^ B.get_grade(l) - self.assertTrue(R == (2 * e_1 + 3 * (e_3 ^ e_1) + 2 * (e_2 ^ e_3))) + self.assertTrue(C == (2 * e_1 + 3 * (e_3 ^ e_1) + 2 * (e_2 ^ e_3))) def test2_12_2_1(self): @@ -94,7 +117,7 @@ def test2_12_2_1(self): Write x = a * e_1 and y = b * (cos(t) * e_1 + sin(t) * e_2), whete t is the angle from a to b. Evaluate the outer product. What is the geometrical interpretation ? """ - (_g2d, e_1, e_2) = Ga.build('e*1|2', g='1 0, 0 1') + R, e_1, e_2 = Ga.build('e*1|2', g='1 0, 0 1') # TODO: use alpha, beta and theta instead of a, b and t (it crashes sympy) a = Symbol('a') @@ -118,7 +141,7 @@ def test2_12_2_1(self): def test2_12_2_2(self): - (_g2d, e_1, e_2) = Ga.build('e*1|2', g='1 0, 0 1') + R, e_1, e_2 = Ga.build('e*1|2', g='1 0, 0 1') a = Symbol('a') b = Symbol('b') @@ -140,7 +163,7 @@ def test2_12_2_4(self): """ Solve the linear system of equation using the outer product. """ - (g3d, e_1, e_2, e_3) = Ga.build('e*1|2|3') + R, e_1, e_2, e_3 = Ga.build('e*1|2|3') a_1 = Symbol('a_1') a_2 = Symbol('a_2') @@ -163,17 +186,17 @@ def test2_12_2_4(self): # Solve x_1 self.assertTrue((x ^ a) == (x_2 * (b ^ a) + x_3 * (c ^ a))) self.assertTrue((x ^ a ^ b) == x_3 * (c ^ a ^ b)) - self.assertTrue((x ^ a ^ b) * (c ^ a ^ b).inv() == Mv(x_3, 'scalar', ga=g3d)) + self.assertTrue((x ^ a ^ b) * (c ^ a ^ b).inv() == R.mv(x_3, 'scalar')) # Solve x_2 self.assertTrue((x ^ b) == (x_1 * (a ^ b) + x_3 * (c ^ b))) self.assertTrue((x ^ b ^ c) == x_1 * (a ^ b ^ c)) - self.assertTrue((x ^ b ^ c) * (a ^ b ^ c).inv() == Mv(x_1, 'scalar', ga=g3d)) + self.assertTrue((x ^ b ^ c) * (a ^ b ^ c).inv() == R.mv(x_1, 'scalar')) # Solve x_3 self.assertTrue((x ^ c) == (x_1 * (a ^ c) + x_2 * (b ^ c))) self.assertTrue((x ^ c ^ a) == x_2 * (b ^ c ^ a)) - self.assertTrue((x ^ c ^ a) * (b ^ c ^ a).inv() == Mv(x_2, 'scalar', ga=g3d)) + self.assertTrue((x ^ c ^ a) * (b ^ c ^ a).inv() == R.mv(x_2, 'scalar')) def test2_12_2_5(self): @@ -181,7 +204,7 @@ def test2_12_2_5(self): Consider R4 with basis {e_1, e_2, e_3, e_4}. Show that the 2-vector B = (e_1 ^ e_2) + (e_3 ^ e_4) is not a 2-blade (i.e., it cannot be written as the outer product of two vectors). """ - (_g4d, e_1, e_2, e_3, e_4) = Ga.build('e*1|2|3|4') + R, e_1, e_2, e_3, e_4 = Ga.build('e*1|2|3|4') # B B = (e_1 ^ e_2) + (e_3 ^ e_4) @@ -227,7 +250,7 @@ def test2_12_2_6(self): """ Show that B = e1 ^ e2 + e3 ^ e4 of the previous exercise doesn't contain any other vector than 0. """ - (_g4d, e_1, e_2, e_3, e_4) = Ga.build('e*1|2|3|4') + R, e_1, e_2, e_3, e_4 = Ga.build('e*1|2|3|4') # B B = (e_1 ^ e_2) + (e_3 ^ e_4) @@ -248,6 +271,7 @@ def test2_12_2_6(self): # TODO: use solve if sympy fix it result = solve_poly_system(system, unknowns) + result = result[0] self.assertTrue(result[0] == 0) self.assertTrue(result[1] == 0) @@ -260,26 +284,13 @@ def test2_12_2_9(self): Prove Ak ^ Bl = (-1**kl) Bl ^ Ak. """ - # very slow if bigger - dimension = 5 - - # create the vector space - # e*1|2|3|4|5 if dimension == 5 # TODO: don't fix the vector space dimension - ga = Ga('e*%s' % '|'.join(str(i) for i in range(1, dimension+1))) - - # an helper for building multivectors - # Ak = a1 ^ a2 ^ ... ^ ak if _build('a', k) - def _build(name, grade): - M = ga.mv('%s1' % name, 'vector') - for k in range(2, grade+1): - M = M ^ ga.mv('%s%d' % (name, k), 'vector') - return M - - # prove for k in [1, 5] and l in [1, 5] - for k, l in itertools.product(range(1, dimension+1), range(1, dimension+1)): - Ak = _build('a', k) - Bl = _build('b', l) + R, e_1, e_2, e_3, e_4, e_5 = Ga.build('e*1|2|3|4|5') + + # Prove for k in [1, 5] and l in [1, 5] + for k, l in product(range(1, R.n + 1), range(1, R.n + 1)): + Ak = R.mv('a', k, 'grade') + Bl = R.mv('b', l, 'grade') self.assertTrue((Ak ^ Bl) == (-1)**(k*l) * (Bl ^ Ak)) diff --git a/tests/test_chapter3.py b/tests/test_chapter3.py index 5b07912b..fb7a0456 100644 --- a/tests/test_chapter3.py +++ b/tests/test_chapter3.py @@ -22,13 +22,32 @@ def assertEquals(self, first, second, msg=None): self.assertTrue(simplify(first - second) == 0, "%s == %s" % (first, second)) + def test_3_1_2(self): + """ + Definition of the scalar product. + """ + + R = Ga('e*1|2|3') + A_blades = [R.mv('A', i, 'grade') for i in range(R.n + 1)] + B_blades = [R.mv('B', i, 'grade') for i in range(R.n + 1)] + + # GAlgebra doesn't define any scalar product but rely on the geometric product instead + for A, B in product(A_blades, B_blades): + A_grades = R.grade_decomposition(A).keys() + B_grades = R.grade_decomposition(B).keys() + self.assertEquals(len(A_grades), 1) + self.assertEquals(len(B_grades), 1) + if A_grades[0] == B_grades[0]: + self.assertTrue((A * B).scalar() != 0) + else: + self.assertTrue((A * B).scalar() == 0) + + def test_3_2_2(self): """ Computing the contraction explicitly. """ - Ga.dual_mode("Iinv+") - R = Ga('e*1|2|3') A_blades = [R.mv('A', i, 'grade') for i in range(R.n + 1)] B_blades = [R.mv('B', i, 'grade') for i in range(R.n + 1)] @@ -41,7 +60,9 @@ def test_3_2_2(self): A = A_blades[0] for B in B_blades: - self.assertEquals(B < A, 0 if B.pure_grade() > 0 else A * B) + B_grades = R.grade_decomposition(B).keys() + self.assertEquals(len(B_grades), 1) + self.assertEquals(B < A, 0 if B_grades[0] else A * B) # vectors A = A_blades[1] @@ -51,7 +72,9 @@ def test_3_2_2(self): # vector and the outer product of 2 blades of various grades (scalars, vectors, 2-vectors...) A = A_blades[1] for B, C in product(B_blades, C_blades): - self.assertEquals(A < (B ^ C), ((A < B) ^ C) + (-1)**B.pure_grade() * (B ^ (A < C))) + B_grades = R.grade_decomposition(B).keys() + self.assertEquals(len(B_grades), 1) + self.assertEquals(A < (B ^ C), ((A < B) ^ C) + (-1)**B_grades[0] * (B ^ (A < C))) # vector and the outer product of 2 blades of various grades (scalars, vectors, 2-vectors...) for A, B, C in product(A_blades, B_blades, C_blades): @@ -81,17 +104,26 @@ def test_3_4(self): The other contraction. """ - Ga.dual_mode("Iinv+") - R = Ga('e*1|2|3') A_blades = [R.mv('A', i, 'grade') for i in range(R.n + 1)] B_blades = [R.mv('B', i, 'grade') for i in range(R.n + 1)] for A, B in product(A_blades, B_blades): - self.assertEquals(B > A, ((-1) ** (A.pure_grade() * (B.pure_grade() - 1))) * (A < B)) + A_grades = R.grade_decomposition(A).keys() + B_grades = R.grade_decomposition(B).keys() + self.assertEquals(len(A_grades), 1) + self.assertEquals(len(B_grades), 1) + self.assertEquals(B > A, ((-1) ** (A_grades[0] * (B_grades[0] - 1))) * (A < B)) - #for A, B in product(A_blades, B_blades): - # self.assertEquals((B > A).pure_grade(), B.pure_grade() - A.pure_grade()) + for A, B in product(A_blades, B_blades): + C = B > A + A_grades = R.grade_decomposition(A).keys() + B_grades = R.grade_decomposition(B).keys() + C_grades = R.grade_decomposition(C).keys() + self.assertEquals(len(A_grades), 1) + self.assertEquals(len(B_grades), 1) + self.assertEquals(len(C_grades), 1) + self.assertTrue(C == 0 or C_grades[0] == B_grades[0] - A_grades[0]) def test_3_5_2(self): @@ -99,13 +131,13 @@ def test_3_5_2(self): The inverse of a blade. """ - Ga.dual_mode("Iinv+") - R = Ga('e*1|2|3') A_blades = [R.mv('A', i, 'grade') for i in range(R.n + 1)] for A in A_blades: - self.assertEquals(A.inv(), ((-1) ** (A.pure_grade() * (A.pure_grade() - 1) / 2)) * (A / A.norm2())) + A_grades = R.grade_decomposition(A).keys() + self.assertEquals(len(A_grades), 1) + self.assertEquals(A.inv(), ((-1) ** (A_grades[0] * (A_grades[0] - 1) / 2)) * (A / A.norm2())) for A in A_blades: self.assertEquals(A < A.inv(), 1) @@ -163,8 +195,6 @@ def test_3_6(self): Orthogonal projection of subspaces. """ - Ga.dual_mode("Iinv+") - R = Ga('e*1|2|3') X_blades = [R.mv('X', i, 'grade') for i in range(R.n + 1)] B_blades = [R.mv('B', i, 'grade') for i in range(R.n + 1)] @@ -263,7 +293,7 @@ def test_3_10_1_2(self): Ga.dual_mode("Iinv+") - _R, e_1, e_2, e_3, e_4 = Ga.build('e*1|2|3|4', g='1 0 0 0, 0 1 0 0, 0 0 1 0, 0 0 0 1') + R, e_1, e_2, e_3, e_4 = Ga.build('e*1|2|3|4', g='1 0 0 0, 0 1 0 0, 0 0 1 0, 0 0 0 1') alpha = Symbol('alpha', real=True) theta = Symbol('theta', real=True) @@ -330,6 +360,73 @@ def test_3_10_1_2(self): self.assertEquals(cosine, 0) + def test3_10_2_1(self): + """ + In 2-D Euclidean space R(2,0) with orthogonal basis {e1, e2}, let us determine the value of the contraction + e1 < (e1 ^ e2) by means of its implicit definition with A = e1 and B = e1 ^ e2. Let X range over the basis of + the blades {1, e1, e2, e1 ^ e2}. This produces four equations, each of which gives you information on the + coefficient of the corresponding basis element in the final result. + Show that e1 < (e1 ^ e2) = (0)1 + 0(e1) + 1(e2) + 0(e1 ^ e2). + """ + + R, e_1, e_2 = Ga.build('e*1|2', g='1 0, 0 1') + + A = e_1 + B = e_1 ^ e_2 + + X = R.mv(1) + self.assertEquals(((X ^ A) * B).scalar(), (X * (A < B)).scalar()) + self.assertEquals(((X ^ A) * B).scalar(), 0) + + X = e_1 + self.assertEquals(((X ^ A) * B).scalar(), (X * (A < B)).scalar()) + self.assertEquals(((X ^ A) * B).scalar(), 0) + + X = e_2 + self.assertEquals(((X ^ A) * B).scalar(), (X * (A < B)).scalar()) + self.assertEquals(((X ^ A) * B).scalar(), 1) + + X = e_1 ^ e_2 + self.assertEquals(((X ^ A) * B).scalar(), (X * (A < B)).scalar()) + self.assertEquals(((X ^ A) * B).scalar(), 0) + + + def test3_10_2_2(self): + """ + Change the metric such that e2 . e2 == 0. Show that you can't determine the coefficient of e2 in the value + of e1 < (e1 ^ e2) like the previous exercise. The use the explicit definition of the contraction to show the + contraction is still well defined, and equal to e1 < (e1 ^ e2) == e2. + """ + + R, e_1, e_2 = Ga.build('e*1|2', g='1 0, 0 0') + + # We can't use the scalar product anymore (because of the metric) + A = e_1 + B = e_1 ^ e_2 + X = e_2 + self.assertEquals(((X ^ A) * B).scalar(), (X * (A < B)).scalar()) + self.assertEquals(((X ^ A) * B).scalar(), 0) # Which is false + + e_1_grades = R.grade_decomposition(e_1).keys() + self.assertEquals(len(e_1_grades), 1) + self.assertEquals(e_1_grades[0], 1) + + # We use the explicit definition of the contraction instead + self.assertEquals(e_1 < (e_1 ^ e_2), ((e_1 < e_1) ^ e_2) + (-1 ** e_1_grades[0]) * (e_1 ^ (e_1 < e_2))) + # We can't use the definition a < b = a . b using GAlgebra so we solved it ourselves... + self.assertEquals(e_1 < (e_1 ^ e_2), (R.mv(1) ^ e_2) + (-1 ** e_1_grades[0]) * (e_1 ^ R.mv(0))) + self.assertEquals(e_1 < (e_1 ^ e_2), e_2) # Which is true + + + def test3_10_2_3(self): + """ + Derive the following dualities for right contraction : + C > (B ^ A) = (C > B) > A and C > (B > A) = (C > B) ^ A when A included in C. + """ + + pass + + if __name__ == '__main__': unittest.main() From cef764f1f60120fcf126c3da642d2051c10e048f Mon Sep 17 00:00:00 2001 From: Sylvain Meunier Date: Thu, 3 Oct 2019 20:44:39 +0200 Subject: [PATCH 36/78] leo dorst book chapter 3 exercices (in progress) --- tests/test_chapter2.py | 14 ++++++++++++++ tests/test_chapter3.py | 20 +++++++++++++++++--- 2 files changed, 31 insertions(+), 3 deletions(-) diff --git a/tests/test_chapter2.py b/tests/test_chapter2.py index 2333bd1c..9e20bcea 100755 --- a/tests/test_chapter2.py +++ b/tests/test_chapter2.py @@ -30,6 +30,20 @@ def test2_9_1(self): self.assertEquals(grades.keys()[0], 0 if C == 0 else k + l) + def test2_9_5(self): + """ + Reversion and grade involution. + """ + R, e_1, e_2, e_3, e_4 = Ga.build('e*1|2|3|4') + + for k in range(1, R.n + 1): + a = [R.mv('a%d' % i, 'vector') for i in range(k)] + A = reduce(Mv.__xor__, a) + A_rev = reduce(Mv.__xor__, reversed(a)) + self.assertEquals(A_rev, A.rev()) + self.assertEquals(A_rev, ((-1) ** ((k * (k - 1)) / 2)) * A) + + def test2_12_1_1(self): """ Compute the outer products of the following 3-space expressions, diff --git a/tests/test_chapter3.py b/tests/test_chapter3.py index fb7a0456..c56d37c1 100644 --- a/tests/test_chapter3.py +++ b/tests/test_chapter3.py @@ -412,9 +412,9 @@ def test3_10_2_2(self): self.assertEquals(e_1_grades[0], 1) # We use the explicit definition of the contraction instead - self.assertEquals(e_1 < (e_1 ^ e_2), ((e_1 < e_1) ^ e_2) + (-1 ** e_1_grades[0]) * (e_1 ^ (e_1 < e_2))) + self.assertEquals(e_1 < (e_1 ^ e_2), ((e_1 < e_1) ^ e_2) + ((-1) ** e_1_grades[0]) * (e_1 ^ (e_1 < e_2))) # We can't use the definition a < b = a . b using GAlgebra so we solved it ourselves... - self.assertEquals(e_1 < (e_1 ^ e_2), (R.mv(1) ^ e_2) + (-1 ** e_1_grades[0]) * (e_1 ^ R.mv(0))) + self.assertEquals(e_1 < (e_1 ^ e_2), (R.mv(1) ^ e_2) + ((-1) ** e_1_grades[0]) * (e_1 ^ R.mv(0))) self.assertEquals(e_1 < (e_1 ^ e_2), e_2) # Which is true @@ -424,7 +424,21 @@ def test3_10_2_3(self): C > (B ^ A) = (C > B) > A and C > (B > A) = (C > B) ^ A when A included in C. """ - pass + R, e_1, e_2, e_3 = Ga.build('e*1|2|3') + + A_blades = [R.mv('X', k, 'grade') for k in range(R.n + 1)] + B_blades = [R.mv('B', l, 'grade') for l in range(R.n + 1)] + C_blades = [R.mv('B', m, 'grade') for m in range(R.n + 1)] + + for A, B, C in product(A_blades, B_blades, C_blades): + M = C > (B ^ A) + self.assertEquals(M, ((A.rev() ^ B.rev()) < C.rev()).rev()) + self.assertEquals(M, (A.rev() < (B.rev() < C.rev())).rev()) + self.assertEquals(M, (B.rev() < C.rev()).rev() > A) + self.assertEquals(M, (C > B) > A) + + M = C > (B > A) + # TODO if __name__ == '__main__': From 5d637bb35f6e8e06ba058977599b3b54ccf78506 Mon Sep 17 00:00:00 2001 From: Sylvain Meunier Date: Fri, 4 Oct 2019 14:23:46 +0200 Subject: [PATCH 37/78] more about cross product --- tests/test_chapter3.py | 36 ++++++++++++++++++++++++++++++++++-- 1 file changed, 34 insertions(+), 2 deletions(-) diff --git a/tests/test_chapter3.py b/tests/test_chapter3.py index c56d37c1..a60b82e7 100644 --- a/tests/test_chapter3.py +++ b/tests/test_chapter3.py @@ -3,7 +3,7 @@ from itertools import product from sympy import simplify, Symbol, cos, sin, sqrt from ga import Ga -from mv import Mv +from mv import Mv, cross class TestChapter3(unittest.TestCase): @@ -210,7 +210,39 @@ def P(X, B): # with the contraction for X, B in product(X_blades, B_blades): self.assertEquals(X < B, P(X, B) < B) - + + + def test3_7_2(self): + """ + The cross product incorporated. + """ + + Ga.dual_mode("Iinv+") + + R, e_1, e_2, e_3 = Ga.build('e*1|2|3') + a = R.mv('a', 'vector') + b = R.mv('b', 'vector') + + A = a < R.I() + B = b < R.I() + + # Cross product definition + self.assertEquals(R.I_inv(), -R.I()) + self.assertEquals(cross(a, b), (a ^ b).dual()) + + # About velocities + self.assertEquals(cross(a, b), (b ^ a) < R.I()) + self.assertEquals(cross(a, b), (a ^ b) < R.I_inv()) + self.assertEquals(cross(a, b), -(b ^ a).dual()) + self.assertEquals(cross(a, b), -b < a.dual()) + self.assertEquals(cross(a, b), b < A) + + # Intersecting planes + self.assertEquals(cross(a, b), ((A < R.I_inv()) ^ (B < R.I_inv())) < R.I_inv()) + self.assertEquals(cross(a, b), (B < R.I_inv()) < ((A < R.I_inv()) < R.I())) + self.assertEquals(cross(a, b), (B < R.I_inv()) < A) + self.assertEquals(cross(a, b), B.dual() < A) + def test_3_10_1_1(self): """ From ff8d2e87c45f6c5a3f8fb0735b1a3cfd1293c656 Mon Sep 17 00:00:00 2001 From: Sylvain Meunier Date: Fri, 4 Oct 2019 18:54:08 +0200 Subject: [PATCH 38/78] leo dorst book chapter 3 exercices (in progress) --- tests/test_chapter2.py | 17 +++++++++++++++++ tests/test_chapter3.py | 22 ++++++++++++++++++++++ 2 files changed, 39 insertions(+) diff --git a/tests/test_chapter2.py b/tests/test_chapter2.py index 9e20bcea..2ad5bb50 100755 --- a/tests/test_chapter2.py +++ b/tests/test_chapter2.py @@ -7,6 +7,23 @@ class TestChapter2(unittest.TestCase): + def test2_3_2(self): + """ + Introducing the outer product. + """ + R, e_1, e_2, e_3 = Ga.build('e*1|2|3') + + a = R.mv('a', 'vector') + b = R.mv('b', 'vector') + c = R.mv('c', 'vector') + alpha = Symbol('alpha') + + self.assertEquals(a ^ b, -b ^ a) + self.assertEquals(a ^ (alpha * b), alpha * (a ^ b)) + self.assertEquals(R.mv(alpha, 'scalar') ^ b, alpha * b) + self.assertEquals(a ^ (b + c), (a ^ b) + (a ^ c)) + + def test2_9_1(self): """ Blades and grades. diff --git a/tests/test_chapter3.py b/tests/test_chapter3.py index a60b82e7..e661f8f4 100644 --- a/tests/test_chapter3.py +++ b/tests/test_chapter3.py @@ -472,6 +472,28 @@ def test3_10_2_3(self): M = C > (B > A) # TODO + def test3_10_2_11(self): + """ + Derive the notorious bac-cab formula for the cross product (a x (b x c) = b (a . c) - c (a . b)), + directly from its definition (3.28). What is the corresponding formula using ^ and < ? + """ + Ga.dual_mode("Iinv+") + + R, e_1, e_2, e_3 = Ga.build('e*1|2|3') + a = R.mv('a', 'vector') + b = R.mv('b', 'vector') + c = R.mv('c', 'vector') + + xx = cross(a, cross(b, c)) + self.assertEquals(xx, (a ^ (b ^ c).dual()).dual()) + self.assertEquals(xx, a < (b ^ c).dual().dual()) + self.assertEquals(xx, -a < (b ^ c)) + self.assertEquals(xx, (-a < b) ^ c - b ^ (-a < c)) + self.assertEquals(xx, b ^ (a < c) - c ^ (a < b)) + self.assertTrue((a < c).is_scalar()) + self.assertTrue((a < b).is_scalar()) + self.assertEquals(xx, b * (a < c) - c * (a < b)) + if __name__ == '__main__': From 7b077f92d95cf255bc9ff10018722a5ec58416d3 Mon Sep 17 00:00:00 2001 From: Sylvain Meunier Date: Fri, 4 Oct 2019 22:23:12 +0200 Subject: [PATCH 39/78] fix Mv.__mul__(A) when A is a scalar and self isn't --- galgebra/mv.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/galgebra/mv.py b/galgebra/mv.py index 63668a29..0ca68a7a 100755 --- a/galgebra/mv.py +++ b/galgebra/mv.py @@ -500,7 +500,7 @@ def __mul__(self, A): if isinstance(A, Dop): return A.Mul(self, A, op='*') - if self.is_scalar(): + if self.is_scalar() or A.is_scalar(): return Mv(self.obj * A, ga=self.Ga) if self.is_blade_rep and A.is_blade_rep: From e6d429112dd655b44e5937f78e74107d6cbb55e4 Mon Sep 17 00:00:00 2001 From: Sylvain Meunier Date: Fri, 4 Oct 2019 22:32:23 +0200 Subject: [PATCH 40/78] leo dorst book chapter 6 exercices (in progress) --- tests/test_chapter6.py | 122 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 122 insertions(+) create mode 100644 tests/test_chapter6.py diff --git a/tests/test_chapter6.py b/tests/test_chapter6.py new file mode 100644 index 00000000..890fe656 --- /dev/null +++ b/tests/test_chapter6.py @@ -0,0 +1,122 @@ +import unittest + +from itertools import product +from sympy import simplify, Symbol, cos, sin, sqrt +from ga import Ga +from mv import Mv, cross + + +class TestChapter3(unittest.TestCase): + + def assertEquals(self, first, second, msg=None): + """ + Compare two expressions are equals. + """ + + if isinstance(first, Mv): + first = first.obj + + if isinstance(second, Mv): + second = second.obj + + self.assertTrue(simplify(first - second) == 0, "%s == %s" % (first, second)) + + + def test6_1_4_1(self): + """ + The geometric product for vectors on a basis. + """ + R, e_1, e_2, e_3 = Ga.build('e*1|2|3', g='1 0 0, 0 1 0, 0 0 1') + + self.assertEquals(e_1 * e_1, 1) + self.assertEquals(e_1 * e_2, e_1 ^ e_2) + self.assertEquals(e_1 * e_3, e_1 ^ e_3) + self.assertEquals(e_2 * e_1, -e_1 ^ e_2) + self.assertEquals(e_2 * e_2, 1) + self.assertEquals(e_2 * e_3, e_2 ^ e_3) + self.assertEquals(e_3 * e_1, -e_1 ^ e_3) + self.assertEquals(e_3 * e_2, -e_2 ^ e_3) + self.assertEquals(e_3 * e_3, 1) + + e_12 = e_1 * e_2 + self.assertEquals(e_12 * e_12, -1) + + e_13 = e_1 * e_3 + self.assertEquals(e_13 * e_13, -1) + + e_23 = e_2 * e_3 + self.assertEquals(e_23 * e_23, -1) + + + def test6_1_4_2(self): + """ + The geometric product for vectors on a basis. + """ + R, e_1, e_2 = Ga.build('e*1|2', g='1 0, 0 1') + ONE = R.mv(1, 'scalar') + e_12 = e_1 ^ e_2 + + self.assertEquals(ONE * ONE, 1) + self.assertEquals(ONE * e_1, e_1) + self.assertEquals(ONE * e_2, e_2) + self.assertEquals(ONE * e_12, e_12) + + self.assertEquals(e_1 * ONE, e_1) + self.assertEquals(e_1 * e_1, 1) + self.assertEquals(e_1 * e_2, e_12) + self.assertEquals(e_1 * e_12, e_2) + + self.assertEquals(e_2 * ONE, e_2) + self.assertEquals(e_2 * e_1, -e_12) + self.assertEquals(e_2 * e_2, 1) + self.assertEquals(e_2 * e_12, -e_1) + + self.assertEquals(e_12 * ONE, e_12) + self.assertEquals(e_12 * e_1, -e_2) + self.assertEquals(e_12 * e_2, e_1) + self.assertEquals(e_12 * e_12, -1) + + a_1 = Symbol('a_1') + a_2 = Symbol('a_2') + a = R.mv((a_1, a_2), 'vector') + + b_1 = Symbol('b_1') + b_2 = Symbol('b_2') + b = R.mv((b_1, b_2), 'vector') + + self.assertEquals(a * b, a_1 * b_1 + a_2 * b_2 + (a_1 * b_2 - a_2 * b_1) * (e_1 ^ e_2)) + + + def test6_3_1(self): + """ + The subspace products from symmetry. + """ + R = Ga('e*1|2|3') + + a = R.mv('a', 'vector') + B_blades = [R.mv('B%d' % k, k, 'grade') for k in range(R.n + 1)] + + def hat(M): + M_grades = R.grade_decomposition(M).keys() + self.assertEquals(len(M_grades), 1) + return ((-1) ** M_grades[0]) * M + + for B in B_blades: + self.assertEquals(a ^ B, 0.5 * (a * B + hat(B) * a)) + self.assertEquals(B ^ a, 0.5 * (B * a + a * hat(B))) + self.assertEquals(a < B, 0.5 * (a * B - hat(B) * a)) + self.assertEquals(B > a, 0.5 * (B * a - a * hat(B))) + self.assertEquals(a * B, (a < B) + (a ^ B)) + + for B in B_blades: + self.assertEquals(hat(B) * a, (hat(B) > a) + (hat(B) ^ a)) + self.assertEquals(hat(B) * a, -(a < B) + (a ^ B)) + self.assertEquals(hat(B) * a, a * B - 2 * (a < B)) + self.assertEquals(hat(B) * a, -(a * B) + 2 * (a ^ B)) + + for B in B_blades: + self.assertEquals(a * B, hat(B) * a + 2 * (a < B)) + self.assertEquals(a * B, -hat(B) * a + 2 * (a ^ B)) + + M = R.mv('m', 'mv') + self.assertEquals(a * M, (a < M) + (a ^ M)) From 9d1b6d044e21d21a8237ba89f583fac41d4e2503 Mon Sep 17 00:00:00 2001 From: Sylvain Meunier Date: Fri, 4 Oct 2019 22:48:42 +0200 Subject: [PATCH 41/78] leo dorst book chapter 6 exercices (in progress) --- tests/test_chapter6.py | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/tests/test_chapter6.py b/tests/test_chapter6.py index 890fe656..da0112de 100644 --- a/tests/test_chapter6.py +++ b/tests/test_chapter6.py @@ -120,3 +120,28 @@ def hat(M): M = R.mv('m', 'mv') self.assertEquals(a * M, (a < M) + (a ^ M)) + + + def test6_3_2(self): + """ + The subspace products as selected grades. + """ + + R = Ga('e*1|2|3') + + A_blades = [R.mv('A%d' % k, k, 'grade') for k in range(R.n + 1)] + B_blades = [R.mv('B%d' % l, l, 'grade') for l in range(R.n + 1)] + + def grade(M): + M_grades = R.grade_decomposition(M).keys() + self.assertEquals(len(M_grades), 1) + return M_grades[0] + + for A, B in product(A_blades, B_blades): + k = grade(A) + l = grade(B) + self.assertEquals(A ^ B, (A * B).get_grade(k + l)) + self.assertEquals(A < B, 0 if k > l else (A * B).get_grade(l - k)) + self.assertEquals(A > B, 0 if l > k else (A * B).get_grade(k - l)) + # TODO: scalar product + From c1eb8313dcb9b247949011787d3078e8e5bbbd0a Mon Sep 17 00:00:00 2001 From: Sylvain Meunier Date: Sun, 6 Oct 2019 11:31:01 +0200 Subject: [PATCH 42/78] leo dorst book chapter 6 exercices (in progress) --- tests/test_chapter6.py | 66 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 66 insertions(+) diff --git a/tests/test_chapter6.py b/tests/test_chapter6.py index da0112de..462f70f0 100644 --- a/tests/test_chapter6.py +++ b/tests/test_chapter6.py @@ -145,3 +145,69 @@ def grade(M): self.assertEquals(A > B, 0 if l > k else (A * B).get_grade(k - l)) # TODO: scalar product + + def test6_6_2_3(self): + """ + The outer product can be defined as the completely antisymmetric summed average of all permutations + of the geometric product of its factors, with a sign for each term depending on oddness or evenness of + the permutation. Derive x ^ y ^ z = 1/3! * (xyz - yxz + yzx - zyx + zxy - xzy) + """ + + R = Ga('e*1|2|3') + + x = R.mv('x', 'vector') + y = R.mv('y', 'vector') + z = R.mv('z', 'vector') + + def hat(M): + M_grades = R.grade_decomposition(M).keys() + self.assertEquals(len(M_grades), 1) + return ((-1) ** M_grades[0]) * M + + self.assertEquals(x * y * z, ((x < y) + (x ^ y)) * z) + self.assertEquals(x * y * z, ((x < y) * z) + ((x ^ y) * z)) + self.assertEquals(x * y * z, ((x < y) * z) - (z < hat(x ^ y)) + (z ^ hat(x ^ y))) + self.assertEquals(hat(x ^ y), x ^ y) + self.assertEquals(x * y * z, ((x < y) * z) - (z < (x ^ y)) + (z ^ x ^ y)) + self.assertEquals(x * y * z, ((x < y) * z) - (z < (x ^ y)) + (x ^ y ^ z)) + + self.assertEquals(y * x * z, ((y < x) + (y ^ x)) * z) + self.assertEquals(y * x * z, ((y < x) * z) + ((y ^ x) * z)) + self.assertEquals(y * x * z, ((y < x) * z) - (z < hat(y ^ x)) + (z ^ hat(y ^ x))) + self.assertEquals(hat(y ^ x), y ^ x) + self.assertEquals(y * x * z, ((y < x) * z) - (z < (y ^ x)) + (z ^ y ^ x)) + self.assertEquals(y * x * z, ((x < y) * z) + (z < (x ^ y)) - (x ^ y ^ z)) + + self.assertEquals(y * z * x, ((y < z) + (y ^ z)) * x) + self.assertEquals(y * z * x, ((y < z) * x) - (x < hat(y ^ z)) + (x ^ hat(y ^ z))) + self.assertEquals(y * z * x, ((y < z) * x) - (x < (y ^ z)) + (x ^ y ^ z)) + + self.assertEquals(z * y * x, ((z < y) + (z ^ y)) * x) + self.assertEquals(z * y * x, ((z < y) * x) - (x < hat(z ^ y)) + (x ^ hat(z ^ y))) + self.assertEquals(z * y * x, ((z < y) * x) - (x < (z ^ y)) - (x ^ y ^ z)) + self.assertEquals(z * y * x, ((y < z) * x) + (x < (y ^ z)) - (x ^ y ^ z)) + + self.assertEquals(z * x * y, ((z < x) + (z ^ x)) * y) + self.assertEquals(z * x * y, ((z < x) * y) + ((z ^ x) * y)) + self.assertEquals(z * x * y, ((z < x) * y) - (y < hat(z ^ x)) + (y ^ hat(z ^ x))) + self.assertEquals(z * x * y, ((z < x) * y) - (y < (z ^ x)) + (y ^ z ^ x)) + self.assertEquals(z * x * y, ((z < x) * y) - (y < (z ^ x)) + (x ^ y ^ z)) + + self.assertEquals(x * z * y, ((x < z) + (x ^ z)) * y) + self.assertEquals(x * z * y, ((x < z) * y) + ((x ^ z) * y)) + self.assertEquals(x * z * y, ((x < z) * y) - (y < hat(x ^ z)) + (y ^ hat(x ^ z))) + self.assertEquals(x * z * y, ((x < z) * y) - (y < (x ^ z)) + (y ^ x ^ z)) + self.assertEquals(x * z * y, ((z < x) * y) + (y < (z ^ x)) - (x ^ y ^ z)) + + self.assertEquals(x * y * z - y * x * z, 2 * ((x ^ y ^ z) - (z < (x ^ y)))) + self.assertEquals(y * z * x - z * y * x, 2 * ((x ^ y ^ z) - (x < (y ^ z)))) + self.assertEquals(z * x * y - x * z * y, 2 * ((x ^ y ^ z) - (y < (z ^ x)))) + + self.assertEquals(z < (x ^ y), ((z < x) ^ y) - (x ^ (z < y))) + self.assertEquals(x < (y ^ z), ((x < y) ^ z) - (y ^ (x < z))) + self.assertEquals(y < (z ^ x), ((y < z) ^ x) - (z ^ (y < x))) + + self.assertEquals(((z < x) ^ y) - (x ^ (z < y)) + ((x < y) ^ z) - (y ^ (x < z)) + ((y < z) ^ x) - (z ^ (y < x)), 0) + + self.assertEquals(x * y * z - y * x * z + y * z * x - z * y * x + z * x * y - x * z * y, 6 * (x ^ y ^ z)) + From 99583d078f04eb7a8e73f7514d4cfc576ecd1ea7 Mon Sep 17 00:00:00 2001 From: Sylvain Meunier Date: Sun, 6 Oct 2019 20:41:36 +0200 Subject: [PATCH 43/78] fix inv, norm2 and norm, enhance inverse test3_5_2 --- galgebra/mv.py | 38 +++++++++++++++----------------------- tests/test_chapter3.py | 28 +++++++++++++++------------- 2 files changed, 30 insertions(+), 36 deletions(-) diff --git a/galgebra/mv.py b/galgebra/mv.py index 0ca68a7a..ba8928da 100755 --- a/galgebra/mv.py +++ b/galgebra/mv.py @@ -577,6 +577,7 @@ def Mv_str(self): if self.i_grade == 0: return str(self.obj) self.obj = expand(self.obj) + self.char_Mv = False self.characterise_Mv() self.obj = metric.Simp.apply(self.obj) #print '\nself.obj =',self.obj @@ -1210,10 +1211,7 @@ def _repr_latex_(self): def norm2(self): reverse = self.rev() product = self * reverse - if product.is_scalar(): - return product.scalar() - else: - raise TypeError('"(' + str(product) + ')**2" is not a scalar in norm2.') + return product.scalar() def norm(self, hint='+'): """ @@ -1237,23 +1235,19 @@ def norm(self, hint='+'): """ reverse = self.rev() product = self * reverse - - if product.is_scalar(): - product = product.scalar() - if product.is_number: - if product >= S(0): - return sqrt(product) - else: - return sqrt(-product) + product = product.scalar() + if product.is_number: + if product >= S(0): + return sqrt(product) else: - if hint == '+': - return metric.square_root_of_expr(product) - elif hint == '-': - return metric.square_root_of_expr(-product) - else: - return sqrt(Abs(product)) + return sqrt(-product) else: - raise TypeError('"(' + str(product) + ')" is not a scalar in norm.') + if hint == '+': + return metric.square_root_of_expr(product) + elif hint == '-': + return metric.square_root_of_expr(-product) + else: + return sqrt(Abs(product)) def inv(self): if self.is_scalar(): # self is a scalar @@ -1262,10 +1256,8 @@ def inv(self): if self_sq.is_scalar(): # self*self is a scalar return (S(1)/self_sq.obj)*self self_rev = self.rev() - self_self_rev = self * self_rev - if(self_self_rev.is_scalar()): # self*self.rev() is a scalar - return (S(1)/self_self_rev.obj) * self_rev - raise TypeError('In inv() for self =' + str(self) + 'self, or self*self or self*self.rev() is not a scalar') + self_self_rev = (self * self_rev).scalar() + return (S(1) / self_self_rev) * self_rev def func(self, fct): # Apply function, fct, to each coefficient of multivector (coefs, bases) = metric.linear_expand(self.obj) diff --git a/tests/test_chapter3.py b/tests/test_chapter3.py index e661f8f4..3e8c63b7 100644 --- a/tests/test_chapter3.py +++ b/tests/test_chapter3.py @@ -131,19 +131,21 @@ def test_3_5_2(self): The inverse of a blade. """ - R = Ga('e*1|2|3') - A_blades = [R.mv('A', i, 'grade') for i in range(R.n + 1)] - - for A in A_blades: - A_grades = R.grade_decomposition(A).keys() - self.assertEquals(len(A_grades), 1) - self.assertEquals(A.inv(), ((-1) ** (A_grades[0] * (A_grades[0] - 1) / 2)) * (A / A.norm2())) - - for A in A_blades: - self.assertEquals(A < A.inv(), 1) - - A = A_blades[1] - self.assertEquals(A.inv(), A / A.norm2()) + for R in [Ga('e*1|2'), Ga('e*1|2|3'), Ga('e*1|2|3|4')]: # , Ga('e*1|2|3|4|5')]: + A_grade_and_blades = [(k, R.mv('A', k, 'grade')) for k in range(R.n + 1)] + for k, A in A_grade_and_blades: + inv_A = A.inv() + rev_A = A.rev() + rev_sign = ((-1) ** (k * (k - 1) / 2)) + norm2 = A.norm2() + self.assertEquals(rev_A, rev_sign * A) + self.assertEquals(inv_A, rev_A / norm2) + # We compute the scalar product using the geometric product + self.assertEquals(inv_A, rev_A / (A * rev_A).scalar()) + self.assertEquals(inv_A, rev_sign * (A / norm2)) + + for k, A in A_grade_and_blades: + self.assertEquals(A < A.inv(), 1) def test_3_5_3(self): From 941a7806c0765da90913045a00c903ecac33c780 Mon Sep 17 00:00:00 2001 From: Sylvain Meunier Date: Mon, 7 Oct 2019 15:34:57 +0200 Subject: [PATCH 44/78] Mv.__init__ can make k-blades using the 'blade' factory, fix Mv.__xor__ for using scalars, replace 'grade' by 'blade' where appropriate and cleanup a few things --- galgebra/mv.py | 13 ++- tests/test_chapter2.py | 81 +++++++++--------- tests/test_chapter3.py | 151 +++++++++++++++------------------ tests/test_chapter6.py | 185 +++++++++++++++++++++++++++++++---------- 4 files changed, 259 insertions(+), 171 deletions(-) diff --git a/galgebra/mv.py b/galgebra/mv.py index ba8928da..54b3bcfc 100755 --- a/galgebra/mv.py +++ b/galgebra/mv.py @@ -163,6 +163,14 @@ def characterise_Mv(self): self.char_Mv = True return + def make_blade(self, *kargs, **kwargs): + # Called by __init__ to make a k-blade + + if isinstance(kargs[0], str) and isinstance(kargs[1], int): + root = kargs[0] + self.obj = reduce(Mv.__xor__, [self.Ga.mv('%s%d' % (root, i), 'vector') for i in range(kargs[1])], self.Ga.mv(1, 'scalar')).obj + + def make_grade(self, *kargs, **kwargs): # Called by __init__ to make a pure grade multivector. @@ -268,7 +276,8 @@ def make_odd(self, *kargs, **kwargs): 'spinor': make_spinor, 'even': make_spinor, 'odd': make_odd, - 'grade': make_grade} + 'grade': make_grade, + 'blade': make_blade} def __init__(self, *kargs, **kwargs): @@ -761,7 +770,7 @@ def __xor__(self, A): # wedge (^) product if isinstance(A, Dop): return A.Mul(self, A, op='^') - if self.is_scalar(): + if self.is_scalar() or isinstance(A, numbers.Number) or A.is_scalar(): return self * A self = self.blade_rep() diff --git a/tests/test_chapter2.py b/tests/test_chapter2.py index 2ad5bb50..3542521b 100755 --- a/tests/test_chapter2.py +++ b/tests/test_chapter2.py @@ -11,38 +11,40 @@ def test2_3_2(self): """ Introducing the outer product. """ - R, e_1, e_2, e_3 = Ga.build('e*1|2|3') + GA = Ga('e*1|2|3') - a = R.mv('a', 'vector') - b = R.mv('b', 'vector') - c = R.mv('c', 'vector') + a = GA.mv('a', 'vector') + b = GA.mv('b', 'vector') + c = GA.mv('c', 'vector') alpha = Symbol('alpha') self.assertEquals(a ^ b, -b ^ a) self.assertEquals(a ^ (alpha * b), alpha * (a ^ b)) - self.assertEquals(R.mv(alpha, 'scalar') ^ b, alpha * b) + self.assertEquals(GA.mv(alpha, 'scalar') ^ b, alpha * b) self.assertEquals(a ^ (b + c), (a ^ b) + (a ^ c)) def test2_9_1(self): """ - Blades and grades. + Blades and grades. Be careful ga.grade_decomposition can't be used to know if a multivector is a blade, + but if we know a multivector is a blade we can retrieve its grade (not fast). + TODO: add a proper grade method to ga module or fix pure_grade... """ - R, e_1, e_2, e_3, e_4, e_5 = Ga.build('e*1|2|3|4|5') + GA = Ga('e*1|2|3|4|5') - # Check for k in [1, R.n] - for k in range(1, R.n + 1): - Ak = R.mv('a', k, 'grade') - grades = R.grade_decomposition(Ak) + # Check for k in [0, R.n] + for k in range(GA.n + 1): + Ak = GA.mv('A', 'blade', k) + grades = GA.grade_decomposition(Ak) self.assertEquals(len(grades), 1) self.assertEquals(grades.keys()[0], k) - # Check for k and l in in [1, R.n] - for k, l in product(range(1, R.n + 1), range(1, R.n + 1)): - Ak = R.mv('a', k, 'grade') - Bl = R.mv('b', l, 'grade') + # Check for k and l in [0, R.n] + for k, l in product(range(GA.n + 1), range(GA.n + 1)): + Ak = GA.mv('A', 'blade', k) + Bl = GA.mv('B', 'blade', l) C = Ak ^ Bl - grades = R.grade_decomposition(C) + grades = GA.grade_decomposition(C) self.assertEquals(len(grades), 1) self.assertEquals(grades.keys()[0], 0 if C == 0 else k + l) @@ -51,10 +53,10 @@ def test2_9_5(self): """ Reversion and grade involution. """ - R, e_1, e_2, e_3, e_4 = Ga.build('e*1|2|3|4') + GA = Ga('e*1|2|3|4') - for k in range(1, R.n + 1): - a = [R.mv('a%d' % i, 'vector') for i in range(k)] + for k in range(1, GA.n + 1): + a = [GA.mv('a%d' % i, 'vector') for i in range(k)] A = reduce(Mv.__xor__, a) A_rev = reduce(Mv.__xor__, reversed(a)) self.assertEquals(A_rev, A.rev()) @@ -66,7 +68,7 @@ def test2_12_1_1(self): Compute the outer products of the following 3-space expressions, giving the result relative to the basis {1, e_1, e_2, e_3, e_1^e_2, e_1^e_3, e_2^e_3, e_1^e_2^e_3}. """ - R, e_1, e_2, e_3 = Ga.build('e*1|2|3') + GA, e_1, e_2, e_3 = Ga.build('e*1|2|3') self.assertTrue((e_1 + e_2) ^ (e_1 + e_3) == (-e_1 ^ e_2) + (e_1 ^ e_3) + (e_2 ^ e_3)) self.assertTrue((e_1 + e_2 + e_3) ^ (2*e_1) == -2*(e_1 ^ e_2) - 2*(e_1 ^ e_3)) self.assertTrue((e_1 - e_2) ^ (e_1 - e_3) == (e_1 ^ e_2) - (e_1 ^ e_3) + (e_2 ^ e_3)) @@ -80,7 +82,7 @@ def test2_12_1_2(self): Given the 2-blade B = e_1 ^ (e_2 - e_3) that represents a plane, determine if each of the following vectors lies in that plane. """ - R, e_1, e_2, e_3 = Ga.build('e*1|2|3') + GA, e_1, e_2, e_3 = Ga.build('e*1|2|3') B = e_1 ^ (e_2 - e_3) self.assertTrue(e_1 ^ B == 0) self.assertFalse((e_1 + e_2) ^ B == 0) @@ -93,7 +95,7 @@ def test2_12_1_3(self): What is the area of the parallelogram spanned by the vectors a = e_1 + 2*e_2 and b = -e_1 - e_2 (relative to the area of e_1 ^ e_2) ? """ - R, e_1, e_2, e_3 = Ga.build('e*1|2|3') + GA, e_1, e_2, e_3 = Ga.build('e*1|2|3') a = e_1 + 2*e_2 b = -e_1 - e_2 B = a ^ b @@ -106,7 +108,7 @@ def test2_12_1_4(self): and direction vector e_2, and the line M with position vector e_2 and direction vector (e_1 + e_2), using 2-blades. """ - R, e_1, e_2 = Ga.build('e*1|2') + GA, e_1, e_2 = Ga.build('e*1|2') # x is defined in the basis {e_1, e_2} a = Symbol('a') @@ -130,13 +132,13 @@ def test2_12_1_5(self): """ Compute (2 + 3 * e_3) ^ (e_1 + (e_2 ^ e_3) using the grade based defining equations of section 2.9.4. """ - R, e_1, e_2, e_3 = Ga.build('e*1|2|3') + GA, e_1, e_2, e_3 = Ga.build('e*1|2|3') A = 2 + 3 * e_3 B = e_1 + (e_2 ^ e_3) - C = R.mv(0, 'scalar') - for k, l in product(range(R.n + 1), range(R.n + 1)): + C = GA.mv(0, 'scalar') + for k, l in product(range(GA.n + 1), range(GA.n + 1)): C += A.get_grade(k) ^ B.get_grade(l) self.assertTrue(C == (2 * e_1 + 3 * (e_3 ^ e_1) + 2 * (e_2 ^ e_3))) @@ -148,7 +150,7 @@ def test2_12_2_1(self): Write x = a * e_1 and y = b * (cos(t) * e_1 + sin(t) * e_2), whete t is the angle from a to b. Evaluate the outer product. What is the geometrical interpretation ? """ - R, e_1, e_2 = Ga.build('e*1|2', g='1 0, 0 1') + GA, e_1, e_2 = Ga.build('e*1|2', g='1 0, 0 1') # TODO: use alpha, beta and theta instead of a, b and t (it crashes sympy) a = Symbol('a') @@ -171,8 +173,9 @@ def test2_12_2_1(self): def test2_12_2_2(self): - - R, e_1, e_2 = Ga.build('e*1|2', g='1 0, 0 1') + """ + """ + GA, e_1, e_2 = Ga.build('e*1|2', g='1 0, 0 1') a = Symbol('a') b = Symbol('b') @@ -194,7 +197,7 @@ def test2_12_2_4(self): """ Solve the linear system of equation using the outer product. """ - R, e_1, e_2, e_3 = Ga.build('e*1|2|3') + GA, e_1, e_2, e_3 = Ga.build('e*1|2|3') a_1 = Symbol('a_1') a_2 = Symbol('a_2') @@ -235,7 +238,7 @@ def test2_12_2_5(self): Consider R4 with basis {e_1, e_2, e_3, e_4}. Show that the 2-vector B = (e_1 ^ e_2) + (e_3 ^ e_4) is not a 2-blade (i.e., it cannot be written as the outer product of two vectors). """ - R, e_1, e_2, e_3, e_4 = Ga.build('e*1|2|3|4') + GA, e_1, e_2, e_3, e_4 = Ga.build('e*1|2|3|4') # B B = (e_1 ^ e_2) + (e_3 ^ e_4) @@ -281,7 +284,7 @@ def test2_12_2_6(self): """ Show that B = e1 ^ e2 + e3 ^ e4 of the previous exercise doesn't contain any other vector than 0. """ - R, e_1, e_2, e_3, e_4 = Ga.build('e*1|2|3|4') + GA, e_1, e_2, e_3, e_4 = Ga.build('e*1|2|3|4') # B B = (e_1 ^ e_2) + (e_3 ^ e_4) @@ -314,15 +317,11 @@ def test2_12_2_9(self): """ Prove Ak ^ Bl = (-1**kl) Bl ^ Ak. """ - - # TODO: don't fix the vector space dimension - R, e_1, e_2, e_3, e_4, e_5 = Ga.build('e*1|2|3|4|5') - - # Prove for k in [1, 5] and l in [1, 5] - for k, l in product(range(1, R.n + 1), range(1, R.n + 1)): - Ak = R.mv('a', k, 'grade') - Bl = R.mv('b', l, 'grade') - self.assertTrue((Ak ^ Bl) == (-1)**(k*l) * (Bl ^ Ak)) + for GA in [Ga('e*1|2'), Ga('e*1|2|3'), Ga('e*1|2|3|4')]: #, Ga('e*1|2|3|4|5')]: + for k, l in product(range(GA.n + 1), range(GA.n + 1)): + Ak = GA.mv('A', 'blade', k) + Bl = GA.mv('B', 'blade', l) + self.assertEquals(Ak ^ Bl, (-1)**(k * l) * (Bl ^ Ak)) if __name__ == '__main__': diff --git a/tests/test_chapter3.py b/tests/test_chapter3.py index 3e8c63b7..a00b9fce 100644 --- a/tests/test_chapter3.py +++ b/tests/test_chapter3.py @@ -26,15 +26,14 @@ def test_3_1_2(self): """ Definition of the scalar product. """ - - R = Ga('e*1|2|3') - A_blades = [R.mv('A', i, 'grade') for i in range(R.n + 1)] - B_blades = [R.mv('B', i, 'grade') for i in range(R.n + 1)] + GA = Ga('e*1|2|3') + A_blades = [GA.mv('A', 'blade', k) for k in range(GA.n + 1)] + B_blades = [GA.mv('B', 'blade', l) for l in range(GA.n + 1)] # GAlgebra doesn't define any scalar product but rely on the geometric product instead for A, B in product(A_blades, B_blades): - A_grades = R.grade_decomposition(A).keys() - B_grades = R.grade_decomposition(B).keys() + A_grades = GA.grade_decomposition(A).keys() + B_grades = GA.grade_decomposition(B).keys() self.assertEquals(len(A_grades), 1) self.assertEquals(len(B_grades), 1) if A_grades[0] == B_grades[0]: @@ -47,11 +46,10 @@ def test_3_2_2(self): """ Computing the contraction explicitly. """ - - R = Ga('e*1|2|3') - A_blades = [R.mv('A', i, 'grade') for i in range(R.n + 1)] - B_blades = [R.mv('B', i, 'grade') for i in range(R.n + 1)] - C_blades = [R.mv('C', i, 'grade') for i in range(R.n + 1)] + GA = Ga('e*1|2|3') + A_blades = [GA.mv('A', 'blade', k) for k in range(GA.n + 1)] + B_blades = [GA.mv('B', 'blade', l) for l in range(GA.n + 1)] + C_blades = [GA.mv('C', 'blade', m) for m in range(GA.n + 1)] # scalar and blades of various grades A = A_blades[0] @@ -60,7 +58,7 @@ def test_3_2_2(self): A = A_blades[0] for B in B_blades: - B_grades = R.grade_decomposition(B).keys() + B_grades = GA.grade_decomposition(B).keys() self.assertEquals(len(B_grades), 1) self.assertEquals(B < A, 0 if B_grades[0] else A * B) @@ -72,7 +70,7 @@ def test_3_2_2(self): # vector and the outer product of 2 blades of various grades (scalars, vectors, 2-vectors...) A = A_blades[1] for B, C in product(B_blades, C_blades): - B_grades = R.grade_decomposition(B).keys() + B_grades = GA.grade_decomposition(B).keys() self.assertEquals(len(B_grades), 1) self.assertEquals(A < (B ^ C), ((A < B) ^ C) + (-1)**B_grades[0] * (B ^ (A < C))) @@ -92,7 +90,7 @@ def test_3_2_2(self): self.assertEquals((alpha * A) < B, alpha * (A < B)) self.assertEquals((alpha * A) < B, A < (alpha * B)) - a = R.mv('a', 1, 'grade') + a = GA.mv('a', 'blade', 1) for A_minus1, B in product(A_blades[:-1], B_blades): A = A_minus1 ^ a self.assertEquals(A < B, (A_minus1 ^ a) < B) @@ -103,23 +101,22 @@ def test_3_4(self): """ The other contraction. """ - - R = Ga('e*1|2|3') - A_blades = [R.mv('A', i, 'grade') for i in range(R.n + 1)] - B_blades = [R.mv('B', i, 'grade') for i in range(R.n + 1)] + GA = Ga('e*1|2|3') + A_blades = [GA.mv('A', 'blade', k) for k in range(GA.n + 1)] + B_blades = [GA.mv('B', 'blade', l) for l in range(GA.n + 1)] for A, B in product(A_blades, B_blades): - A_grades = R.grade_decomposition(A).keys() - B_grades = R.grade_decomposition(B).keys() + A_grades = GA.grade_decomposition(A).keys() + B_grades = GA.grade_decomposition(B).keys() self.assertEquals(len(A_grades), 1) self.assertEquals(len(B_grades), 1) self.assertEquals(B > A, ((-1) ** (A_grades[0] * (B_grades[0] - 1))) * (A < B)) for A, B in product(A_blades, B_blades): C = B > A - A_grades = R.grade_decomposition(A).keys() - B_grades = R.grade_decomposition(B).keys() - C_grades = R.grade_decomposition(C).keys() + A_grades = GA.grade_decomposition(A).keys() + B_grades = GA.grade_decomposition(B).keys() + C_grades = GA.grade_decomposition(C).keys() self.assertEquals(len(A_grades), 1) self.assertEquals(len(B_grades), 1) self.assertEquals(len(C_grades), 1) @@ -130,9 +127,8 @@ def test_3_5_2(self): """ The inverse of a blade. """ - - for R in [Ga('e*1|2'), Ga('e*1|2|3'), Ga('e*1|2|3|4')]: # , Ga('e*1|2|3|4|5')]: - A_grade_and_blades = [(k, R.mv('A', k, 'grade')) for k in range(R.n + 1)] + for GA in [Ga('e*1|2'), Ga('e*1|2|3'), Ga('e*1|2|3|4')]: # , Ga('e*1|2|3|4|5')]: + A_grade_and_blades = [(k, GA.mv('A', 'blade', k)) for k in range(GA.n + 1)] for k, A in A_grade_and_blades: inv_A = A.inv() rev_A = A.rev() @@ -152,38 +148,36 @@ def test_3_5_3(self): """ Orthogonal complement and duality. """ - Ga.dual_mode("Iinv+") # some blades by grades for each space - spaces = [([R.mv('A', i, 'grade') for i in range(R.n + 1)], R) for R in [Ga('e*1|2'), Ga('e*1|2|3'), Ga('e*1|2|3|4')]] + spaces = [([GA.mv('A', 'blade', k) for k in range(GA.n + 1)], R) for R in [Ga('e*1|2'), Ga('e*1|2|3'), Ga('e*1|2|3|4')]] # dualization for blades, R in spaces: for A in blades: - self.assertEquals(A.dual(), A < R.I_inv()) + self.assertEquals(A.dual(), A < GA.I_inv()) # dualization sign for blades, R in spaces: for A in blades: - self.assertEquals(A.dual().dual(), ((-1) ** (R.n * (R.n - 1) / 2)) * A) + self.assertEquals(A.dual().dual(), ((-1) ** (GA.n * (GA.n - 1) / 2)) * A) # undualization for blades, R in spaces: for A in blades: - self.assertEquals(A, A.dual() < R.I()) + self.assertEquals(A, A.dual() < GA.I()) def test_3_5_4(self): """ The duality relationships. """ - Ga.dual_mode("Iinv+") R = Ga('e*1|2|3') - A_blades = [R.mv('A', i, 'grade') for i in range(R.n + 1)] - B_blades = [R.mv('B', i, 'grade') for i in range(R.n + 1)] + A_blades = [GA.mv('A', 'blade', k) for k in range(GA.n + 1)] + B_blades = [GA.mv('B', 'blade', l) for l in range(GA.n + 1)] for A, B in product(A_blades, B_blades): self.assertEquals((A ^ B).dual(), A < B.dual()) @@ -196,10 +190,9 @@ def test_3_6(self): """ Orthogonal projection of subspaces. """ - - R = Ga('e*1|2|3') - X_blades = [R.mv('X', i, 'grade') for i in range(R.n + 1)] - B_blades = [R.mv('B', i, 'grade') for i in range(R.n + 1)] + GA = Ga('e*1|2|3') + X_blades = [GA.mv('X', 'blade', k) for k in range(GA.n + 1)] + B_blades = [GA.mv('B', 'blade', l) for l in range(GA.n + 1)] # projection of X on B def P(X, B): @@ -218,31 +211,30 @@ def test3_7_2(self): """ The cross product incorporated. """ - Ga.dual_mode("Iinv+") - R, e_1, e_2, e_3 = Ga.build('e*1|2|3') - a = R.mv('a', 'vector') - b = R.mv('b', 'vector') + GA = Ga('e*1|2|3') + a = GA.mv('a', 'vector') + b = GA.mv('b', 'vector') - A = a < R.I() - B = b < R.I() + A = a < GA.I() + B = b < GA.I() # Cross product definition - self.assertEquals(R.I_inv(), -R.I()) + self.assertEquals(GA.I_inv(), -GA.I()) self.assertEquals(cross(a, b), (a ^ b).dual()) # About velocities - self.assertEquals(cross(a, b), (b ^ a) < R.I()) - self.assertEquals(cross(a, b), (a ^ b) < R.I_inv()) + self.assertEquals(cross(a, b), (b ^ a) < GA.I()) + self.assertEquals(cross(a, b), (a ^ b) < GA.I_inv()) self.assertEquals(cross(a, b), -(b ^ a).dual()) self.assertEquals(cross(a, b), -b < a.dual()) self.assertEquals(cross(a, b), b < A) # Intersecting planes - self.assertEquals(cross(a, b), ((A < R.I_inv()) ^ (B < R.I_inv())) < R.I_inv()) - self.assertEquals(cross(a, b), (B < R.I_inv()) < ((A < R.I_inv()) < R.I())) - self.assertEquals(cross(a, b), (B < R.I_inv()) < A) + self.assertEquals(cross(a, b), ((A < GA.I_inv()) ^ (B < GA.I_inv())) < GA.I_inv()) + self.assertEquals(cross(a, b), (B < GA.I_inv()) < ((A < GA.I_inv()) < GA.I())) + self.assertEquals(cross(a, b), (B < GA.I_inv()) < A) self.assertEquals(cross(a, b), B.dual() < A) @@ -252,11 +244,9 @@ def test_3_10_1_1(self): Compute the following expressions, giving the results relative to the basis {1, e_1, e_2, e_3, e_1 ^ e_2, e_2 ^ e_3, e_3 ^ e_1, e_1 ^ e_2 ^ e_3}. """ - Ga.dual_mode("Iinv+") - R, e_1, e_2, e_3 = Ga.build('e*1|2|3', g='1 0 0, 0 1 0, 0 0 1') - + GA, e_1, e_2, e_3 = Ga.build('e*1|2|3', g='1 0 0, 0 1 0, 0 0 1') a = e_1 + e_2 b = e_2 + e_3 @@ -299,35 +289,34 @@ def test_3_10_1_1(self): self.assertEquals(a < (e_1 ^ e_2 ^ e_3), (e_2 ^ e_3) + (e_3 ^ e_1)) # f - self.assertEquals(a < R.I_inv(), a < (-e_1 ^ e_2 ^ e_3)) # equals because we fixed the metric - self.assertEquals(a < R.I_inv(), a < ((e_1 ^ e_2) ^ (-e_3))) # almost last drill - self.assertEquals(a < R.I_inv(), -(e_2 ^ e_3) - (e_3 ^ e_1)) + self.assertEquals(a < GA.I_inv(), a < (-e_1 ^ e_2 ^ e_3)) # equals because we fixed the metric + self.assertEquals(a < GA.I_inv(), a < ((e_1 ^ e_2) ^ (-e_3))) # almost last drill + self.assertEquals(a < GA.I_inv(), -(e_2 ^ e_3) - (e_3 ^ e_1)) # g - self.assertEquals((a ^ b) < R.I_inv(), -((b ^ a) < R.I_inv())) - self.assertEquals((a ^ b) < R.I_inv(), -(b < (a < R.I_inv()))) - self.assertEquals((a ^ b) < R.I_inv(), -((e_2 + e_3) < (-(e_2 ^ e_3) + (e_1 ^ e_3)))) - self.assertEquals((a ^ b) < R.I_inv(), ((e_2 + e_3) < (e_2 ^ e_3)) - ((e_2 + e_3) < (e_1 ^ e_3))) - self.assertEquals((a ^ b) < R.I_inv(), ((e_2 < (e_2 ^ e_3)) + (e_3 < (e_2 ^ e_3)) - (e_2 < (e_1 ^ e_3)) - (e_3 < (e_1 ^ e_3)))) + self.assertEquals((a ^ b) < GA.I_inv(), -((b ^ a) < GA.I_inv())) + self.assertEquals((a ^ b) < GA.I_inv(), -(b < (a < GA.I_inv()))) + self.assertEquals((a ^ b) < GA.I_inv(), -((e_2 + e_3) < (-(e_2 ^ e_3) + (e_1 ^ e_3)))) + self.assertEquals((a ^ b) < GA.I_inv(), ((e_2 + e_3) < (e_2 ^ e_3)) - ((e_2 + e_3) < (e_1 ^ e_3))) + self.assertEquals((a ^ b) < GA.I_inv(), ((e_2 < (e_2 ^ e_3)) + (e_3 < (e_2 ^ e_3)) - (e_2 < (e_1 ^ e_3)) - (e_3 < (e_1 ^ e_3)))) self.assertEquals(e_2 < (e_2 ^ e_3), e_3) self.assertEquals(e_3 < (e_2 ^ e_3), -e_2) self.assertEquals(e_2 < (e_1 ^ e_3), 0) self.assertEquals(e_3 < (e_1 ^ e_3), -e_1) - self.assertEquals((a ^ b) < R.I_inv(), e_3 - e_2 + e_1) + self.assertEquals((a ^ b) < GA.I_inv(), e_3 - e_2 + e_1) # h - self.assertEquals(a < (b < R.I_inv()), (a ^ b) < R.I_inv()) - self.assertEquals(a < (b < R.I_inv()), e_3 - e_2 + e_1) + self.assertEquals(a < (b < GA.I_inv()), (a ^ b) < GA.I_inv()) + self.assertEquals(a < (b < GA.I_inv()), e_3 - e_2 + e_1) def test_3_10_1_2(self): """ Compute the cosine of the angle between the following subspaces given on an orthonormal basis of a Euclidean space. """ - Ga.dual_mode("Iinv+") - R, e_1, e_2, e_3, e_4 = Ga.build('e*1|2|3|4', g='1 0 0 0, 0 1 0 0, 0 0 1 0, 0 0 0 1') + GA, e_1, e_2, e_3, e_4 = Ga.build('e*1|2|3|4', g='1 0 0 0, 0 1 0 0, 0 0 1 0, 0 0 0 1') alpha = Symbol('alpha', real=True) theta = Symbol('theta', real=True) @@ -402,13 +391,11 @@ def test3_10_2_1(self): coefficient of the corresponding basis element in the final result. Show that e1 < (e1 ^ e2) = (0)1 + 0(e1) + 1(e2) + 0(e1 ^ e2). """ - - R, e_1, e_2 = Ga.build('e*1|2', g='1 0, 0 1') - + GA, e_1, e_2 = Ga.build('e*1|2', g='1 0, 0 1') A = e_1 B = e_1 ^ e_2 - X = R.mv(1) + X = GA.mv(1) self.assertEquals(((X ^ A) * B).scalar(), (X * (A < B)).scalar()) self.assertEquals(((X ^ A) * B).scalar(), 0) @@ -431,8 +418,7 @@ def test3_10_2_2(self): of e1 < (e1 ^ e2) like the previous exercise. The use the explicit definition of the contraction to show the contraction is still well defined, and equal to e1 < (e1 ^ e2) == e2. """ - - R, e_1, e_2 = Ga.build('e*1|2', g='1 0, 0 0') + GA, e_1, e_2 = Ga.build('e*1|2', g='1 0, 0 0') # We can't use the scalar product anymore (because of the metric) A = e_1 @@ -441,14 +427,14 @@ def test3_10_2_2(self): self.assertEquals(((X ^ A) * B).scalar(), (X * (A < B)).scalar()) self.assertEquals(((X ^ A) * B).scalar(), 0) # Which is false - e_1_grades = R.grade_decomposition(e_1).keys() + e_1_grades = GA.grade_decomposition(e_1).keys() self.assertEquals(len(e_1_grades), 1) self.assertEquals(e_1_grades[0], 1) # We use the explicit definition of the contraction instead self.assertEquals(e_1 < (e_1 ^ e_2), ((e_1 < e_1) ^ e_2) + ((-1) ** e_1_grades[0]) * (e_1 ^ (e_1 < e_2))) # We can't use the definition a < b = a . b using GAlgebra so we solved it ourselves... - self.assertEquals(e_1 < (e_1 ^ e_2), (R.mv(1) ^ e_2) + ((-1) ** e_1_grades[0]) * (e_1 ^ R.mv(0))) + self.assertEquals(e_1 < (e_1 ^ e_2), (GA.mv(1) ^ e_2) + ((-1) ** e_1_grades[0]) * (e_1 ^ GA.mv(0))) self.assertEquals(e_1 < (e_1 ^ e_2), e_2) # Which is true @@ -457,12 +443,11 @@ def test3_10_2_3(self): Derive the following dualities for right contraction : C > (B ^ A) = (C > B) > A and C > (B > A) = (C > B) ^ A when A included in C. """ + GA = Ga('e*1|2|3') - R, e_1, e_2, e_3 = Ga.build('e*1|2|3') - - A_blades = [R.mv('X', k, 'grade') for k in range(R.n + 1)] - B_blades = [R.mv('B', l, 'grade') for l in range(R.n + 1)] - C_blades = [R.mv('B', m, 'grade') for m in range(R.n + 1)] + A_blades = [GA.mv('X', 'blade', k) for k in range(GA.n + 1)] + B_blades = [GA.mv('B', 'blade', l) for l in range(GA.n + 1)] + C_blades = [GA.mv('B', 'blade', m) for m in range(GA.n + 1)] for A, B, C in product(A_blades, B_blades, C_blades): M = C > (B ^ A) @@ -481,10 +466,10 @@ def test3_10_2_11(self): """ Ga.dual_mode("Iinv+") - R, e_1, e_2, e_3 = Ga.build('e*1|2|3') - a = R.mv('a', 'vector') - b = R.mv('b', 'vector') - c = R.mv('c', 'vector') + GA = Ga('e*1|2|3') + a = GA.mv('a', 'vector') + b = GA.mv('b', 'vector') + c = GA.mv('c', 'vector') xx = cross(a, cross(b, c)) self.assertEquals(xx, (a ^ (b ^ c).dual()).dual()) diff --git a/tests/test_chapter6.py b/tests/test_chapter6.py index 462f70f0..b2c56211 100644 --- a/tests/test_chapter6.py +++ b/tests/test_chapter6.py @@ -1,12 +1,13 @@ import unittest from itertools import product -from sympy import simplify, Symbol, cos, sin, sqrt +from sympy import simplify, Symbol, solve_poly_system from ga import Ga -from mv import Mv, cross +from printer import GaLatexPrinter +from mv import Mv, com -class TestChapter3(unittest.TestCase): +class TestChapter6(unittest.TestCase): def assertEquals(self, first, second, msg=None): """ @@ -19,14 +20,16 @@ def assertEquals(self, first, second, msg=None): if isinstance(second, Mv): second = second.obj - self.assertTrue(simplify(first - second) == 0, "%s == %s" % (first, second)) + diff = simplify(first - second) + + self.assertTrue(diff == 0, "\n%s\n==\n%s\n%s" % (first, second, diff)) def test6_1_4_1(self): """ The geometric product for vectors on a basis. """ - R, e_1, e_2, e_3 = Ga.build('e*1|2|3', g='1 0 0, 0 1 0, 0 0 1') + GA, e_1, e_2, e_3 = Ga.build('e*1|2|3', g='1 0 0, 0 1 0, 0 0 1') self.assertEquals(e_1 * e_1, 1) self.assertEquals(e_1 * e_2, e_1 ^ e_2) @@ -52,8 +55,8 @@ def test6_1_4_2(self): """ The geometric product for vectors on a basis. """ - R, e_1, e_2 = Ga.build('e*1|2', g='1 0, 0 1') - ONE = R.mv(1, 'scalar') + GA, e_1, e_2 = Ga.build('e*1|2', g='1 0, 0 1') + ONE = GA.mv(1, 'scalar') e_12 = e_1 ^ e_2 self.assertEquals(ONE * ONE, 1) @@ -78,11 +81,11 @@ def test6_1_4_2(self): a_1 = Symbol('a_1') a_2 = Symbol('a_2') - a = R.mv((a_1, a_2), 'vector') + a = GA.mv((a_1, a_2), 'vector') b_1 = Symbol('b_1') b_2 = Symbol('b_2') - b = R.mv((b_1, b_2), 'vector') + b = GA.mv((b_1, b_2), 'vector') self.assertEquals(a * b, a_1 * b_1 + a_2 * b_2 + (a_1 * b_2 - a_2 * b_1) * (e_1 ^ e_2)) @@ -91,34 +94,31 @@ def test6_3_1(self): """ The subspace products from symmetry. """ - R = Ga('e*1|2|3') - - a = R.mv('a', 'vector') - B_blades = [R.mv('B%d' % k, k, 'grade') for k in range(R.n + 1)] - - def hat(M): - M_grades = R.grade_decomposition(M).keys() - self.assertEquals(len(M_grades), 1) - return ((-1) ** M_grades[0]) * M - - for B in B_blades: - self.assertEquals(a ^ B, 0.5 * (a * B + hat(B) * a)) - self.assertEquals(B ^ a, 0.5 * (B * a + a * hat(B))) - self.assertEquals(a < B, 0.5 * (a * B - hat(B) * a)) - self.assertEquals(B > a, 0.5 * (B * a - a * hat(B))) + GA = Ga('e*1|2|3') + a = GA.mv('a', 'vector') + B_grade_and_blades = [(k, GA.mv('B%d' % k, 'blade', k)) for k in range(GA.n + 1)] + + def hat(k, M): + return ((-1) ** k) * M + + for k, B in B_grade_and_blades: + self.assertEquals(a ^ B, 0.5 * (a * B + hat(k, B) * a)) + self.assertEquals(B ^ a, 0.5 * (B * a + a * hat(k, B))) + self.assertEquals(a < B, 0.5 * (a * B - hat(k, B) * a)) + self.assertEquals(B > a, 0.5 * (B * a - a * hat(k, B))) self.assertEquals(a * B, (a < B) + (a ^ B)) - for B in B_blades: - self.assertEquals(hat(B) * a, (hat(B) > a) + (hat(B) ^ a)) - self.assertEquals(hat(B) * a, -(a < B) + (a ^ B)) - self.assertEquals(hat(B) * a, a * B - 2 * (a < B)) - self.assertEquals(hat(B) * a, -(a * B) + 2 * (a ^ B)) + for k, B in B_grade_and_blades: + self.assertEquals(hat(k, B) * a, (hat(k, B) > a) + (hat(k, B) ^ a)) + self.assertEquals(hat(k, B) * a, -(a < B) + (a ^ B)) + self.assertEquals(hat(k, B) * a, a * B - 2 * (a < B)) + self.assertEquals(hat(k, B) * a, -(a * B) + 2 * (a ^ B)) - for B in B_blades: - self.assertEquals(a * B, hat(B) * a + 2 * (a < B)) - self.assertEquals(a * B, -hat(B) * a + 2 * (a ^ B)) + for k, B in B_grade_and_blades: + self.assertEquals(a * B, hat(k, B) * a + 2 * (a < B)) + self.assertEquals(a * B, -hat(k, B) * a + 2 * (a ^ B)) - M = R.mv('m', 'mv') + M = GA.mv('m', 'mv') self.assertEquals(a * M, (a < M) + (a ^ M)) @@ -127,17 +127,18 @@ def test6_3_2(self): The subspace products as selected grades. """ - R = Ga('e*1|2|3') + GA = Ga('e*1|2|3') - A_blades = [R.mv('A%d' % k, k, 'grade') for k in range(R.n + 1)] - B_blades = [R.mv('B%d' % l, l, 'grade') for l in range(R.n + 1)] + # This test should work for blades and other graded multivectors + A_grades = [GA.mv('A%d' % k, 'grade', k) for k in range(R.n + 1)] + B_grades = [GA.mv('B%d' % l, 'grade', l) for l in range(R.n + 1)] def grade(M): - M_grades = R.grade_decomposition(M).keys() + M_grades = GA.grade_decomposition(M).keys() self.assertEquals(len(M_grades), 1) return M_grades[0] - for A, B in product(A_blades, B_blades): + for A, B in product(A_grades, B_grades): k = grade(A) l = grade(B) self.assertEquals(A ^ B, (A * B).get_grade(k + l)) @@ -152,15 +153,13 @@ def test6_6_2_3(self): of the geometric product of its factors, with a sign for each term depending on oddness or evenness of the permutation. Derive x ^ y ^ z = 1/3! * (xyz - yxz + yzx - zyx + zxy - xzy) """ - - R = Ga('e*1|2|3') - - x = R.mv('x', 'vector') - y = R.mv('y', 'vector') - z = R.mv('z', 'vector') + GA = Ga('e*1|2|3') + x = GA.mv('x', 'vector') + y = GA.mv('y', 'vector') + z = GA.mv('z', 'vector') def hat(M): - M_grades = R.grade_decomposition(M).keys() + M_grades = GA.grade_decomposition(M).keys() self.assertEquals(len(M_grades), 1) return ((-1) ** M_grades[0]) * M @@ -211,3 +210,99 @@ def hat(M): self.assertEquals(x * y * z - y * x * z + y * z * x - z * y * x + z * x * y - x * z * y, 6 * (x ^ y ^ z)) + + def test6_6_2_4(self): + """ + The parts of a certain grade of a geometric product of blades are not necessarily blades. + Show that in a 4D space with orthogonal basis, a counterexample is the grade 2 of the product + e1 * (e1 + e2) * (e2 + e3) * (e1 + e4). + """ + GA, e_1, e_2, e_3, e_4 = Ga.build('e*1|2|3|4', g='1 0 0 0, 0 1 0 0, 0 0 1 0, 0 0 0 1') + M = e_1 * (e_1 + e_2) * (e_2 + e_3) * (e_1 + e_4) + self.assertFalse(M.get_grade(2).is_blade()) + + + def test6_6_2_6(self): + """ + Prove (X ^ A) * B = X * (A < B) using the grade based definition of ^, * and <. + """ + GA = Ga('e*1|2|3') + X_grade_blades = [(j, GA.mv('X', 'blade', j)) for j in range(GA.n + 1)] + A_grade_blades = [(k, GA.mv('A', 'blade', k)) for k in range(GA.n + 1)] + B_grade_blades = [(l, GA.mv('B', 'blade', l)) for l in range(GA.n + 1)] + + for (j, X), (k, A), (l, B) in product(X_grade_blades, A_grade_blades, B_grade_blades): + self.assertEquals((X * A).get_grade(j + k), X ^ A) + self.assertEquals((A * B).get_grade(l - k), 0 if k > l else A < B) + self.assertTrue(((X * A).get_grade(j + k) * B).get_grade(0) != 0 if j + k == l else True) + self.assertTrue((X * (A * B).get_grade(l - k)).get_grade(0) != 0 if j == l - k else True) + self.assertEquals(((X * A).get_grade(j + k) * B).get_grade(0), (X * (A * B).get_grade(l - k)).get_grade(0)) + + + def test6_6_2_7(self): + """ + In the formula (x < (1/A)) * A, show we can replace the geometric product by a contraction, so that + it is in fact the projection (x < (1/A)) < A. + """ + GA = Ga('e*1|2|3|4', g='1 0 0 0, 0 1 0 0, 0 0 1 0, 0 0 0 1') + x = GA.mv('x', 'vector') + A_grade_and_blades = [(k, GA.mv('A', 'blade', k)) for k in range(0, GA.n + 1)] + + for k, A in A_grade_and_blades: + rev_sign = (-1) ** ((k * (k - 1)) / 2) + self.assertEquals(rev_sign * A, A.rev()) + M = x < A.inv() + self.assertEquals(M, x < (A.rev() / (A * A.rev()))) + self.assertEquals(M, x < ((rev_sign * A) / (A * (rev_sign * A)))) + self.assertEquals(M, x < (A / (A * A))) + self.assertEquals(M, (1 / (A * A).scalar()) * (x < A)) + + + def test6_6_2_9(self): + """ + In a 4D space with orthonormal basis {e1, e2, e3, e4}, project the 2-blade X = (e1 + e2) ^ (e3 + e4) onto + the 2-blade A = (e1 ^ e3). Then determine the rejection as the difference of X and its projection. Show + that is not a blade. + """ + GA, e_1, e_2, e_3, e_4 = Ga.build('e*1|2|3|4', g='1 0 0 0, 0 1 0 0, 0 0 1 0, 0 0 0 1') + X = (e_1 + e_2) ^ (e_3 + e_4) + A = (e_1 ^ e_3) + + # projection of X onto B + def proj(X, B): + return (X < B.inv()) < B + + P = proj(X, A) + R = X - P + + # A 2-blade + a_1 = Symbol('a_1') + a_2 = Symbol('a_2') + a_3 = Symbol('a_3') + a_4 = Symbol('a_4') + a = GA.mv((a_1, a_2, a_3, a_4), 'vector') + + b_1 = Symbol('b_1') + b_2 = Symbol('b_2') + b_3 = Symbol('b_3') + b_4 = Symbol('b_4') + b = GA.mv((b_1, b_2, b_3, b_4), 'vector') + S = a ^ b + + # Try to solve the system and show there is no solution + system = [ + (S_coef) - (R_coef) for S_coef, R_coef in zip(S.blade_coefs(), R.blade_coefs()) + ] + + unknowns = [ + a_1, a_2, a_3, a_4, b_1, b_2, b_3, b_4 + ] + + # TODO: use solve if sympy fix it + result = solve_poly_system(system, unknowns) + self.assertTrue(result is None) + + +if __name__ == '__main__': + + unittest.main() From a23387722bfb8360a8ca1afea2c1c5787a0b196f Mon Sep 17 00:00:00 2001 From: Sylvain Meunier Date: Mon, 7 Oct 2019 15:52:16 +0200 Subject: [PATCH 45/78] leo dorst book chapter 6 exercices (in progress) --- tests/test_chapter6.py | 55 +++++++++++++++++++++++++++++++----------- 1 file changed, 41 insertions(+), 14 deletions(-) diff --git a/tests/test_chapter6.py b/tests/test_chapter6.py index b2c56211..442e7f09 100644 --- a/tests/test_chapter6.py +++ b/tests/test_chapter6.py @@ -218,8 +218,34 @@ def test6_6_2_4(self): e1 * (e1 + e2) * (e2 + e3) * (e1 + e4). """ GA, e_1, e_2, e_3, e_4 = Ga.build('e*1|2|3|4', g='1 0 0 0, 0 1 0 0, 0 0 1 0, 0 0 0 1') - M = e_1 * (e_1 + e_2) * (e_2 + e_3) * (e_1 + e_4) - self.assertFalse(M.get_grade(2).is_blade()) + R = (e_1 * (e_1 + e_2) * (e_2 + e_3) * (e_1 + e_4)).get_grade(2) + + # A 2-blade + a_1 = Symbol('a_1') + a_2 = Symbol('a_2') + a_3 = Symbol('a_3') + a_4 = Symbol('a_4') + a = GA.mv((a_1, a_2, a_3, a_4), 'vector') + + b_1 = Symbol('b_1') + b_2 = Symbol('b_2') + b_3 = Symbol('b_3') + b_4 = Symbol('b_4') + b = GA.mv((b_1, b_2, b_3, b_4), 'vector') + S = a ^ b + + # Try to solve the system and show there is no solution + system = [ + S_coef - R_coef for S_coef, R_coef in zip(S.blade_coefs(), R.blade_coefs()) + ] + + unknowns = [ + a_1, a_2, a_3, a_4, b_1, b_2, b_3, b_4 + ] + + # TODO: use solve if sympy fix it + result = solve_poly_system(system, unknowns) + self.assertTrue(result is None) def test6_6_2_6(self): @@ -244,18 +270,19 @@ def test6_6_2_7(self): In the formula (x < (1/A)) * A, show we can replace the geometric product by a contraction, so that it is in fact the projection (x < (1/A)) < A. """ - GA = Ga('e*1|2|3|4', g='1 0 0 0, 0 1 0 0, 0 0 1 0, 0 0 0 1') - x = GA.mv('x', 'vector') - A_grade_and_blades = [(k, GA.mv('A', 'blade', k)) for k in range(0, GA.n + 1)] - for k, A in A_grade_and_blades: - rev_sign = (-1) ** ((k * (k - 1)) / 2) - self.assertEquals(rev_sign * A, A.rev()) - M = x < A.inv() - self.assertEquals(M, x < (A.rev() / (A * A.rev()))) - self.assertEquals(M, x < ((rev_sign * A) / (A * (rev_sign * A)))) - self.assertEquals(M, x < (A / (A * A))) - self.assertEquals(M, (1 / (A * A).scalar()) * (x < A)) + GA_list = [ + Ga('e*1|2', g='1 0, 0 1'), + Ga('e*1|2|3', g='1 0 0, 0 1 0, 0 0 1'), + Ga('e*1|2|3|4', g='1 0 0 0, 0 1 0 0, 0 0 1 0, 0 0 0 1'), + # Ga('e*1|2|3|4|5', g='1 0 0 0 0, 0 1 0 0 0, 0 0 1 0 0, 0 0 0 1 0, 0 0 0 0 1'), + ] + + for GA in GA_list: + x = GA.mv('x', 'vector') + A_grade_and_blades = [(k, GA.mv('A', 'blade', k)) for k in range(GA.n + 1)] + for k, A in A_grade_and_blades: + self.assertEquals((x < A.inv()) * A, (x < A.inv()) < A) def test6_6_2_9(self): @@ -291,7 +318,7 @@ def proj(X, B): # Try to solve the system and show there is no solution system = [ - (S_coef) - (R_coef) for S_coef, R_coef in zip(S.blade_coefs(), R.blade_coefs()) + S_coef - R_coef for S_coef, R_coef in zip(S.blade_coefs(), R.blade_coefs()) ] unknowns = [ From b6ee442b55b364094dc78929f96e4bf91dccad7b Mon Sep 17 00:00:00 2001 From: Sylvain Meunier Date: Mon, 7 Oct 2019 18:55:19 +0200 Subject: [PATCH 46/78] leo dorst book chapter 7 exercices (in progress) --- tests/test_chapter7.py | 104 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 104 insertions(+) create mode 100644 tests/test_chapter7.py diff --git a/tests/test_chapter7.py b/tests/test_chapter7.py new file mode 100644 index 00000000..9987afc5 --- /dev/null +++ b/tests/test_chapter7.py @@ -0,0 +1,104 @@ +import unittest + +from sympy import simplify, Symbol, pi, cos, sin, solve, sqrt, Rational, Mod +from ga import Ga +from mv import Mv + + +class TestChapter7(unittest.TestCase): + + def assertEquals(self, first, second, msg=None): + """ + Compare two expressions are equals. + """ + + if isinstance(first, Mv): + first = first.obj + + if isinstance(second, Mv): + second = second.obj + + diff = simplify(first - second) + + self.assertTrue(diff == 0, "\n%s\n==\n%s\n%s" % (first, second, diff)) + + def test7_9_1(self): + """ + Drills. + """ + Ga.dual_mode("Iinv+") + + GA, e_1, e_2, e_3 = Ga.build("e*1|2|3", g='1 0 0, 0 1 0, 0 0 1') + + # .1 Compute R1 + R1 = ((e_1 ^ e_2) * (-pi / 4)).exp() + self.assertEquals(R1 * e_1 * R1.rev(), e_2) + + # .2 Compute R2 + R2 = ((e_3 ^ e_1) * (pi / 4)).exp() + + # .3 Compute R2 R1 + R2R1 = R2 * R1 + self.assertEquals(R2R1 * (e_1 ^ e_2) * R2R1.rev(), -e_2 ^ e_3) + + # .4 Compute the axis and angle of R2 R1 + a_1 = Symbol('a_1') + a_2 = Symbol('a_2') + a_3 = Symbol('a_3') + a = GA.mv((a_1, a_2, a_3), 'vector') + + theta = Symbol('theta') + A = a.dual() + R = cos(theta / 2) + A * sin(theta / 2) + + system = (R2R1 - R).blade_coefs() + unknowns = [a_1, a_2, a_3, theta] + + results = solve(system, unknowns, dict=True) + self.assertEquals(a.subs(results[0]), (-e_1 - e_2 + e_3) / sqrt(3)) + self.assertEquals(Mod(theta.subs(results[0]), 2 * pi), Mod(Rational(2, 3) * pi, 2 * pi)) + self.assertEquals(a.subs(results[1]), -(-e_1 - e_2 + e_3) / sqrt(3)) + self.assertEquals(Mod(theta.subs(results[1]), 2 * pi), Mod(-Rational(2, 3) * pi, 2 * pi)) + + # Check .4 against .3 + a = a.subs(results[0]) + theta = theta.subs(results[0]) + A = a.dual() + R = cos(theta / 2) + A * sin(theta / 2) + self.assertEquals(R * (e_1 ^ e_2) * R.rev(), -e_2 ^ e_3) + + # .5 Compute the product of rotors + GA, e_1, e_2, e_3, e_4 = Ga.build("e*1|2|3|4", g='1 0 0 0, 0 1 0 0, 0 0 1 0, 0 0 0 1') + + R1 = ((e_1 ^ e_4) * (-pi / 4)).exp() + R2 = ((e_2 ^ e_3) * (-pi / 4)).exp() + R = R2 * R1 + + self.assertEquals(R * (e_1 ^ e_2) * R.rev(), -e_3 ^ e_4) + + # .6 Reflect in a plane + A = (e_1 + e_2) ^ e_3 + B = e_1 ^ e_4 + b = B.dual() + + def hat(k, M): + return ((-1) ** k) * M + + self.assertEquals(b * hat(2, A) * b.inv(), (-e_1 + e_2) ^ e_3) + + # .7 Reflect the dual plane reflector e1 in the plane e1 ^ e3 + GA, e_1, e_2, e_3 = Ga.build("e*1|2|3", g='1 0 0, 0 1 0, 0 0 1') + + a = e_1 + B = e_1 ^ e_3 + b = B.dual() + + self.assertEquals(-b * a * b.inv(), e_1) + + # Reset + Ga.dual_mode() + + +if __name__ == '__main__': + + unittest.main() From 5b124d9a1db63af02873bd8da8dddf4894776af8 Mon Sep 17 00:00:00 2001 From: Sylvain Meunier Date: Mon, 7 Oct 2019 21:56:41 +0200 Subject: [PATCH 47/78] Small fixes, cleanup and use don't use grade_decomposition if we already know the grade --- tests/test_chapter2.py | 6 +-- tests/test_chapter3.py | 98 +++++++++++++++++++++--------------------- tests/test_chapter6.py | 14 ++---- 3 files changed, 55 insertions(+), 63 deletions(-) diff --git a/tests/test_chapter2.py b/tests/test_chapter2.py index 3542521b..fabfff59 100755 --- a/tests/test_chapter2.py +++ b/tests/test_chapter2.py @@ -220,17 +220,17 @@ def test2_12_2_4(self): # Solve x_1 self.assertTrue((x ^ a) == (x_2 * (b ^ a) + x_3 * (c ^ a))) self.assertTrue((x ^ a ^ b) == x_3 * (c ^ a ^ b)) - self.assertTrue((x ^ a ^ b) * (c ^ a ^ b).inv() == R.mv(x_3, 'scalar')) + self.assertTrue((x ^ a ^ b) * (c ^ a ^ b).inv() == GA.mv(x_3, 'scalar')) # Solve x_2 self.assertTrue((x ^ b) == (x_1 * (a ^ b) + x_3 * (c ^ b))) self.assertTrue((x ^ b ^ c) == x_1 * (a ^ b ^ c)) - self.assertTrue((x ^ b ^ c) * (a ^ b ^ c).inv() == R.mv(x_1, 'scalar')) + self.assertTrue((x ^ b ^ c) * (a ^ b ^ c).inv() == GA.mv(x_1, 'scalar')) # Solve x_3 self.assertTrue((x ^ c) == (x_1 * (a ^ c) + x_2 * (b ^ c))) self.assertTrue((x ^ c ^ a) == x_2 * (b ^ c ^ a)) - self.assertTrue((x ^ c ^ a) * (b ^ c ^ a).inv() == R.mv(x_2, 'scalar')) + self.assertTrue((x ^ c ^ a) * (b ^ c ^ a).inv() == GA.mv(x_2, 'scalar')) def test2_12_2_5(self): diff --git a/tests/test_chapter3.py b/tests/test_chapter3.py index a00b9fce..b0e0c568 100644 --- a/tests/test_chapter3.py +++ b/tests/test_chapter3.py @@ -26,17 +26,13 @@ def test_3_1_2(self): """ Definition of the scalar product. """ - GA = Ga('e*1|2|3') - A_blades = [GA.mv('A', 'blade', k) for k in range(GA.n + 1)] - B_blades = [GA.mv('B', 'blade', l) for l in range(GA.n + 1)] + GA = Ga('e*1|2|3|4') + A_blades = [(k, GA.mv('A', 'blade', k)) for k in range(GA.n + 1)] + B_blades = [(l, GA.mv('B', 'blade', l)) for l in range(GA.n + 1)] # GAlgebra doesn't define any scalar product but rely on the geometric product instead - for A, B in product(A_blades, B_blades): - A_grades = GA.grade_decomposition(A).keys() - B_grades = GA.grade_decomposition(B).keys() - self.assertEquals(len(A_grades), 1) - self.assertEquals(len(B_grades), 1) - if A_grades[0] == B_grades[0]: + for (k, A), (l, B) in product(A_blades, B_blades): + if k == l: self.assertTrue((A * B).scalar() != 0) else: self.assertTrue((A * B).scalar() == 0) @@ -47,51 +43,47 @@ def test_3_2_2(self): Computing the contraction explicitly. """ GA = Ga('e*1|2|3') - A_blades = [GA.mv('A', 'blade', k) for k in range(GA.n + 1)] - B_blades = [GA.mv('B', 'blade', l) for l in range(GA.n + 1)] - C_blades = [GA.mv('C', 'blade', m) for m in range(GA.n + 1)] + A_blades = [(k, GA.mv('A', 'blade', k)) for k in range(GA.n + 1)] + B_blades = [(l, GA.mv('B', 'blade', l)) for l in range(GA.n + 1)] + C_blades = [(m, GA.mv('C', 'blade', m)) for m in range(GA.n + 1)] # scalar and blades of various grades - A = A_blades[0] - for B in B_blades: + k, A = A_blades[0] + for l, B in B_blades: self.assertEquals(A < B, A * B) - A = A_blades[0] - for B in B_blades: - B_grades = GA.grade_decomposition(B).keys() - self.assertEquals(len(B_grades), 1) - self.assertEquals(B < A, 0 if B_grades[0] else A * B) + k, A = A_blades[0] + for l, B in B_blades: + self.assertEquals(B < A, 0 if l > 0 else (A * B).scalar()) # vectors - A = A_blades[1] - B = B_blades[1] - self.assertEquals(A < B, A | B) + k, A = A_blades[1] + l, B = B_blades[1] + self.assertEquals(A < B, (A * B).scalar()) # vector and the outer product of 2 blades of various grades (scalars, vectors, 2-vectors...) - A = A_blades[1] - for B, C in product(B_blades, C_blades): - B_grades = GA.grade_decomposition(B).keys() - self.assertEquals(len(B_grades), 1) - self.assertEquals(A < (B ^ C), ((A < B) ^ C) + (-1)**B_grades[0] * (B ^ (A < C))) + k, A = A_blades[1] + for (l, B), (m, C) in product(B_blades, C_blades): + self.assertEquals(A < (B ^ C), ((A < B) ^ C) + (-1)**l * (B ^ (A < C))) # vector and the outer product of 2 blades of various grades (scalars, vectors, 2-vectors...) - for A, B, C in product(A_blades, B_blades, C_blades): + for (k, A), (l, B), (m, C) in product(A_blades, B_blades, C_blades): self.assertEquals((A ^ B) < C, A < (B < C)) # distributive properties - for A, B, C in product(A_blades, B_blades, C_blades): + for (k, A), (l, B), (m, C) in product(A_blades, B_blades, C_blades): self.assertEquals((A + B) < C, (A < C) + (B < C)) - for A, B, C in product(A_blades, B_blades, C_blades): + for (k, A), (l, B), (m, C) in product(A_blades, B_blades, C_blades): self.assertEquals(A < (B + C), (A < B) + (A < C)) alpha = Symbol("alpha") - for A, B in product(A_blades, B_blades): + for (k, A), (l, B) in product(A_blades, B_blades): self.assertEquals((alpha * A) < B, alpha * (A < B)) self.assertEquals((alpha * A) < B, A < (alpha * B)) a = GA.mv('a', 'blade', 1) - for A_minus1, B in product(A_blades[:-1], B_blades): + for (k, A_minus1), (l, B) in product(A_blades[:-1], B_blades): A = A_minus1 ^ a self.assertEquals(A < B, (A_minus1 ^ a) < B) self.assertEquals(A < B, A_minus1 < (a < B)) @@ -102,31 +94,25 @@ def test_3_4(self): The other contraction. """ GA = Ga('e*1|2|3') - A_blades = [GA.mv('A', 'blade', k) for k in range(GA.n + 1)] - B_blades = [GA.mv('B', 'blade', l) for l in range(GA.n + 1)] + A_grade_and_blades = [(k, GA.mv('A', 'blade', k)) for k in range(GA.n + 1)] + B_grade_and_blades = [(l, GA.mv('B', 'blade', l)) for l in range(GA.n + 1)] - for A, B in product(A_blades, B_blades): - A_grades = GA.grade_decomposition(A).keys() - B_grades = GA.grade_decomposition(B).keys() - self.assertEquals(len(A_grades), 1) - self.assertEquals(len(B_grades), 1) - self.assertEquals(B > A, ((-1) ** (A_grades[0] * (B_grades[0] - 1))) * (A < B)) + for (k, A), (l, B) in product(A_grade_and_blades, B_grade_and_blades): + self.assertEquals(B > A, ((-1) ** (k * (l - 1))) * (A < B)) - for A, B in product(A_blades, B_blades): + for (k, A), (l, B) in product(A_grade_and_blades, B_grade_and_blades): C = B > A - A_grades = GA.grade_decomposition(A).keys() - B_grades = GA.grade_decomposition(B).keys() C_grades = GA.grade_decomposition(C).keys() - self.assertEquals(len(A_grades), 1) - self.assertEquals(len(B_grades), 1) self.assertEquals(len(C_grades), 1) - self.assertTrue(C == 0 or C_grades[0] == B_grades[0] - A_grades[0]) + self.assertTrue(C == 0 or C_grades[0] == l - k) def test_3_5_2(self): """ The inverse of a blade. """ + Ga.dual_mode("Iinv+") + for GA in [Ga('e*1|2'), Ga('e*1|2|3'), Ga('e*1|2|3|4')]: # , Ga('e*1|2|3|4|5')]: A_grade_and_blades = [(k, GA.mv('A', 'blade', k)) for k in range(GA.n + 1)] for k, A in A_grade_and_blades: @@ -143,6 +129,8 @@ def test_3_5_2(self): for k, A in A_grade_and_blades: self.assertEquals(A < A.inv(), 1) + Ga.dual_mode() + def test_3_5_3(self): """ @@ -151,10 +139,10 @@ def test_3_5_3(self): Ga.dual_mode("Iinv+") # some blades by grades for each space - spaces = [([GA.mv('A', 'blade', k) for k in range(GA.n + 1)], R) for R in [Ga('e*1|2'), Ga('e*1|2|3'), Ga('e*1|2|3|4')]] + spaces = [([GA.mv('A', 'blade', k) for k in range(GA.n + 1)], GA) for GA in [Ga('e*1|2'), Ga('e*1|2|3'), Ga('e*1|2|3|4')]] # dualization - for blades, R in spaces: + for blades, GA in spaces: for A in blades: self.assertEquals(A.dual(), A < GA.I_inv()) @@ -168,6 +156,8 @@ def test_3_5_3(self): for A in blades: self.assertEquals(A, A.dual() < GA.I()) + Ga.dual_mode() + def test_3_5_4(self): """ @@ -175,7 +165,7 @@ def test_3_5_4(self): """ Ga.dual_mode("Iinv+") - R = Ga('e*1|2|3') + GA = Ga('e*1|2|3') A_blades = [GA.mv('A', 'blade', k) for k in range(GA.n + 1)] B_blades = [GA.mv('B', 'blade', l) for l in range(GA.n + 1)] @@ -185,6 +175,8 @@ def test_3_5_4(self): for A, B in product(A_blades, B_blades): self.assertEquals((A < B).dual(), A ^ B.dual()) + Ga.dual_mode() + def test_3_6(self): """ @@ -237,6 +229,8 @@ def test3_7_2(self): self.assertEquals(cross(a, b), (B < GA.I_inv()) < A) self.assertEquals(cross(a, b), B.dual() < A) + Ga.dual_mode() + def test_3_10_1_1(self): """ @@ -308,6 +302,8 @@ def test_3_10_1_1(self): # h self.assertEquals(a < (b < GA.I_inv()), (a ^ b) < GA.I_inv()) self.assertEquals(a < (b < GA.I_inv()), e_3 - e_2 + e_1) + + Ga.dual_mode() def test_3_10_1_2(self): @@ -382,6 +378,8 @@ def test_3_10_1_2(self): self.assertEquals(num, 0) self.assertEquals(cosine, 0) + Ga.dual_mode() + def test3_10_2_1(self): """ @@ -481,6 +479,8 @@ def test3_10_2_11(self): self.assertTrue((a < b).is_scalar()) self.assertEquals(xx, b * (a < c) - c * (a < b)) + Ga.dual_mode() + if __name__ == '__main__': diff --git a/tests/test_chapter6.py b/tests/test_chapter6.py index 442e7f09..37e9d7c9 100644 --- a/tests/test_chapter6.py +++ b/tests/test_chapter6.py @@ -126,21 +126,13 @@ def test6_3_2(self): """ The subspace products as selected grades. """ - GA = Ga('e*1|2|3') # This test should work for blades and other graded multivectors - A_grades = [GA.mv('A%d' % k, 'grade', k) for k in range(R.n + 1)] - B_grades = [GA.mv('B%d' % l, 'grade', l) for l in range(R.n + 1)] - - def grade(M): - M_grades = GA.grade_decomposition(M).keys() - self.assertEquals(len(M_grades), 1) - return M_grades[0] + A_grade_and_blades = [(k, GA.mv('A%d' % k, 'grade', k)) for k in range(GA.n + 1)] + B_grade_and_blades = [(l, GA.mv('B%d' % l, 'grade', l)) for l in range(GA.n + 1)] - for A, B in product(A_grades, B_grades): - k = grade(A) - l = grade(B) + for (k, A), (l, B) in product(A_grade_and_blades, B_grade_and_blades): self.assertEquals(A ^ B, (A * B).get_grade(k + l)) self.assertEquals(A < B, 0 if k > l else (A * B).get_grade(l - k)) self.assertEquals(A > B, 0 if l > k else (A * B).get_grade(k - l)) From a94289bc7bbbab456d70f7716e5e9f84ada8ab3b Mon Sep 17 00:00:00 2001 From: Sylvain Meunier Date: Wed, 9 Oct 2019 16:16:33 +0200 Subject: [PATCH 48/78] Fix add missing parenthesis --- tests/test_chapter3.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_chapter3.py b/tests/test_chapter3.py index b0e0c568..a358614f 100644 --- a/tests/test_chapter3.py +++ b/tests/test_chapter3.py @@ -473,8 +473,8 @@ def test3_10_2_11(self): self.assertEquals(xx, (a ^ (b ^ c).dual()).dual()) self.assertEquals(xx, a < (b ^ c).dual().dual()) self.assertEquals(xx, -a < (b ^ c)) - self.assertEquals(xx, (-a < b) ^ c - b ^ (-a < c)) - self.assertEquals(xx, b ^ (a < c) - c ^ (a < b)) + self.assertEquals(xx, ((-a < b) ^ c) - (b ^ (-a < c))) + self.assertEquals(xx, (b ^ (a < c)) - (c ^ (a < b))) self.assertTrue((a < c).is_scalar()) self.assertTrue((a < b).is_scalar()) self.assertEquals(xx, b * (a < c) - c * (a < b)) From 7a7617f135677f6b404474b5287acf6a63b10261 Mon Sep 17 00:00:00 2001 From: Sylvain Meunier Date: Fri, 11 Oct 2019 14:15:16 +0200 Subject: [PATCH 49/78] leo dorst book chapter 10 exercises (in progress) --- tests/test_chapter10.py | 85 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 85 insertions(+) create mode 100644 tests/test_chapter10.py diff --git a/tests/test_chapter10.py b/tests/test_chapter10.py new file mode 100644 index 00000000..4fcb137c --- /dev/null +++ b/tests/test_chapter10.py @@ -0,0 +1,85 @@ +import unittest + +from sympy import simplify, sqrt, Rational, Symbol +from ga import Ga +from mv import Mv + + +class TestChapter7(unittest.TestCase): + + def assertEquals(self, first, second, msg=None): + """ + Compare two expressions are equals. + """ + + if isinstance(first, Mv): + first = first.obj + + if isinstance(second, Mv): + second = second.obj + + diff = simplify(first - second) + + self.assertTrue(diff == 0, "\n%s\n==\n%s\n%s" % (first, second, diff)) + + def test_11_12_1(self): + """ + Compute the 2-blades corresponding to the lines gives by the data below. Which of + the lines are the same, considered as weighted oriented elements of geometry, which + are the same as offset subspaces ? + """ + GA, e_0, e_1, e_2, e_3 = Ga.build("e*0|1|2|3", g='-1 0 0 0, 0 1 0 0, 0 0 1 0, 0 0 0 1') + + p = e_0 + e_1 + q = e_0 + e_2 + d = e_2 - e_1 + self.assertEquals(p ^ q, p ^ d) + self.assertEquals(p ^ q, q ^ d) + + r = e_0 + 2 * (e_2 - e_1) + e = 2 * (e_2 - e_1) + s = e_0 + 3 * (e_2 - e_1) + t = 2 * (e_0 + e_2) + self.assertEquals(2 * (p ^ q), p ^ e) + self.assertEquals(2 * (p ^ q), p ^ t) + + def test11_12_2_1(self): + """ + Let an orthonormal coordinate system be given in 3-dimensional Euclidean space. + Compute the support vector of the line with direction u = e1 + 2 e2 - e3, through the point p = e1 - 3 e2. + What is the distance of the line to the origin ? + """ + GA, e_0, e_1, e_2, e_3 = Ga.build("e*0|1|2|3", g='-1 0 0 0, 0 1 0 0, 0 0 1 0, 0 0 0 1') + + L = (e_1 + 2 * e_2 - e_3) ^ (e_0 + e_1 - 3 * e_2) + d = (e_0.inv() < (e_0 ^ L)) / (e_0.inv() < L) + self.assertEquals(d.norm(), sqrt(Rational(35, 6))) + + def test11_12_2_2(self): + """ + Convert the line of the previous exercise into a parametric equation x = p + t * u; express t as a function of x + for a point x on the line. + """ + GA, e_0, e_1, e_2, e_3 = Ga.build("e*0|1|2|3", g='-1 0 0 0, 0 1 0 0, 0 0 1 0, 0 0 0 1') + + u = e_1 + 2 * e_2 - e_3 + p = e_0 + e_1 - 3 * e_2 + L = u ^ p + + t = Symbol('t') + x_1 = Symbol('x_1') + x_2 = Symbol('x_2') + x_3 = Symbol('x_3') + x = e_0 + x_1 * e_1 + x_2 * e_2 + x_3 * e_3 + + # x(t) + x_t = (e_0 + e_1 - 3 * e_2) + t * (e_1 + 2 * e_2 - e_3) + + # t(x) + t_x = (x - (e_0 + e_1 - 3 * e_2)) / (e_1 + 2 * e_2 - e_3) + + for i in range(11): + t_value = Rational(i, 10) + x_value = x_t.subs({t: t_value}) + self.assertEquals(x_value ^ L, 0) + self.assertEquals(t_x.subs(zip([x_1, x_2, x_3], x_value.blade_coefs([e_1, e_2, e_3]))), t_value) From c2634e8ebbd6575185b0eacf128f3ca95c046e3f Mon Sep 17 00:00:00 2001 From: Sylvain Meunier Date: Fri, 11 Oct 2019 19:07:32 +0200 Subject: [PATCH 50/78] Rename a file --- tests/{test_chapter10.py => test_chapter11.py} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename tests/{test_chapter10.py => test_chapter11.py} (100%) diff --git a/tests/test_chapter10.py b/tests/test_chapter11.py similarity index 100% rename from tests/test_chapter10.py rename to tests/test_chapter11.py From 080f6c084cbd2db921529589df41a69e30ca43bb Mon Sep 17 00:00:00 2001 From: Sylvain Meunier Date: Fri, 11 Oct 2019 19:07:56 +0200 Subject: [PATCH 51/78] Rename a file --- tests/test_chapter11.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_chapter11.py b/tests/test_chapter11.py index 4fcb137c..843d0663 100644 --- a/tests/test_chapter11.py +++ b/tests/test_chapter11.py @@ -5,7 +5,7 @@ from mv import Mv -class TestChapter7(unittest.TestCase): +class TestChapter11(unittest.TestCase): def assertEquals(self, first, second, msg=None): """ From ff08cb630950b9af2afeaca0afad928b363322cc Mon Sep 17 00:00:00 2001 From: Sylvain Meunier Date: Sat, 12 Oct 2019 22:05:52 +0200 Subject: [PATCH 52/78] leo dorst book chapter 11 exercises (in progress) --- tests/test_chapter11.py | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/tests/test_chapter11.py b/tests/test_chapter11.py index 843d0663..deaf7fdd 100644 --- a/tests/test_chapter11.py +++ b/tests/test_chapter11.py @@ -22,6 +22,33 @@ def assertEquals(self, first, second, msg=None): self.assertTrue(diff == 0, "\n%s\n==\n%s\n%s" % (first, second, diff)) + def test11_4(self): + """ + All planes are 3-blades. + """ + GA, e_0, e_1, e_2, e_3 = Ga.build("e*0|1|2|3", g='-1 0 0 0, 0 1 0 0, 0 0 1 0, 0 0 0 1') + + p0 = Symbol('p0') + q0 = Symbol('q0') + r0 = Symbol('r0') + + p = GA.mv((p0, Symbol('p1'), Symbol('p2'), Symbol('p3')), 'vector') + q = GA.mv((q0, Symbol('q1'), Symbol('q2'), Symbol('q3')), 'vector') + r = GA.mv((r0, Symbol('r1'), Symbol('r2'), Symbol('r3')), 'vector') + + p_inf = p.subs({p0: 0}) + q_inf = q.subs({q0: 0}) + r_inf = r.subs({r0: 0}) + + p = p.subs({p0: 1}) + q = q.subs({q0: 1}) + r = r.subs({r0: 1}) + + self.assertEquals(p ^ q ^ r, p ^ (q - p) ^ (r - p)) + self.assertEquals(p ^ q ^ r, p ^ (q_inf - p_inf) ^ (r_inf - p_inf)) + self.assertEquals(p ^ q ^ r, ((p + q + r) / 3) ^ ((p ^ q) + (q ^ r) + (r ^ p))) + + def test_11_12_1(self): """ Compute the 2-blades corresponding to the lines gives by the data below. Which of From 2f26ea4c947dd1f56aeb38db9f10c2f60e91ab81 Mon Sep 17 00:00:00 2001 From: Sylvain Meunier Date: Sun, 13 Oct 2019 20:50:26 +0200 Subject: [PATCH 53/78] leo dorst book chapter 11 exercises (in progress) --- tests/test_chapter11.py | 76 +++++++++++++++++++++++++++++++++++++++-- 1 file changed, 74 insertions(+), 2 deletions(-) diff --git a/tests/test_chapter11.py b/tests/test_chapter11.py index deaf7fdd..da945a54 100644 --- a/tests/test_chapter11.py +++ b/tests/test_chapter11.py @@ -22,6 +22,21 @@ def assertEquals(self, first, second, msg=None): self.assertTrue(diff == 0, "\n%s\n==\n%s\n%s" % (first, second, diff)) + def assertNotEquals(self, first, second, msg=None): + """ + Compare two expressions are equals. + """ + + if isinstance(first, Mv): + first = first.obj + + if isinstance(second, Mv): + second = second.obj + + diff = simplify(first - second) + + self.assertTrue(diff != 0, "\n%s\n!=\n%s\n%s" % (first, second, diff)) + def test11_4(self): """ All planes are 3-blades. @@ -48,8 +63,65 @@ def test11_4(self): self.assertEquals(p ^ q ^ r, p ^ (q_inf - p_inf) ^ (r_inf - p_inf)) self.assertEquals(p ^ q ^ r, ((p + q + r) / 3) ^ ((p ^ q) + (q ^ r) + (r ^ p))) - - def test_11_12_1(self): + def test11_6(self): + """ + Dual representation. + """ + Ga.dual_mode('Iinv+') + + GA_list = [ + Ga("e*0|1|2", g='-1 0 0, 0 1 0, 0 0 1'), + Ga("e*0|1|2|3", g='-1 0 0 0, 0 1 0 0, 0 0 1 0, 0 0 0 1'), + Ga("e*0|1|2|3|5", g='-1 0 0 0 0, 0 1 0 0 0, 0 0 1 0 0, 0 0 0 1 0, 0 0 0 0 1'), + ] + + for GA in GA_list: + e_0 = GA.mv_basis[0] + e_0_inv = e_0.inv() + + Ip = GA.I() + Ip_inv = GA.I_inv() + Ir = e_0_inv < Ip + Ir_inv = Ir.inv() + self.assertEquals(Ip, e_0 ^ Ir) + self.assertEquals(Ip, e_0 * Ir) + + p = GA.mv([1] + [Symbol('p%d' % i) for i in range(1, GA.n)], 'vector') + + v = [ + GA.mv([0] + [Symbol('q%d' % i) for i in range(1, GA.n)], 'vector'), + GA.mv([0] + [Symbol('r%d' % i) for i in range(1, GA.n)], 'vector'), + GA.mv([0] + [Symbol('s%d' % i) for i in range(1, GA.n)], 'vector'), + GA.mv([0] + [Symbol('t%d' % i) for i in range(1, GA.n)], 'vector'), + ] + + # We test available finite k-flats + for k in range(1, GA.n): + A = reduce(Mv.__xor__, v[:k]) + X = (p ^ A) + self.assertNotEquals(X, 0) + M = e_0_inv < (e_0 ^ X) + # Very slow + #d = (e_0_inv < (e_0 ^ X)) / (e_0_inv < X) + #d_inv = d.inv() + + def hat(A): + return ((-1) ** A.pure_grade()) * A + + self.assertEquals(hat(A < Ir_inv), ((-1) ** (GA.n - 1)) * (hat(A) < Ir_inv)) + + Xd = (p ^ A).dual() + self.assertEquals(Xd, (p ^ A) < Ip_inv) + self.assertEquals(Xd, p < (A < Ip_inv)) + self.assertEquals(Xd, p < ((A < Ir_inv) * e_0_inv)) + self.assertEquals(Xd, hat(A < Ir_inv) - e_0_inv * (p < hat(A < Ir_inv))) + # Very slow + #self.assertEquals(Xd, hat(A < Ir_inv) + e_0_inv * hat(M < Ir_inv)) + #self.assertEquals(Xd, (e_0_inv - d_inv) * hat(M < Ir_inv)) + + Ga.dual_mode() + + def test11_12_1(self): """ Compute the 2-blades corresponding to the lines gives by the data below. Which of the lines are the same, considered as weighted oriented elements of geometry, which From cc0bb70d332074709bcbd4766dbe56be615a000a Mon Sep 17 00:00:00 2001 From: Sylvain Meunier Date: Tue, 22 Oct 2019 14:57:45 +0200 Subject: [PATCH 54/78] Revert mistake about inv, norm and norm2 --- galgebra/mv.py | 37 +++++++++++++++++++++++-------------- 1 file changed, 23 insertions(+), 14 deletions(-) diff --git a/galgebra/mv.py b/galgebra/mv.py index 54b3bcfc..de01eb0f 100755 --- a/galgebra/mv.py +++ b/galgebra/mv.py @@ -1220,7 +1220,10 @@ def _repr_latex_(self): def norm2(self): reverse = self.rev() product = self * reverse - return product.scalar() + if product.is_scalar(): + return product.scalar() + else: + raise TypeError('"(' + str(product) + ')**2" is not a scalar in norm2.') def norm(self, hint='+'): """ @@ -1244,19 +1247,23 @@ def norm(self, hint='+'): """ reverse = self.rev() product = self * reverse - product = product.scalar() - if product.is_number: - if product >= S(0): - return sqrt(product) + + if product.is_scalar(): + product = product.scalar() + if product.is_number: + if product >= S(0): + return sqrt(product) + else: + return sqrt(-product) else: - return sqrt(-product) + if hint == '+': + return metric.square_root_of_expr(product) + elif hint == '-': + return metric.square_root_of_expr(-product) + else: + return sqrt(Abs(product)) else: - if hint == '+': - return metric.square_root_of_expr(product) - elif hint == '-': - return metric.square_root_of_expr(-product) - else: - return sqrt(Abs(product)) + raise TypeError('"(' + str(product) + ')" is not a scalar in norm.') def inv(self): if self.is_scalar(): # self is a scalar @@ -1265,8 +1272,10 @@ def inv(self): if self_sq.is_scalar(): # self*self is a scalar return (S(1)/self_sq.obj)*self self_rev = self.rev() - self_self_rev = (self * self_rev).scalar() - return (S(1) / self_self_rev) * self_rev + self_self_rev = self * self_rev + if(self_self_rev.is_scalar()): # self*self.rev() is a scalar + return (S(1)/self_self_rev.obj) * self_rev + raise TypeError('In inv() for self =' + str(self) + 'self, or self*self or self*self.rev() is not a scalar') def func(self, fct): # Apply function, fct, to each coefficient of multivector (coefs, bases) = metric.linear_expand(self.obj) From 1b41f89e8deedcd4a24f87cefbc63fa95ac5d68f Mon Sep 17 00:00:00 2001 From: Sylvain Meunier Date: Tue, 22 Oct 2019 16:08:01 +0200 Subject: [PATCH 55/78] A test about associativity of the outer product and calculating determinants. --- tests/test_chapter2.py | 39 ++++++++++++++++++++++++++++++++++++--- 1 file changed, 36 insertions(+), 3 deletions(-) diff --git a/tests/test_chapter2.py b/tests/test_chapter2.py index fabfff59..e083d721 100755 --- a/tests/test_chapter2.py +++ b/tests/test_chapter2.py @@ -24,17 +24,49 @@ def test2_3_2(self): self.assertEquals(a ^ (b + c), (a ^ b) + (a ^ c)) + def test_2_4_2(self): + """ + Associativity of the outer product and calculating determinants. + """ + GA, e_1, e_2, e_3 = Ga.build('e*1|2|3') + + a_1 = Symbol('a_1') + a_2 = Symbol('a_2') + a_3 = Symbol('a_3') + b_1 = Symbol('b_1') + b_2 = Symbol('b_2') + b_3 = Symbol('b_3') + c_1 = Symbol('c_1') + c_2 = Symbol('c_2') + c_3 = Symbol('c_3') + + a = GA.mv((a_1, a_2, a_3), 'vector') + b = GA.mv((b_1, b_2, b_3), 'vector') + c = GA.mv((c_1, c_2, c_3), 'vector') + + self.assertEquals(a ^ b ^ c, a ^ (b ^ c)) + self.assertEquals(a ^ b ^ c, (a ^ b) ^ c) + + m = Matrix([ + [a_1, b_1, c_1], + [a_2, b_2, c_2], + [a_3, b_3, c_3] + ]) + + self.assertEquals(a ^ b ^ c, m.det() * (e_1 ^ e_2 ^ e_3)) + + def test2_9_1(self): """ - Blades and grades. Be careful ga.grade_decomposition can't be used to know if a multivector is a blade, - but if we know a multivector is a blade we can retrieve its grade (not fast). - TODO: add a proper grade method to ga module or fix pure_grade... + Blades and grades. Be careful ga.grade_decomposition and Mv.pure_grade can't tell if a multivector is a blade, + but if we know a multivector is a blade we can retrieve its grade. """ GA = Ga('e*1|2|3|4|5') # Check for k in [0, R.n] for k in range(GA.n + 1): Ak = GA.mv('A', 'blade', k) + self.assertEquals(Ak.pure_grade(), k) grades = GA.grade_decomposition(Ak) self.assertEquals(len(grades), 1) self.assertEquals(grades.keys()[0], k) @@ -44,6 +76,7 @@ def test2_9_1(self): Ak = GA.mv('A', 'blade', k) Bl = GA.mv('B', 'blade', l) C = Ak ^ Bl + self.assertEquals(C.pure_grade(), 0 if C == 0 else k + l) grades = GA.grade_decomposition(C) self.assertEquals(len(grades), 1) self.assertEquals(grades.keys()[0], 0 if C == 0 else k + l) From f25281b274bd8a860285d663b680e468b8d4a66a Mon Sep 17 00:00:00 2001 From: Sylvain Meunier Date: Tue, 22 Oct 2019 22:17:05 +0200 Subject: [PATCH 56/78] Generate some flat geometric algebra (like ganja.js) --- galgebra/generator.py | 106 +++++++++++++++++++++++++++++++++++++++ tests/test_generator.py | 107 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 213 insertions(+) create mode 100644 galgebra/generator.py create mode 100644 tests/test_generator.py diff --git a/galgebra/generator.py b/galgebra/generator.py new file mode 100644 index 00000000..f3206197 --- /dev/null +++ b/galgebra/generator.py @@ -0,0 +1,106 @@ +# -*- coding: utf-8 -*- + +from sympy import Symbol + +from ga import Ga + + +def create_multivector(GA, name): + blades = [1] + GA.blades_lst + mv = GA.mv(0, 'scalar') + for blade_index, blade in enumerate(blades): + mv += Symbol('{name}[{i}]'.format(name=name, i=blade_index)) * blade + return mv + + +_CLASS_TEMPLATE = '''# -*- coding: utf-8 -*- + + +class FlatMv(object): + def __init__(self, coefs): + assert len(coefs) == {class_blade_count} + self.coefs = coefs + + def __getitem__(self, index): + return self.coefs[index] +''' + +_BINARY_OPERATOR_TEMPLATE = ''' + def __{op_name}__(self, other): + x = self.coefs + y = other.coefs + return FlatMv([ + {op_list} + ]) +''' + +_METHOD_TEMPLATE=''' + def {method_name}(self): + x = self.coefs + return FlatMv([ + {method_list} + ]) +''' + + +def format_class(class_blade_count): + return _CLASS_TEMPLATE.format(class_blade_count=class_blade_count) + + +def format_op_name(name): + return name + + +def format_op_list(mv): + return ',\n '.join(str(blade_coef) for blade_coef in mv.blade_coefs()) + + +def format_binary_operator(name, mv): + return _BINARY_OPERATOR_TEMPLATE.format(op_name=format_op_name(name), op_list=format_op_list(mv)) + + +def format_method_name(name): + return name + + +def format_method_list(mv): + return ',\n '.join(str(blade_coef) for blade_coef in mv.blade_coefs()) + + +def format_method(name, mv): + return _METHOD_TEMPLATE.format(method_name=format_method_name(name), method_list=format_method_list(mv)) + + +def format_geometric_algebra(GA): + + X = create_multivector(GA, 'x') + Y = create_multivector(GA, 'y') + + flat_geometric_algebra = format_class(len(GA.blades_lst0)) + flat_geometric_algebra += format_binary_operator('add', X + Y) + flat_geometric_algebra += format_binary_operator('sub', X - Y) + flat_geometric_algebra += format_binary_operator('mul', X * Y) + flat_geometric_algebra += format_binary_operator('xor', X ^ Y) + flat_geometric_algebra += format_binary_operator('lshift', X << Y) + flat_geometric_algebra += format_binary_operator('rshift', X >> Y) + flat_geometric_algebra += format_method('dual', X.dual()) + flat_geometric_algebra += format_method('rev', X.rev()) + + return flat_geometric_algebra + + +def flatten(flat_ga_module, mv): + return flat_ga_module.FlatMv([blade_coef for blade_coef in mv.blade_coefs()]) + + +def expand(GA, flat_mv): + assert len(flat_mv.coefs) == len(GA.blades_lst0) + mv = GA.mv(0, 'scalar') + for blade_coef, blade in zip(flat_mv.coefs, GA.blades_lst0): + mv += blade_coef * blade + return mv + + +if __name__ == "__main__": + GA = Ga('e*1|2|3', g=[1, 1, 1]) + print format_geometric_algebra(GA) diff --git a/tests/test_generator.py b/tests/test_generator.py new file mode 100644 index 00000000..b222e5f8 --- /dev/null +++ b/tests/test_generator.py @@ -0,0 +1,107 @@ +import unittest +import importlib + +from sympy import simplify + +from ga import Ga +from mv import Mv +from generator import format_geometric_algebra, flatten, expand + + +class TestGenerator(unittest.TestCase): + + def assertEquals(self, first, second, msg=None): + """ + Compare two expressions are equals. + """ + + if isinstance(first, Mv): + first = first.obj + + if isinstance(second, Mv): + second = second.obj + + diff = simplify(first - second) + + self.assertTrue(diff == 0, "\n%s\n==\n%s\n%s" % (first, second, diff)) + + def setUp(self): + + Ga.dual_mode("Iinv+") + GA = Ga('e*1|2|3', g=[1, 1, 1]) + with open('flat_ga.py', 'wt') as flat_GA_file: + flat_GA_file.write(format_geometric_algebra(GA)) + self.GA = GA + + flat_GA = importlib.import_module('flat_ga') + self.flat_GA = flat_GA + + def test_add(self): + + GA = self.GA + flat_GA = self.flat_GA + X = GA.mv('x', 'mv') + Y = GA.mv('y', 'mv') + + self.assertEquals(X + Y, expand(GA, flatten(flat_GA, X) + flatten(flat_GA, Y))) + + def test_sub(self): + + GA = self.GA + flat_GA = self.flat_GA + X = GA.mv('x', 'mv') + Y = GA.mv('y', 'mv') + + self.assertEquals(X - Y, expand(GA, flatten(flat_GA, X) - flatten(flat_GA, Y))) + + def test_mul(self): + + GA = self.GA + flat_GA = self.flat_GA + X = GA.mv('x', 'mv') + Y = GA.mv('y', 'mv') + + self.assertEquals(X * Y, expand(GA, flatten(flat_GA, X) * flatten(flat_GA, Y))) + + def test_xor(self): + + GA = self.GA + flat_GA = self.flat_GA + X = GA.mv('x', 'mv') + Y = GA.mv('y', 'mv') + + self.assertEquals(X ^ Y, expand(GA, flatten(flat_GA, X) ^ flatten(flat_GA, Y))) + + def test_lshift(self): + + GA = self.GA + flat_GA = self.flat_GA + X = GA.mv('x', 'mv') + Y = GA.mv('y', 'mv') + + self.assertEquals(X << Y, expand(GA, flatten(flat_GA, X) << flatten(flat_GA, Y))) + + def test_rshift(self): + + GA = self.GA + flat_GA = self.flat_GA + X = GA.mv('x', 'mv') + Y = GA.mv('y', 'mv') + + self.assertEquals(X >> Y, expand(GA, flatten(flat_GA, X) >> flatten(flat_GA, Y))) + + def test_dual(self): + + GA = self.GA + flat_GA = self.flat_GA + X = GA.mv('x', 'mv') + + self.assertEquals(X.dual(), expand(GA, flatten(flat_GA, X).dual())) + + def test_rev(self): + + GA = self.GA + flat_GA = self.flat_GA + X = GA.mv('x', 'mv') + + self.assertEquals(X.rev(), expand(GA, flatten(flat_GA, X).rev())) From 7905e5021505df5074aeabcbf137b61d98582093 Mon Sep 17 00:00:00 2001 From: Sylvain Meunier Date: Wed, 23 Oct 2019 22:14:23 +0200 Subject: [PATCH 57/78] leo dorst book chapter 13 exercises (in progress) --- tests/test_chapter13.py | 111 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 111 insertions(+) create mode 100644 tests/test_chapter13.py diff --git a/tests/test_chapter13.py b/tests/test_chapter13.py new file mode 100644 index 00000000..629a6bf3 --- /dev/null +++ b/tests/test_chapter13.py @@ -0,0 +1,111 @@ +import unittest + +from sympy import simplify, Symbol, S + +from ga import Ga +from mv import Mv + + +class TestChapter13(unittest.TestCase): + + def assertEquals(self, first, second, msg=None): + """ + Compare two expressions are equals. + """ + + if isinstance(first, Mv): + first = first.obj + + if isinstance(second, Mv): + second = second.obj + + diff = simplify(first - second) + + self.assertTrue(diff == 0, "\n%s\n==\n%s\n%s" % (first, second, diff)) + + def test_13_1_2(self): + """ + Points as null vectors. + """ + g = [ + [+0, +0, +0, +0, -1], + [+0, +1, +0, +0, +0], + [+0, +0, +1, +0, +0], + [+0, +0, +0, +1, +0], + [-1, +0, +0, +1, +0], + ] + + GA, o, e_1, e_2, e_3, inf = Ga.build('o e1 e2 e3 inf', g=g) + + def vector(x, y, z): + return x * e_1 + y * e_2 + z * e_3 + + def point(v): + return o + v + S.Half * v * v * inf + + p = point(vector(Symbol('px'), Symbol('py'), Symbol('pz'))) + q = point(vector(Symbol('qx'), Symbol('qy'), Symbol('qz'))) + + self.assertEquals(p | q, -S.Half * q * q + p | q - S.Half * p * p) + self.assertEquals(p | q, -S.Half * (q - p) * (q - p)) + + def test_13_1_3(self): + """ + General vectors represent dual planes and spheres. + """ + g = [ + [+0, +0, +0, +0, -1], + [+0, +1, +0, +0, +0], + [+0, +0, +1, +0, +0], + [+0, +0, +0, +1, +0], + [-1, +0, +0, +1, +0], + ] + + GA, o, e_1, e_2, e_3, inf = Ga.build('o e1 e2 e3 inf', g=g) + + def vector(x, y, z): + return x * e_1 + y * e_2 + z * e_3 + + # Point + def point(alpha, v): + return alpha * (o + v + S.Half * v * v * inf) + + alpha = Symbol('alpha') + p = point(alpha, vector(Symbol('px'), Symbol('py'), Symbol('pz'))) + self.assertEquals(p | p, S.Zero) + self.assertEquals(inf | p, -alpha) + + # Dual plane + def dual_plane(n, delta): + return n + delta * inf + + nx = Symbol('nx') + ny = Symbol('ny') + nz = Symbol('nz') + p = dual_plane(vector(nx, ny, nz), Symbol('delta')) + self.assertEquals(p | p, nx * nx + ny * ny + nz * nz) + self.assertEquals(inf | p, S.Zero) + + # Dual sphere + def dual_sphere(alpha, c, r): + return alpha * (c - S.Half * r * r * inf) + + def dual_im_sphere(alpha, c, r): + return alpha * (c + S.Half * r * r * inf) + + cx = Symbol('cx') + cy = Symbol('cy') + cz = Symbol('cz') + r = Symbol('r') + + c = point(1, vector(cx, cy, cz)) + self.assertEquals(c * c, S.Zero) + self.assertEquals(-inf | c, S.One) + + s = dual_sphere(alpha, c, r) + self.assertEquals(s | s, alpha * alpha * r * r) + self.assertEquals(-inf | s, alpha) + + im_s = dual_im_sphere(alpha, c, r) + self.assertEquals(im_s | im_s, -alpha * alpha * r * r) + self.assertEquals(-inf | im_s, alpha) From fd8828891088ef9e713edde273da5cfbfcc8cb81 Mon Sep 17 00:00:00 2001 From: Sylvain Meunier Date: Fri, 25 Oct 2019 21:25:51 +0200 Subject: [PATCH 58/78] first pga version --- galgebra/ga.py | 41 ++++++++++++ galgebra/mv.py | 30 ++++++++- tests/test_pga.py | 167 ++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 237 insertions(+), 1 deletion(-) create mode 100644 tests/test_pga.py diff --git a/galgebra/ga.py b/galgebra/ga.py index bdb7fd60..95ba1fc0 100755 --- a/galgebra/ga.py +++ b/galgebra/ga.py @@ -7,6 +7,7 @@ symbols, sqrt, Abs, numbers from collections import OrderedDict #from sympy.core.compatibility import combinations +from sympy.combinatorics.permutations import Permutation from itertools import combinations import printer import metric @@ -527,6 +528,46 @@ def parametric(self, coords): def basis_vectors(self): return tuple(self.basis) + def build_cobases(self): + """ + Cobases for building Poincare duality, this is useful for defining wedge and vee without using I nor any metric. + """ + basis_indexes = tuple(self.n_range) + self.coindexes = [] + self.coindexes_lst = [] + for i in reversed(basis_indexes): + base_tuple = tuple(reversed(tuple(combinations(basis_indexes, i + 1)))) + self.coindexes.append(base_tuple) + self.coindexes_lst += list(base_tuple) + self.coindexes.append(()) + self.coindexes = tuple(self.coindexes) + + self.coblades_lst = [] + self.coblades_inv_lst = [] + + def format_symbol_name(symbol_index): + return (''.join([str(self.basis[i]) + '^' for i in symbol_index]))[:-1] + + assert self.indexes[0] == () and len(self.coindexes[0]) == 1 + cobase_index = self.coindexes[0][0] + coblade = Symbol(format_symbol_name(cobase_index), commutative=False) + coblade_inv = S(1) + self.coblades_lst.append(coblade) + self.coblades_inv_lst.append(coblade_inv) + + for grade_index, cograde_index in zip(self.indexes[1:], self.coindexes[1:]): + for base_index, cobase_index in zip(grade_index, cograde_index): + coblade_sign = 1 if Permutation(base_index + cobase_index).is_even else -1 + coblade = coblade_sign * Symbol(format_symbol_name(cobase_index), commutative=False) + coblade_inv = coblade_sign * Symbol(format_symbol_name(base_index), commutative=False) + self.coblades_lst.append(coblade) + self.coblades_inv_lst.append(coblade_inv) + + self.coblades_inv_lst = list(reversed(self.coblades_inv_lst)) + + self.coblades_lst0 = self.coblades_lst + [S(1),] + self.coblades_inv_lst0 = [Symbol(format_symbol_name(self.indexes[-1][0]), commutative=False)] + self.coblades_inv_lst + def build_bases(self): """ The bases for the multivector (geometric) algebra are formed from diff --git a/galgebra/mv.py b/galgebra/mv.py index de01eb0f..a5a46aa9 100755 --- a/galgebra/mv.py +++ b/galgebra/mv.py @@ -991,6 +991,20 @@ def get_coefs(self, grade): (coefs, bases) = zip(*cb) return coefs + def J(self): + # TODO: If we pick our basis smartly, we can probably use J in place of Jinv... + obj = 0 + for coef, coblade in zip(self.blade_coefs(), self.Ga.coblades_lst0): + obj += coef * coblade + return Mv(obj, ga=self.Ga) + + def Jinv(self): + # TODO: If we pick our basis smartly, we can probably use J in place of Jinv... + obj = 0 + for coef, coblade_inv in zip(self.blade_coefs(), self.Ga.coblades_inv_lst0): + obj += coef * coblade_inv + return Mv(obj, ga=self.Ga) + def blade_coefs(self, blade_lst=None): """ For a multivector, A, and a list of basis blades, blade_lst return @@ -1001,7 +1015,7 @@ def blade_coefs(self, blade_lst=None): blade_lst = [self.Ga.mv(ONE)] + self.Ga.mv_blades_lst else: for blade in blade_lst: - if not blade.is_base() or not blade.is_blade(): + if not blade.is_base(): # or not blade.is_blade(): can't be used with degenerate metrics raise ValueError("%s expression isn't a basis blade" % blade) blade_lst = [x.obj for x in blade_lst] (coefs, bases) = metric.linear_expand(self.obj) @@ -2492,6 +2506,20 @@ def correlation(u, v, dec=3): # Compute the correlation coefficient of vectors return ulocal.dot(vlocal) / (ulocal.norm() * vlocal.norm()). evalf(dec) +def J(A): + if isinstance(A, Mv): + return A.J() + else: + raise ValueError('A not a multivector in J(A)') + + +def Jinv(A): + if isinstance(A, Mv): + return A.Jinv() + else: + raise ValueError('A not a multivector in J(A)') + + def cross(v1, v2): if v1.is_vector() and v2.is_vector() and v1.Ga.name == v2.Ga.name and v1.Ga.n == 3: return -v1.Ga.I() * (v1 ^ v2) diff --git a/tests/test_pga.py b/tests/test_pga.py new file mode 100644 index 00000000..e359b4c4 --- /dev/null +++ b/tests/test_pga.py @@ -0,0 +1,167 @@ +import unittest + +from sympy import simplify, Rational, symbols, Symbol, S + +from ga import Ga +from mv import Mv, J, Jinv + + +class TestChapter11(unittest.TestCase): + + def assertEquals(self, first, second, msg=None): + """ + Compare two expressions are equals. + """ + if isinstance(first, Mv): + first = first.obj + + if isinstance(second, Mv): + second = second.obj + + diff = simplify(first - second) + + self.assertTrue(diff == 0, "\n%s\n==\n%s\n%s" % (first, second, diff)) + + def setUp(self): + """ + Setup 3D Projective Geometric Algebra aka PGA. + """ + PGA, e_0, e_1, e_2, e_3 = Ga.build('e*0|1|2|3', g=[0, 1, 1, 1]) + + # TODO: move this somewhere useful... + PGA.build_cobases() + + self.PGA = PGA + self.e_0 = e_0 + self.e_1 = e_1 + self.e_2 = e_2 + self.e_3 = e_3 + self.e_032 = e_0 ^ e_3 ^ e_2 + self.e_013 = e_0 ^ e_1 ^ e_3 + self.e_021 = e_0 ^ e_2 ^ e_1 + self.e_123 = e_1 ^ e_2 ^ e_3 + self.e_0123 = e_0 ^ e_1 ^ e_2 ^ e_3 + + def homogenize(self, P): + """ + For testing equality. + """ + return P / (P.blade_coefs([self.e_123])[0]) + + def point(self, x, y, z): + """ + Make a point. + """ + return x * self.e_032 + y * self.e_013 + z * self.e_021 + self.e_123 + + def direction(self, x, y, z): + """ + Make an ideal point (direction). + """ + return x * self.e_032 + y * self.e_013 + z * self.e_021 + + def plane(self, a, b, c, d): + """ + Make a plane. + """ + return a * self.e_1 + b * self.e_2 + c * self.e_3 + d * self.e_0 + + def test_J(self): + + PGA = self.PGA + + for k in range(PGA.n + 1): + X = PGA.mv('x', k, 'grade') + self.assertEquals(X, Jinv(J(X))) + + X = PGA.mv('x', 'mv') + self.assertEquals(X, Jinv(J(X))) + + def test_geometry_incidence_planes_meet_into_points(self): + """ + Planes meet into points. + """ + x = Symbol('x') + y = Symbol('y') + z = Symbol('z') + p1 = self.plane(1, 0, 0, -x) + p2 = self.plane(0, 1, 0, -y) + p3 = self.plane(0, 0, 1, -z) + + P = self.homogenize(p1 ^ p2 ^ p3) + self.assertEquals(P, self.point(x, y, z)) + self.assertEquals(P, self.homogenize(p1 ^ p3 ^ p2)) + self.assertEquals(P, self.homogenize(p2 ^ p1 ^ p3)) + self.assertEquals(P, self.homogenize(p2 ^ p3 ^ p1)) + self.assertEquals(P, self.homogenize(p3 ^ p1 ^ p2)) + self.assertEquals(P, self.homogenize(p3 ^ p2 ^ p1)) + + def test_geometry_incidence_planes_meet_into_points_2(self): + """ + Planes meet into points. + """ + P1 = self.point(*symbols('x1 y1 z1')) + P2 = self.point(*symbols('x2 y2 z2')) + P3 = self.point(*symbols('x3 y3 z3')) + P4 = self.point(*symbols('x4 y4 z4')) + p123 = Jinv(J(P1) ^ J(P2) ^ J(P3)) + p124 = Jinv(J(P1) ^ J(P2) ^ J(P4)) + p234 = Jinv(J(P2) ^ J(P3) ^ J(P4)) + p314 = Jinv(J(P3) ^ J(P1) ^ J(P4)) + + # TODO: find a way to meet planes faster... + self.assertEquals(self.homogenize(p123 ^ p124 ^ p314), P1) + self.assertEquals(self.homogenize(p123 ^ p124 ^ p234), P2) + self.assertEquals(self.homogenize(p123 ^ p234 ^ p314), P3) + self.assertEquals(self.homogenize(p124 ^ p234 ^ p314), P4) + + def test_geometry_incidence_points_join_into_planes(self): + """ + Points join into planes. + """ + x1, y1, z1 = symbols('x1 y1 z1') + P1 = self.point(x1, y1, z1) + + x2, y2, z2 = symbols('x2 y2 z2') + P2 = self.point(x2, y2, z2) + + x3, y3, z3 = symbols('x3 y3 z3') + P3 = self.point(x3, y3, z3) + + pp = Jinv(J(P1) ^ J(P2) ^ J(P3)) + pm = Jinv(J(P1) ^ J(P3) ^ J(P2)) + self.assertEquals(pp, -pm) + + px = Symbol('px') + py = Symbol('py') + pz = Symbol('pz') + + coefs = pp.blade_coefs([self.e_0, self.e_1, self.e_2, self.e_3]) + p = coefs[0] + px * coefs[1] + py * coefs[2] + pz * coefs[3] + self.assertEquals(p.subs({px: x1, py: y1, pz: z1}), S.Zero) + self.assertEquals(p.subs({px: x2, py: y2, pz: z2}), S.Zero) + self.assertEquals(p.subs({px: x3, py: y3, pz: z3}), S.Zero) + + coefs = pm.blade_coefs([self.e_0, self.e_1, self.e_2, self.e_3]) + p = coefs[0] + px * coefs[1] + py * coefs[2] + pz * coefs[3] + self.assertEquals(p.subs({px: x1, py: y1, pz: z1}), S.Zero) + self.assertEquals(p.subs({px: x2, py: y2, pz: z2}), S.Zero) + self.assertEquals(p.subs({px: x3, py: y3, pz: z3}), S.Zero) + + def test_geometry_incidence_join_and_plane_side(self): + """ + Join and plane side. + """ + P1 = self.point(1, 0, 0) + P2 = self.point(0, 1, 0) + P3 = self.point(0, 0, 1) + pp = Jinv(J(P1) ^ J(P2) ^ J(P3)) + + px = Symbol('px') + py = Symbol('py') + pz = Symbol('pz') + coefs = pp.blade_coefs([self.e_0, self.e_1, self.e_2, self.e_3]) + p = coefs[0] + px * coefs[1] + py * coefs[2] + pz * coefs[3] + + self.assertTrue(p.subs({px: Rational(1, 3) - 0.01, py: Rational(1, 3) - 0.01, pz: Rational(1, 3) - 0.01}) < 0.0) + self.assertTrue(p.subs({px: Rational(1, 3) + 0.01, py: Rational(1, 3) + 0.01, pz: Rational(1, 3) + 0.01}) > 0.0) From e9a8d806d3eab6232c61a384fc1d4b654692d856 Mon Sep 17 00:00:00 2001 From: Sylvain Meunier Date: Sun, 27 Oct 2019 16:50:29 +0100 Subject: [PATCH 59/78] More pga tests --- tests/test_pga.py | 132 +++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 119 insertions(+), 13 deletions(-) diff --git a/tests/test_pga.py b/tests/test_pga.py index e359b4c4..65435c6c 100644 --- a/tests/test_pga.py +++ b/tests/test_pga.py @@ -1,12 +1,12 @@ import unittest -from sympy import simplify, Rational, symbols, Symbol, S +from sympy import Abs, asin, pi, Rational, S, simplify, sqrt, Symbol, symbols from ga import Ga from mv import Mv, J, Jinv -class TestChapter11(unittest.TestCase): +class TestPGA(unittest.TestCase): def assertEquals(self, first, second, msg=None): """ @@ -36,12 +36,20 @@ def setUp(self): self.e_1 = e_1 self.e_2 = e_2 self.e_3 = e_3 + self.e_01 = e_0 ^ e_1 + self.e_02 = e_0 ^ e_2 + self.e_03 = e_0 ^ e_3 + self.e_12 = e_1 ^ e_2 + self.e_31 = e_3 ^ e_1 + self.e_23 = e_2 ^ e_3 self.e_032 = e_0 ^ e_3 ^ e_2 self.e_013 = e_0 ^ e_1 ^ e_3 self.e_021 = e_0 ^ e_2 ^ e_1 self.e_123 = e_1 ^ e_2 ^ e_3 self.e_0123 = e_0 ^ e_1 ^ e_2 ^ e_3 + self.e_13 = e_1 ^ e_3 + def homogenize(self, P): """ For testing equality. @@ -66,6 +74,15 @@ def plane(self, a, b, c, d): """ return a * self.e_1 + b * self.e_2 + c * self.e_3 + d * self.e_0 + def line_norm(self, l, hint=None): + assert hint == 'euclidean' or hint == 'ideal' + if hint == 'euclidean': + d, e, f = l.blade_coefs([self.e_12, self.e_13, self.e_23]) + return sqrt(d * d + e * e + f * f) + elif hint == 'ideal': + a, b, c = l.blade_coefs([self.e_01, self.e_02, self.e_03]) + return sqrt(a * a + b * b + c * c) + def test_J(self): PGA = self.PGA @@ -132,21 +149,24 @@ def test_geometry_incidence_points_join_into_planes(self): pm = Jinv(J(P1) ^ J(P3) ^ J(P2)) self.assertEquals(pp, -pm) - px = Symbol('px') - py = Symbol('py') - pz = Symbol('pz') + print pp + print pm + + a = Symbol('a') + b = Symbol('b') + c = Symbol('c') coefs = pp.blade_coefs([self.e_0, self.e_1, self.e_2, self.e_3]) - p = coefs[0] + px * coefs[1] + py * coefs[2] + pz * coefs[3] - self.assertEquals(p.subs({px: x1, py: y1, pz: z1}), S.Zero) - self.assertEquals(p.subs({px: x2, py: y2, pz: z2}), S.Zero) - self.assertEquals(p.subs({px: x3, py: y3, pz: z3}), S.Zero) + p = coefs[0] + a * coefs[1] + b * coefs[2] + c * coefs[3] + self.assertEquals(p.subs({a: x1, b: y1, c: z1}), S.Zero) + self.assertEquals(p.subs({a: x2, b: y2, c: z2}), S.Zero) + self.assertEquals(p.subs({a: x3, b: y3, c: z3}), S.Zero) coefs = pm.blade_coefs([self.e_0, self.e_1, self.e_2, self.e_3]) - p = coefs[0] + px * coefs[1] + py * coefs[2] + pz * coefs[3] - self.assertEquals(p.subs({px: x1, py: y1, pz: z1}), S.Zero) - self.assertEquals(p.subs({px: x2, py: y2, pz: z2}), S.Zero) - self.assertEquals(p.subs({px: x3, py: y3, pz: z3}), S.Zero) + p = coefs[0] + a * coefs[1] + b * coefs[2] + c * coefs[3] + self.assertEquals(p.subs({a: x1, b: y1, c: z1}), S.Zero) + self.assertEquals(p.subs({a: x2, b: y2, c: z2}), S.Zero) + self.assertEquals(p.subs({a: x3, b: y3, c: z3}), S.Zero) def test_geometry_incidence_join_and_plane_side(self): """ @@ -165,3 +185,89 @@ def test_geometry_incidence_join_and_plane_side(self): self.assertTrue(p.subs({px: Rational(1, 3) - 0.01, py: Rational(1, 3) - 0.01, pz: Rational(1, 3) - 0.01}) < 0.0) self.assertTrue(p.subs({px: Rational(1, 3) + 0.01, py: Rational(1, 3) + 0.01, pz: Rational(1, 3) + 0.01}) > 0.0) + + def test_geometry_incidence_points_join_into_lines(self): + """ + Points join into lines, planes meet into lines. + """ + PGA = self.PGA + + x0, y0, z0 = symbols('x0 y0 z0') + P0 = self.point(x0, y0, z0) + + x1, y1, z1 = symbols('x1 y1 z1') + P1 = self.point(x1, y1, z1) + + x2, y2, z2 = symbols('x2 y2 z2') + P2 = self.point(x2, y2, z2) + + x3, y3, z3 = symbols('x3 y3 z3') + P3 = self.point(x3, y3, z3) + + lp = Jinv(J(P1) ^ J(P2)) + lm = Jinv(J(P2) ^ J(P1)) + self.assertEquals(lp, -lm) + + p012 = Jinv(J(P0) ^ J(P1) ^ J(P2)) + p123 = Jinv(J(P1) ^ J(P2) ^ J(P3)) + l = p012 ^ p123 + + print l / self.line_norm(l) + + def test_metric_distance_of_points(self): + """ + We can measure distance between normalized points using the joining line norm. + """ + x0, y0, z0 = symbols('x0 y0 z0') + P0 = self.point(x0, y0, z0) + + x1, y1, z1 = symbols('x1 y1 z1') + P1 = self.point(x1, y1, z1) + + d = sqrt((x1 - x0) ** 2 + (y1 - y0) ** 2 + (z1 - z0) ** 2) + + lp = Jinv(J(P0) ^ J(P1)) + self.assertEquals(self.line_norm(lp, 'euclidean'), d) + + lm = Jinv(J(P1) ^ J(P0)) + self.assertEquals(self.line_norm(lm, 'euclidean'), d) + + def test_metric_angle_of_intersecting_planes(self): + """ + We can measure the angle between normalized planes using the meeting line norm. + """ + x = Symbol('x') + y = Symbol('y') + p1 = self.plane(1, 0, 0, -x) + p2 = self.plane(0, 1, 0, -y) + + self.assertEquals(asin(self.line_norm(p1 ^ p2, 'euclidean')), pi / 2) + + def test_metric_distance_between_parallel_planes(self): + """ + We can measure the distance between two parallel and normalized planes using the meeting line norm. + """ + nx, ny, nz, x, y = symbols('nx ny nz x y') + + n_norm = sqrt(nx * nx + ny * ny + nz * nz) + p1 = self.plane(nx / n_norm, ny / n_norm, nz / n_norm, -x) + p2 = self.plane(nx / n_norm, ny / n_norm, nz / n_norm, -y) + + self.assertEquals(self.line_norm(p1 ^ p2, 'ideal'), sqrt((x - y)**2)) + + def test_metric_oriented_distance_between_point_and_plane(self): + """ + We can measure the distance between a normalized point and a normalized plane. + """ + + x0, y0, z0 = symbols('x0 y0 z0') + P0 = self.point(x0, y0, z0) + + d0 = Symbol('d0') + p0 = self.plane(1, 0, 0, -d0) + + self.assertEquals(Jinv(J(P0) ^ J(p0)), d0 - x0) + self.assertEquals(Jinv(J(p0) ^ J(P0)), x0 - d0) + + self.assertEquals(P0 ^ p0, (d0 - x0) * self.e_0123) # TODO: how can we use inf norm here ? + self.assertEquals(p0 ^ P0, (x0 - d0) * self.e_0123) # From 188aeb1f469c1a5d005fb1b4f6618c52910621ad Mon Sep 17 00:00:00 2001 From: Sylvain Meunier Date: Sun, 27 Oct 2019 21:25:20 +0100 Subject: [PATCH 60/78] Generate flat pga --- galgebra/generator.py | 26 +++++++++++++++++++++----- tests/test_generator.py | 20 ++++++++++++++++---- 2 files changed, 37 insertions(+), 9 deletions(-) diff --git a/galgebra/generator.py b/galgebra/generator.py index f3206197..6e7afafd 100644 --- a/galgebra/generator.py +++ b/galgebra/generator.py @@ -3,6 +3,7 @@ from sympy import Symbol from ga import Ga +from mv import J, Jinv def create_multivector(GA, name): @@ -34,7 +35,7 @@ def __{op_name}__(self, other): ]) ''' -_METHOD_TEMPLATE=''' +_UNARY_METHOD_TEMPLATE = ''' def {method_name}(self): x = self.coefs return FlatMv([ @@ -42,6 +43,15 @@ def {method_name}(self): ]) ''' +_BINARY_METHOD_TEMPLATE = ''' + def {method_name}(self, other): + x = self.coefs + y = other.coefs + return FlatMv([ + {method_list} + ]) +''' + def format_class(class_blade_count): return _CLASS_TEMPLATE.format(class_blade_count=class_blade_count) @@ -67,8 +77,12 @@ def format_method_list(mv): return ',\n '.join(str(blade_coef) for blade_coef in mv.blade_coefs()) -def format_method(name, mv): - return _METHOD_TEMPLATE.format(method_name=format_method_name(name), method_list=format_method_list(mv)) +def format_unary_method(name, mv): + return _UNARY_METHOD_TEMPLATE.format(method_name=format_method_name(name), method_list=format_method_list(mv)) + + +def format_binary_method(name, mv): + return _BINARY_METHOD_TEMPLATE.format(method_name=format_method_name(name), method_list=format_method_list(mv)) def format_geometric_algebra(GA): @@ -80,11 +94,13 @@ def format_geometric_algebra(GA): flat_geometric_algebra += format_binary_operator('add', X + Y) flat_geometric_algebra += format_binary_operator('sub', X - Y) flat_geometric_algebra += format_binary_operator('mul', X * Y) + flat_geometric_algebra += format_binary_operator('and', Jinv(J(X) ^ J(Y))) flat_geometric_algebra += format_binary_operator('xor', X ^ Y) flat_geometric_algebra += format_binary_operator('lshift', X << Y) flat_geometric_algebra += format_binary_operator('rshift', X >> Y) - flat_geometric_algebra += format_method('dual', X.dual()) - flat_geometric_algebra += format_method('rev', X.rev()) + flat_geometric_algebra += format_binary_method('meet', Jinv(J(X) ^ J(Y))) + flat_geometric_algebra += format_binary_method('join', X ^ Y) + flat_geometric_algebra += format_unary_method('rev', X.rev()) return flat_geometric_algebra diff --git a/tests/test_generator.py b/tests/test_generator.py index b222e5f8..8b056e05 100644 --- a/tests/test_generator.py +++ b/tests/test_generator.py @@ -4,7 +4,7 @@ from sympy import simplify from ga import Ga -from mv import Mv +from mv import Mv, J, Jinv from generator import format_geometric_algebra, flatten, expand @@ -28,7 +28,8 @@ def assertEquals(self, first, second, msg=None): def setUp(self): Ga.dual_mode("Iinv+") - GA = Ga('e*1|2|3', g=[1, 1, 1]) + GA = Ga('e*1|2|3|4', g=[0, 1, 1, 1]) + GA.build_cobases() with open('flat_ga.py', 'wt') as flat_GA_file: flat_GA_file.write(format_geometric_algebra(GA)) self.GA = GA @@ -63,6 +64,15 @@ def test_mul(self): self.assertEquals(X * Y, expand(GA, flatten(flat_GA, X) * flatten(flat_GA, Y))) + def test_and(self): + + GA = self.GA + flat_GA = self.flat_GA + X = GA.mv('x', 'mv') + Y = GA.mv('y', 'mv') + + self.assertEquals(Jinv(J(X) ^ J(Y)), expand(GA, flatten(flat_GA, X) & flatten(flat_GA, Y))) + def test_xor(self): GA = self.GA @@ -90,13 +100,15 @@ def test_rshift(self): self.assertEquals(X >> Y, expand(GA, flatten(flat_GA, X) >> flatten(flat_GA, Y))) - def test_dual(self): + def test_meet_and_join(self): GA = self.GA flat_GA = self.flat_GA X = GA.mv('x', 'mv') + Y = GA.mv('y', 'mv') - self.assertEquals(X.dual(), expand(GA, flatten(flat_GA, X).dual())) + self.assertEquals(Jinv(J(X) ^ J(Y)), expand(GA, flatten(flat_GA, X).meet(flatten(flat_GA, Y)))) + self.assertEquals(X ^ Y, expand(GA, flatten(flat_GA, X).join(flatten(flat_GA, Y)))) def test_rev(self): From 9697689dd6f70cf036cd4624c9e644623866994b Mon Sep 17 00:00:00 2001 From: Sylvain Meunier Date: Mon, 28 Oct 2019 15:24:53 +0100 Subject: [PATCH 61/78] Change norm computation for pga tests --- tests/test_pga.py | 127 +++++++++++++++++++++++++++++++--------------- 1 file changed, 85 insertions(+), 42 deletions(-) diff --git a/tests/test_pga.py b/tests/test_pga.py index 65435c6c..98fb11ac 100644 --- a/tests/test_pga.py +++ b/tests/test_pga.py @@ -1,9 +1,9 @@ import unittest -from sympy import Abs, asin, pi, Rational, S, simplify, sqrt, Symbol, symbols +from sympy import acos, asin, pi, Rational, S, simplify, sqrt, Symbol, symbols from ga import Ga -from mv import Mv, J, Jinv +from mv import Mv, J, Jinv, com class TestPGA(unittest.TestCase): @@ -74,23 +74,33 @@ def plane(self, a, b, c, d): """ return a * self.e_1 + b * self.e_2 + c * self.e_3 + d * self.e_0 - def line_norm(self, l, hint=None): - assert hint == 'euclidean' or hint == 'ideal' - if hint == 'euclidean': - d, e, f = l.blade_coefs([self.e_12, self.e_13, self.e_23]) - return sqrt(d * d + e * e + f * f) - elif hint == 'ideal': - a, b, c = l.blade_coefs([self.e_01, self.e_02, self.e_03]) - return sqrt(a * a + b * b + c * c) + def norm(self, X): + assert len(self.PGA.grade_decomposition(X)) == 1 + squared_norm = X | X + if not squared_norm.is_scalar(): + raise ValueError("X | X isn't a scalar") + squared_norm = squared_norm.scalar() + if squared_norm == S.Zero: + raise ValueError("X k-vector is null") + return sqrt(abs(squared_norm)) + + def ideal_norm(self, X): + assert len(self.PGA.grade_decomposition(X)) == 1 + squared_norm = 0 + for c in X.blade_coefs(): + squared_norm += c * c + if squared_norm == S.Zero: + raise ValueError("X k-vector is null") + return sqrt(squared_norm) def test_J(self): - + """ + Check we can join and meet using J and Jinv. + """ PGA = self.PGA - for k in range(PGA.n + 1): X = PGA.mv('x', k, 'grade') self.assertEquals(X, Jinv(J(X))) - X = PGA.mv('x', 'mv') self.assertEquals(X, Jinv(J(X))) @@ -127,10 +137,10 @@ def test_geometry_incidence_planes_meet_into_points_2(self): p314 = Jinv(J(P3) ^ J(P1) ^ J(P4)) # TODO: find a way to meet planes faster... - self.assertEquals(self.homogenize(p123 ^ p124 ^ p314), P1) - self.assertEquals(self.homogenize(p123 ^ p124 ^ p234), P2) - self.assertEquals(self.homogenize(p123 ^ p234 ^ p314), P3) - self.assertEquals(self.homogenize(p124 ^ p234 ^ p314), P4) + #self.assertEquals(self.homogenize(p123 ^ p124 ^ p314), P1) + #self.assertEquals(self.homogenize(p123 ^ p124 ^ p234), P2) + #self.assertEquals(self.homogenize(p123 ^ p234 ^ p314), P3) + #self.assertEquals(self.homogenize(p124 ^ p234 ^ p314), P4) def test_geometry_incidence_points_join_into_planes(self): """ @@ -149,9 +159,6 @@ def test_geometry_incidence_points_join_into_planes(self): pm = Jinv(J(P1) ^ J(P3) ^ J(P2)) self.assertEquals(pp, -pm) - print pp - print pm - a = Symbol('a') b = Symbol('b') c = Symbol('c') @@ -188,31 +195,19 @@ def test_geometry_incidence_join_and_plane_side(self): def test_geometry_incidence_points_join_into_lines(self): """ - Points join into lines, planes meet into lines. + Points join into lines. """ - PGA = self.PGA - - x0, y0, z0 = symbols('x0 y0 z0') - P0 = self.point(x0, y0, z0) - x1, y1, z1 = symbols('x1 y1 z1') P1 = self.point(x1, y1, z1) x2, y2, z2 = symbols('x2 y2 z2') P2 = self.point(x2, y2, z2) - x3, y3, z3 = symbols('x3 y3 z3') - P3 = self.point(x3, y3, z3) - lp = Jinv(J(P1) ^ J(P2)) lm = Jinv(J(P2) ^ J(P1)) self.assertEquals(lp, -lm) - p012 = Jinv(J(P0) ^ J(P1) ^ J(P2)) - p123 = Jinv(J(P1) ^ J(P2) ^ J(P3)) - l = p012 ^ p123 - - print l / self.line_norm(l) + # TODO: add more tests... def test_metric_distance_of_points(self): """ @@ -224,13 +219,13 @@ def test_metric_distance_of_points(self): x1, y1, z1 = symbols('x1 y1 z1') P1 = self.point(x1, y1, z1) - d = sqrt((x1 - x0) ** 2 + (y1 - y0) ** 2 + (z1 - z0) ** 2) + d = sqrt(abs((x1 - x0) ** 2 + (y1 - y0) ** 2 + (z1 - z0) ** 2)) lp = Jinv(J(P0) ^ J(P1)) - self.assertEquals(self.line_norm(lp, 'euclidean'), d) + self.assertEquals(self.norm(lp), d) lm = Jinv(J(P1) ^ J(P0)) - self.assertEquals(self.line_norm(lm, 'euclidean'), d) + self.assertEquals(self.norm(lm), d) def test_metric_angle_of_intersecting_planes(self): """ @@ -241,7 +236,7 @@ def test_metric_angle_of_intersecting_planes(self): p1 = self.plane(1, 0, 0, -x) p2 = self.plane(0, 1, 0, -y) - self.assertEquals(asin(self.line_norm(p1 ^ p2, 'euclidean')), pi / 2) + self.assertEquals(asin(self.norm(p1 ^ p2)), pi / 2) def test_metric_distance_between_parallel_planes(self): """ @@ -253,13 +248,12 @@ def test_metric_distance_between_parallel_planes(self): p1 = self.plane(nx / n_norm, ny / n_norm, nz / n_norm, -x) p2 = self.plane(nx / n_norm, ny / n_norm, nz / n_norm, -y) - self.assertEquals(self.line_norm(p1 ^ p2, 'ideal'), sqrt((x - y)**2)) + self.assertEquals(self.ideal_norm(p1 ^ p2), sqrt((x - y)**2)) def test_metric_oriented_distance_between_point_and_plane(self): """ We can measure the distance between a normalized point and a normalized plane. """ - x0, y0, z0 = symbols('x0 y0 z0') P0 = self.point(x0, y0, z0) @@ -269,5 +263,54 @@ def test_metric_oriented_distance_between_point_and_plane(self): self.assertEquals(Jinv(J(P0) ^ J(p0)), d0 - x0) self.assertEquals(Jinv(J(p0) ^ J(P0)), x0 - d0) - self.assertEquals(P0 ^ p0, (d0 - x0) * self.e_0123) # TODO: how can we use inf norm here ? - self.assertEquals(p0 ^ P0, (x0 - d0) * self.e_0123) # + # TODO: We could assume d0 - x0 is not null for simplifying further + self.assertEquals(self.ideal_norm(P0 ^ p0), sqrt((d0 - x0)**2)) + + def test_metric_oriented_distance_between_point_and_line(self): + """ + We can measure the distance between a normalized point and a normalized line. + """ + x0, y0, z0 = symbols('x0 y0 z0') + P0 = self.point(x0, y0, z0) + + x1, y1, z1 = symbols('x1 y1 z1') + P1 = self.point(x1, y1, z1) + + x2, y2, z2 = symbols('x2 y2 z2') + P2 = self.point(x2, y2, z2) + + l0 = Jinv(J(P1) ^ J(P2)) + + print Jinv(J(P0) ^ J(l0)) + + print self.norm(Jinv(J(P0) ^ J(l0))) + + #self.assertEquals(self.line_norm(Jinv(J(P0) ^ J(l0)), 'euclidean'), d0 - x0) + #self.assertEquals(self.line_norm(Jinv(J(P0) ^ J(l0)), 'euclidean'), x0 - d0) + + def test_metric_common_normal_line(self): + """ + We can find the common normal line of two normalized lines. + """ + x0, y0, z0 = symbols('x0 y0 z0') + P0 = self.point(x0, y0, z0) + + x1, y1, z1 = symbols('x1 y1 z1') + P1 = self.point(x1, y1, z1) + + x2, y2, z2 = symbols('x2 y2 z2') + P2 = self.point(x2, y2, z2) + + x3, y3, z3 = symbols('x3 y3 z3') + P3 = self.point(x3, y3, z3) + + l0 = Jinv(J(P0) ^ J(P1)) + l1 = Jinv(J(P2) ^ J(P3)) + ln = com(l0, l1) + + ln /= self.norm(ln) + l0 /= self.norm(l0) + l1 /= self.norm(l1) + + self.assertEquals(acos(l0 | ln), S.Half * pi) + self.assertEquals(acos(l1 | ln), S.Half * pi) From f5d4d8793ed712917fca84266bc1c93f9ae3e034 Mon Sep 17 00:00:00 2001 From: Sylvain Meunier Date: Tue, 29 Oct 2019 21:48:32 +0100 Subject: [PATCH 62/78] More pga tests and squared norm --- tests/test_pga.py | 80 ++++++++++++++++++++++++++++++++++------------- 1 file changed, 58 insertions(+), 22 deletions(-) diff --git a/tests/test_pga.py b/tests/test_pga.py index 98fb11ac..f0898389 100644 --- a/tests/test_pga.py +++ b/tests/test_pga.py @@ -22,6 +22,23 @@ def assertEquals(self, first, second, msg=None): self.assertTrue(diff == 0, "\n%s\n==\n%s\n%s" % (first, second, diff)) + def assertProjEquals(self, X, Y): + """ + Compare two points, two planes or two lines up to a scalar. + """ + assert isinstance(X, Mv) + assert isinstance(Y, Mv) + + X /= self.norm(X) + Y /= self.norm(Y) + + # We can't easily retrieve the sign, so we test both + diff = simplify(X.obj - Y.obj) + if diff != S.Zero: + diff = simplify(X.obj + Y.obj) + + self.assertTrue(diff == S.Zero, "\n%s\n==\n%s" % (X, Y)) + def setUp(self): """ Setup 3D Projective Geometric Algebra aka PGA. @@ -50,12 +67,6 @@ def setUp(self): self.e_13 = e_1 ^ e_3 - def homogenize(self, P): - """ - For testing equality. - """ - return P / (P.blade_coefs([self.e_123])[0]) - def point(self, x, y, z): """ Make a point. @@ -84,6 +95,14 @@ def norm(self, X): raise ValueError("X k-vector is null") return sqrt(abs(squared_norm)) + def norm2(self, X): + assert len(self.PGA.grade_decomposition(X)) == 1 + squared_norm = X | X + if not squared_norm.is_scalar(): + raise ValueError("X | X isn't a scalar") + squared_norm = squared_norm.scalar() + return squared_norm + def ideal_norm(self, X): assert len(self.PGA.grade_decomposition(X)) == 1 squared_norm = 0 @@ -115,13 +134,13 @@ def test_geometry_incidence_planes_meet_into_points(self): p2 = self.plane(0, 1, 0, -y) p3 = self.plane(0, 0, 1, -z) - P = self.homogenize(p1 ^ p2 ^ p3) - self.assertEquals(P, self.point(x, y, z)) - self.assertEquals(P, self.homogenize(p1 ^ p3 ^ p2)) - self.assertEquals(P, self.homogenize(p2 ^ p1 ^ p3)) - self.assertEquals(P, self.homogenize(p2 ^ p3 ^ p1)) - self.assertEquals(P, self.homogenize(p3 ^ p1 ^ p2)) - self.assertEquals(P, self.homogenize(p3 ^ p2 ^ p1)) + P = p1 ^ p2 ^ p3 + self.assertProjEquals(P, self.point(x, y, z)) + self.assertProjEquals(P, p1 ^ p3 ^ p2) + self.assertProjEquals(P, p2 ^ p1 ^ p3) + self.assertProjEquals(P, p2 ^ p3 ^ p1) + self.assertProjEquals(P, p3 ^ p1 ^ p2) + self.assertProjEquals(P, p3 ^ p2 ^ p1) def test_geometry_incidence_planes_meet_into_points_2(self): """ @@ -137,10 +156,10 @@ def test_geometry_incidence_planes_meet_into_points_2(self): p314 = Jinv(J(P3) ^ J(P1) ^ J(P4)) # TODO: find a way to meet planes faster... - #self.assertEquals(self.homogenize(p123 ^ p124 ^ p314), P1) - #self.assertEquals(self.homogenize(p123 ^ p124 ^ p234), P2) - #self.assertEquals(self.homogenize(p123 ^ p234 ^ p314), P3) - #self.assertEquals(self.homogenize(p124 ^ p234 ^ p314), P4) + #self.assertProjEquals(p123 ^ p124 ^ p314, P1) + #self.assertProjEquals(p123 ^ p124 ^ p234, P2) + #self.assertProjEquals(p123 ^ p234 ^ p314, P3) + #self.assertProjEquals(p124 ^ p234 ^ p314, P4) def test_geometry_incidence_points_join_into_planes(self): """ @@ -281,12 +300,12 @@ def test_metric_oriented_distance_between_point_and_line(self): l0 = Jinv(J(P1) ^ J(P2)) - print Jinv(J(P0) ^ J(l0)) + d = self.norm(Jinv(J(P0) ^ J(l0))) - print self.norm(Jinv(J(P0) ^ J(l0))) - - #self.assertEquals(self.line_norm(Jinv(J(P0) ^ J(l0)), 'euclidean'), d0 - x0) - #self.assertEquals(self.line_norm(Jinv(J(P0) ^ J(l0)), 'euclidean'), x0 - d0) + self.assertEquals(d.subs({x0: 2, y0: 0, z0: 0, x1: 0, y1: 2, z1: 0, x2: 1, y2: 2, z2: 0}), 2) + self.assertEquals(d.subs({x0: 3, y0: 0, z0: 0, x1: 0, y1: 2, z1: 0, x2: 1, y2: 2, z2: 0}), 2) + self.assertEquals(d.subs({x0: 2, y0: 0, z0: 0, x1: 0, y1: 1, z1: 0, x2: 1, y2: 1, z2: 0}), 1) + self.assertEquals(d.subs({x0: 2, y0: 0, z0: 0, x1: 0, y1: 2, z1: 0, x2: 2, y2: 0, z2: 0}), 0) def test_metric_common_normal_line(self): """ @@ -314,3 +333,20 @@ def test_metric_common_normal_line(self): self.assertEquals(acos(l0 | ln), S.Half * pi) self.assertEquals(acos(l1 | ln), S.Half * pi) + + def test_metric_angle_between_lines(self): + """ + We can measure the angle between to normalized lines. + """ + x1, y1 = symbols('x1 y1', real=True) + P0 = self.point(0, 0, 0) + P1 = self.point(x1, y1, 0) + P2 = self.point(-y1, x1, 0) + + l0 = Jinv(J(P0) ^ J(P1)) # TODO: this feels weird... but ganja does the same + l1 = Jinv(J(P2) ^ J(P0)) # + + l0 /= self.norm(l0) + l1 /= self.norm(l1) + + self.assertEquals(acos(l0 | l1), pi / 2) From 1e2cda96b09da9953d4ad10f1abfd71245fbb88e Mon Sep 17 00:00:00 2001 From: Sylvain Meunier Date: Thu, 31 Oct 2019 14:37:33 +0100 Subject: [PATCH 63/78] Cleanup, rotors and translators (work in progress) --- tests/test_pga.py | 182 ++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 174 insertions(+), 8 deletions(-) diff --git a/tests/test_pga.py b/tests/test_pga.py index f0898389..1a0a852b 100644 --- a/tests/test_pga.py +++ b/tests/test_pga.py @@ -133,14 +133,13 @@ def test_geometry_incidence_planes_meet_into_points(self): p1 = self.plane(1, 0, 0, -x) p2 = self.plane(0, 1, 0, -y) p3 = self.plane(0, 0, 1, -z) - - P = p1 ^ p2 ^ p3 - self.assertProjEquals(P, self.point(x, y, z)) - self.assertProjEquals(P, p1 ^ p3 ^ p2) - self.assertProjEquals(P, p2 ^ p1 ^ p3) - self.assertProjEquals(P, p2 ^ p3 ^ p1) - self.assertProjEquals(P, p3 ^ p1 ^ p2) - self.assertProjEquals(P, p3 ^ p2 ^ p1) + R = self.point(x, y, z) + self.assertProjEquals(R, p1 ^ p2 ^ p3) + self.assertProjEquals(R, p1 ^ p3 ^ p2) + self.assertProjEquals(R, p2 ^ p1 ^ p3) + self.assertProjEquals(R, p2 ^ p3 ^ p1) + self.assertProjEquals(R, p3 ^ p1 ^ p2) + self.assertProjEquals(R, p3 ^ p2 ^ p1) def test_geometry_incidence_planes_meet_into_points_2(self): """ @@ -350,3 +349,170 @@ def test_metric_angle_between_lines(self): l1 /= self.norm(l1) self.assertEquals(acos(l0 | l1), pi / 2) + + @staticmethod + def rotor_cs(alpha, l): + return cos(alpha / 2) + sin(alpha / 2) * l + + @staticmethod + def rotor_exp(alpha, l): + return (alpha / 2 * l).exp() + + def test_motors_rotator(self): + """ + Rotate anything around a normalized line. + """ + Or = self.point(+0, +0, +0) + Xp = self.point(+1, +0, +0) + Yp = self.point(+0, +1, +0) + Zp = self.point(+0, +0, +1) + Xm = self.point(-1, +0, +0) + Ym = self.point(+0, -1, +0) + Zm = self.point(+0, +0, -1) + + lXp = Jinv(J(Or) ^ J(Xp)) + lXm = Jinv(J(Or) ^ J(Xm)) + lYp = Jinv(J(Or) ^ J(Yp)) + lYm = Jinv(J(Or) ^ J(Ym)) + lZp = Jinv(J(Or) ^ J(Zp)) + lZm = Jinv(J(Or) ^ J(Zm)) + + d = Symbol('d') + pXp = self.plane(+1, 0, 0, d) + pXm = self.plane(-1, 0, 0, d) + pYp = self.plane(0, +1, 0, d) + pYm = self.plane(0, -1, 0, d) + pZp = self.plane(0, 0, +1, d) + pZm = self.plane(0, 0, -1, d) + + # Around X+ + RXp = self.rotor_cs(pi / 2, lXp) + self.assertEquals(RXp, self.rotor_exp(pi / 2, lXp)) + # Points + self.assertProjEquals(RXp * Yp * RXp.rev(), Zp) + self.assertProjEquals(RXp * Zp * RXp.rev(), Ym) + self.assertProjEquals(RXp * Ym * RXp.rev(), Zm) + self.assertProjEquals(RXp * Zm * RXp.rev(), Yp) + # Lines + self.assertProjEquals(RXp * lYp * RXp.rev(), lZp) + self.assertProjEquals(RXp * lZp * RXp.rev(), lYm) + self.assertProjEquals(RXp * lYm * RXp.rev(), lZm) + self.assertProjEquals(RXp * lZm * RXp.rev(), lYp) + # Planes + self.assertProjEquals(RXp * pYp * RXp.rev(), pZp) + self.assertProjEquals(RXp * pZp * RXp.rev(), pYm) + self.assertProjEquals(RXp * pYm * RXp.rev(), pZm) + self.assertProjEquals(RXp * pZm * RXp.rev(), pYp) + + # Around X- + RXm = self.rotor_cs(pi / 2, lXm) + self.assertEquals(RXm, self.rotor_exp(pi / 2, lXm)) + # Points + self.assertProjEquals(RXm * Yp * RXm.rev(), Zm) + self.assertProjEquals(RXm * Zm * RXm.rev(), Ym) + self.assertProjEquals(RXm * Ym * RXm.rev(), Zp) + self.assertProjEquals(RXm * Zp * RXm.rev(), Yp) + # Lines + self.assertProjEquals(RXm * lYp * RXm.rev(), lZm) + self.assertProjEquals(RXm * lZm * RXm.rev(), lYm) + self.assertProjEquals(RXm * lYm * RXm.rev(), lZp) + self.assertProjEquals(RXm * lZp * RXm.rev(), lYp) + # Planes + self.assertProjEquals(RXm * pYp * RXm.rev(), pZm) + self.assertProjEquals(RXm * pZm * RXm.rev(), pYm) + self.assertProjEquals(RXm * pYm * RXm.rev(), pZp) + self.assertProjEquals(RXm * pZp * RXm.rev(), pYp) + + # Around Y+ + RYp = self.rotor_cs(pi / 2, lYp) + self.assertEquals(RYp, self.rotor_exp(pi / 2, lYp)) + # Points + self.assertProjEquals(RYp * Xp * RYp.rev(), Zm) + self.assertProjEquals(RYp * Zm * RYp.rev(), Xm) + self.assertProjEquals(RYp * Xm * RYp.rev(), Zp) + self.assertProjEquals(RYp * Zp * RYp.rev(), Xp) + # Lines + self.assertProjEquals(RYp * lXp * RYp.rev(), lZm) + self.assertProjEquals(RYp * lZm * RYp.rev(), lXm) + self.assertProjEquals(RYp * lXm * RYp.rev(), lZp) + self.assertProjEquals(RYp * lZp * RYp.rev(), lXp) + # Planes + self.assertProjEquals(RYp * pXp * RYp.rev(), pZm) + self.assertProjEquals(RYp * pZm * RYp.rev(), pXm) + self.assertProjEquals(RYp * pXm * RYp.rev(), pZp) + self.assertProjEquals(RYp * pZp * RYp.rev(), pXp) + + # Around Y- + RYm = self.rotor_cs(pi / 2, lYm) + self.assertEquals(RYm, self.rotor_exp(pi / 2, lYm)) + # Points + self.assertProjEquals(RYm * Xp * RYm.rev(), Zp) + self.assertProjEquals(RYm * Zp * RYm.rev(), Xm) + self.assertProjEquals(RYm * Xm * RYm.rev(), Zm) + self.assertProjEquals(RYm * Zm * RYm.rev(), Xp) + # Lines + self.assertProjEquals(RYm * lXp * RYm.rev(), lZp) + self.assertProjEquals(RYm * lZp * RYm.rev(), lXm) + self.assertProjEquals(RYm * lXm * RYm.rev(), lZm) + self.assertProjEquals(RYm * lZm * RYm.rev(), lXp) + # Planes + self.assertProjEquals(RYm * pXp * RYm.rev(), pZp) + self.assertProjEquals(RYm * pZp * RYm.rev(), pXm) + self.assertProjEquals(RYm * pXm * RYm.rev(), pZm) + self.assertProjEquals(RYm * pZm * RYm.rev(), pXp) + + # Around Z+ + RZp = self.rotor_cs(pi / 2, lZp) + self.assertEquals(RZp, self.rotor_exp(pi / 2, lZp)) + # Points + self.assertProjEquals(RZp * Xp * RZp.rev(), Yp) + self.assertProjEquals(RZp * Yp * RZp.rev(), Xm) + self.assertProjEquals(RZp * Xm * RZp.rev(), Ym) + self.assertProjEquals(RZp * Ym * RZp.rev(), Xp) + # Lines + self.assertProjEquals(RZp * lXp * RZp.rev(), lYp) + self.assertProjEquals(RZp * lYp * RZp.rev(), lXm) + self.assertProjEquals(RZp * lXm * RZp.rev(), lYm) + self.assertProjEquals(RZp * lYm * RZp.rev(), lXp) + # Planes + self.assertProjEquals(RZp * pXp * RZp.rev(), pYp) + self.assertProjEquals(RZp * pYp * RZp.rev(), pXm) + self.assertProjEquals(RZp * pXm * RZp.rev(), pYm) + self.assertProjEquals(RZp * pYm * RZp.rev(), pXp) + + # Around Z- + RZm = self.rotor_cs(pi / 2, lZm) + self.assertEquals(RZm, self.rotor_exp(pi / 2, lZm)) + # Points + self.assertProjEquals(RZm * Xp * RZm.rev(), Ym) + self.assertProjEquals(RZm * Ym * RZm.rev(), Xm) + self.assertProjEquals(RZm * Xm * RZm.rev(), Yp) + self.assertProjEquals(RZm * Yp * RZm.rev(), Xp) + # Lines + self.assertProjEquals(RZm * lXp * RZm.rev(), lYm) + self.assertProjEquals(RZm * lYm * RZm.rev(), lXm) + self.assertProjEquals(RZm * lXm * RZm.rev(), lYp) + self.assertProjEquals(RZm * lYp * RZm.rev(), lXp) + # Planes + self.assertProjEquals(RZm * pXp * RZm.rev(), pYm) + self.assertProjEquals(RZm * pYm * RZm.rev(), pXm) + self.assertProjEquals(RZm * pXm * RZm.rev(), pYp) + self.assertProjEquals(RZm * pYp * RZm.rev(), pXp) + + def translator(self, d, l): + return 1 + (d / 2) * (l * self.e_0123) + + def test_motors_translator(self): + """ + Translate anything by a normalized line. + """ + x0, y0, z0 = symbols('x0 y0 z0') + Px = self.point(x0, y0, z0) + + P0 = self.point(0, 0, 0) + P1 = self.point(1, 0, 0) + l = Jinv(J(P0) ^ J(P1)) + + d = Symbol('d') + T = self.translator(d, l) + self.assertProjEquals(T * Px * T.rev(), self.point(x0 - d, y0, z0)) # TODO : like ganja.js but weird... From 229bd4e318e0916f0fddf62c3e4874ba3049ee8f Mon Sep 17 00:00:00 2001 From: Sylvain Meunier Date: Thu, 31 Oct 2019 21:38:58 +0100 Subject: [PATCH 64/78] implement custom basis (like enki) and add more tests --- galgebra/ga.py | 118 ++++++++++------- galgebra/metric.py | 3 +- galgebra/mv.py | 4 +- tests/test_pga.py | 318 +++++++++++++++++++++++++++++++++++++++++++-- 4 files changed, 384 insertions(+), 59 deletions(-) diff --git a/galgebra/ga.py b/galgebra/ga.py index 95ba1fc0..32610d8b 100755 --- a/galgebra/ga.py +++ b/galgebra/ga.py @@ -316,7 +316,7 @@ def __init__(self, bases, **kwargs): metric.Metric.__init__(self, bases, **kwargs) self.par_coords = None - self.build_bases() + self.build_bases(kwargs.get('sign_and_indexes', None)) self.dot_mode = '|' self.basis_product_tables() @@ -528,47 +528,54 @@ def parametric(self, coords): def basis_vectors(self): return tuple(self.basis) - def build_cobases(self): + def build_cobases(self, coindexes=None): """ Cobases for building Poincare duality, this is useful for defining wedge and vee without using I nor any metric. """ - basis_indexes = tuple(self.n_range) - self.coindexes = [] - self.coindexes_lst = [] - for i in reversed(basis_indexes): - base_tuple = tuple(reversed(tuple(combinations(basis_indexes, i + 1)))) - self.coindexes.append(base_tuple) - self.coindexes_lst += list(base_tuple) - self.coindexes.append(()) - self.coindexes = tuple(self.coindexes) - - self.coblades_lst = [] - self.coblades_inv_lst = [] + # TODO: check this can be used with another GA than 3D PGA... + if coindexes is None: + basis_indexes = tuple(self.n_range) + self.coindexes = [] + self.coindexes_lst = [] + for i in reversed(basis_indexes): + base_tuple = tuple(reversed(tuple(combinations(basis_indexes, i + 1)))) + self.coindexes.append(base_tuple) + self.coindexes_lst += list(base_tuple) + self.coindexes.append(()) + self.coindexes = tuple(self.coindexes) + else: + self.coindexes = coindexes + self.coindexes_lst = [index for cograde_index in coindexes for index in cograde_index] def format_symbol_name(symbol_index): return (''.join([str(self.basis[i]) + '^' for i in symbol_index]))[:-1] - assert self.indexes[0] == () and len(self.coindexes[0]) == 1 - cobase_index = self.coindexes[0][0] - coblade = Symbol(format_symbol_name(cobase_index), commutative=False) - coblade_inv = S(1) - self.coblades_lst.append(coblade) - self.coblades_inv_lst.append(coblade_inv) + n = self.n - for grade_index, cograde_index in zip(self.indexes[1:], self.coindexes[1:]): - for base_index, cobase_index in zip(grade_index, cograde_index): - coblade_sign = 1 if Permutation(base_index + cobase_index).is_even else -1 + self.coblades_lst = [] + for cograde_index in self.coindexes: + k = len(cograde_index[0]) if len(cograde_index) > 0 else 0 + for cobase_index in cograde_index: + coblade_sign = -1 if k == n - 1 and k % 2 == 1 else 1 coblade = coblade_sign * Symbol(format_symbol_name(cobase_index), commutative=False) - coblade_inv = coblade_sign * Symbol(format_symbol_name(base_index), commutative=False) self.coblades_lst.append(coblade) - self.coblades_inv_lst.append(coblade_inv) + self.coblades_lst0 = self.coblades_lst + [S(1),] + self.coblades_inv_lst = [] + for grade_index in self.indexes: + k = len(grade_index[0]) if len(grade_index) > 0 else 0 + for base_index in grade_index: + coblade_inv_sign = -1 if k == 1 and k % 2 == 1 else 1 + coblade_inv = coblade_inv_sign * Symbol(format_symbol_name(base_index), commutative=False) + self.coblades_inv_lst.append(coblade_inv) self.coblades_inv_lst = list(reversed(self.coblades_inv_lst)) + self.coblades_inv_lst0 = self.coblades_inv_lst + [S(1),] - self.coblades_lst0 = self.coblades_lst + [S(1),] - self.coblades_inv_lst0 = [Symbol(format_symbol_name(self.indexes[-1][0]), commutative=False)] + self.coblades_inv_lst + self.mv_blades_lst0 = [] + for obj in self.blades_lst0: + self.mv_blades_lst0.append(self.mv(obj)) - def build_bases(self): + def build_bases(self, sign_and_indexes=None): """ The bases for the multivector (geometric) algebra are formed from all combinations of the bases of the vector space and the scalars. @@ -604,14 +611,18 @@ def build_bases(self): """ # index list for multivector bases and blades by grade - basis_indexes = tuple(self.n_range) - self.indexes = [()] - self.indexes_lst = [] - for i in basis_indexes: - base_tuple = tuple(combinations(basis_indexes, i + 1)) - self.indexes.append(base_tuple) - self.indexes_lst += list(base_tuple) - self.indexes = tuple(self.indexes) + if sign_and_indexes is None: + basis_indexes = tuple(self.n_range) + self.indexes = [()] + self.indexes_lst = [] + for i in basis_indexes: + base_tuple = tuple(combinations(basis_indexes, i + 1)) + self.indexes.append(base_tuple) + self.indexes_lst += list(base_tuple) + self.indexes = tuple(self.indexes) + else: + self.indexes = sign_and_indexes[1] + self.indexes_lst = [index for grade_index in self.indexes for index in grade_index] # list of non-commutative symbols for multivector bases and blades # by grade and as a flattened list @@ -634,9 +645,20 @@ def build_bases(self): self.blades_to_indexes = [] self.indexes_to_blades = [] - for (index, blade) in zip(self.indexes_lst, self.blades_lst): - self.blades_to_indexes.append((blade, index)) - self.indexes_to_blades.append((index, blade)) + if sign_and_indexes is None: + for (index, blade) in zip(self.indexes_lst, self.blades_lst): + self.blades_to_indexes.append((blade, (1, index))) + self.indexes_to_blades.append((index, blade)) + else: + basis_indexes = tuple(self.n_range) + default_indexes_lst = [] + for i in basis_indexes: + base_tuple = tuple(combinations(basis_indexes, i + 1)) + default_indexes_lst += list(base_tuple) + signs_lst = [sign for grade_sign in sign_and_indexes[0] for sign in grade_sign] + for (default_index, sign, blade) in zip(default_indexes_lst, signs_lst, self.blades_lst): + self.blades_to_indexes.append((blade, (sign, default_index))) + self.indexes_to_blades.append((default_index, sign * blade)) self.blades_to_indexes_dict = OrderedDict(self.blades_to_indexes) self.indexes_to_blades_dict = OrderedDict(self.indexes_to_blades) @@ -778,11 +800,11 @@ def geometric_product_basis_blades(self, blade12): # geometric (*) product for orthogonal basis if self.is_ortho: (blade1, blade2) = blade12 - index1 = self.blades_to_indexes_dict[blade1] - index2 = self.blades_to_indexes_dict[blade2] + sign1, index1 = self.blades_to_indexes_dict[blade1] + sign2, index2 = self.blades_to_indexes_dict[blade2] blade_index = list(index1 + index2) repeats = [] - sgn = 1 + sgn = sign1 * sign2 for i in range(1, len(blade_index)): save = blade_index[i] j = i @@ -921,15 +943,15 @@ def wedge_product_basis_blades(self, blade12): # blade12 = blade1*blade2 # outer (^) product of basis blades # this method works for both orthogonal and non-orthogonal basis (blade1, blade2) = blade12 - index1 = self.blades_to_indexes_dict[blade1] - index2 = self.blades_to_indexes_dict[blade2] + sign1, index1 = self.blades_to_indexes_dict[blade1] + sign2, index2 = self.blades_to_indexes_dict[blade2] index12 = list(index1 + index2) if len(index12) > self.n: return 0 (sgn, wedge12) = Ga.blade_reduce(index12) if sgn != 0: - return(sgn * self.indexes_to_blades_dict[tuple(wedge12)]) + return(sgn * sign1 * sign2 * self.indexes_to_blades_dict[tuple(wedge12)]) else: return 0 @@ -939,8 +961,8 @@ def dot_product_basis_blades(self, blade12): # dot (|), left (<), and right (>) products # dot product for orthogonal basis (blade1, blade2) = blade12 - index1 = self.blades_to_indexes_dict[blade1] - index2 = self.blades_to_indexes_dict[blade2] + sign1, index1 = self.blades_to_indexes_dict[blade1] + sign2, index2 = self.blades_to_indexes_dict[blade2] index = list(index1 + index2) grade1 = len(index1) grade2 = len(index2) @@ -956,7 +978,7 @@ def dot_product_basis_blades(self, blade12): if grade < 0: return 0 n = len(index) - sgn = 1 + sgn = sign1 * sign2 result = 1 ordered = False while n > grade: diff --git a/galgebra/metric.py b/galgebra/metric.py index a42387b5..29434c5d 100755 --- a/galgebra/metric.py +++ b/galgebra/metric.py @@ -256,7 +256,8 @@ class Metric(object): 'norm': (False, 'True to normalize basis vectors'), 'debug': (False, 'True to print out debugging information'), 'gsym': (None, 'String s to use "det("+s+")" function in reciprocal basis'), - 'sig': ('e', 'Signature of metric, default is (n,0) a Euclidean metric')} + 'sig': ('e', 'Signature of metric, default is (n,0) a Euclidean metric'), + 'sign_and_indexes': (None, 'bases indexes')} @staticmethod def dot_orthogonal(V1, V2, g=None): diff --git a/galgebra/mv.py b/galgebra/mv.py index a5a46aa9..9b6fe99c 100755 --- a/galgebra/mv.py +++ b/galgebra/mv.py @@ -994,14 +994,14 @@ def get_coefs(self, grade): def J(self): # TODO: If we pick our basis smartly, we can probably use J in place of Jinv... obj = 0 - for coef, coblade in zip(self.blade_coefs(), self.Ga.coblades_lst0): + for coef, coblade in zip(self.blade_coefs(self.Ga.mv_blades_lst0), self.Ga.coblades_lst0): obj += coef * coblade return Mv(obj, ga=self.Ga) def Jinv(self): # TODO: If we pick our basis smartly, we can probably use J in place of Jinv... obj = 0 - for coef, coblade_inv in zip(self.blade_coefs(), self.Ga.coblades_inv_lst0): + for coef, coblade_inv in zip(self.blade_coefs(self.Ga.mv_blades_lst0), self.Ga.coblades_inv_lst0): obj += coef * coblade_inv return Mv(obj, ga=self.Ga) diff --git a/tests/test_pga.py b/tests/test_pga.py index 1a0a852b..5039011b 100644 --- a/tests/test_pga.py +++ b/tests/test_pga.py @@ -1,6 +1,6 @@ import unittest -from sympy import acos, asin, pi, Rational, S, simplify, sqrt, Symbol, symbols +from sympy import acos, asin, cos, sin, pi, Rational, S, simplify, sqrt, Symbol, symbols from ga import Ga from mv import Mv, J, Jinv, com @@ -20,7 +20,7 @@ def assertEquals(self, first, second, msg=None): diff = simplify(first - second) - self.assertTrue(diff == 0, "\n%s\n==\n%s\n%s" % (first, second, diff)) + self.assertTrue(diff == 0, "\n%s\n%s\n==\n%s\n%s" % (msg, first, second, diff)) def assertProjEquals(self, X, Y): """ @@ -43,10 +43,36 @@ def setUp(self): """ Setup 3D Projective Geometric Algebra aka PGA. """ - PGA, e_0, e_1, e_2, e_3 = Ga.build('e*0|1|2|3', g=[0, 1, 1, 1]) + indexes = ( + (), + ((0,), (1,), (2,), (3,)), + ((0, 1), (0, 2), (0, 3), (1, 2), (3, 1), (2, 3)), + ((0, 2, 1), (0, 1, 3), (0, 3, 2), (1, 2, 3)), + ((0, 1, 2, 3),) + ) + + # Internal products use lexical ordering for computing expressions + signs = ( + (), + (1, 1, 1, 1), + (1, 1, 1, 1, -1, 1), + (-1, 1, -1, 1), + (1,) + ) + + # TODO: build this from indexes... + coindexes = ( + ((0, 1, 2, 3),), + ((1, 2, 3), (0, 3, 2), (0, 1, 3), (0, 2, 1)), + ((2, 3), (3, 1), (1, 2), (0, 3), (0, 2), (0, 1)), + ((3,), (2,), (1,), (0,)), + (), + ) + + PGA, e_0, e_1, e_2, e_3 = Ga.build('e*0|1|2|3', g=[0, 1, 1, 1], sign_and_indexes=(signs, indexes)) # TODO: move this somewhere useful... - PGA.build_cobases() + PGA.build_cobases(coindexes=coindexes) self.PGA = PGA self.e_0 = e_0 @@ -112,6 +138,282 @@ def ideal_norm(self, X): raise ValueError("X k-vector is null") return sqrt(squared_norm) + def test_multiplication_table(self): + """ + Reproduce multiplication table. + """ + # 1 + self.assertEquals(S.One * self.e_0, self.e_0) + self.assertEquals(S.One * self.e_1, self.e_1) + self.assertEquals(S.One * self.e_2, self.e_2) + self.assertEquals(S.One * self.e_3, self.e_3) + self.assertEquals(S.One * self.e_01, self.e_01) + self.assertEquals(S.One * self.e_02, self.e_02) + self.assertEquals(S.One * self.e_03, self.e_03) + self.assertEquals(S.One * self.e_12, self.e_12) + self.assertEquals(S.One * self.e_31, self.e_31) + self.assertEquals(S.One * self.e_23, self.e_23) + self.assertEquals(S.One * self.e_021, self.e_021) + self.assertEquals(S.One * self.e_013, self.e_013) + self.assertEquals(S.One * self.e_032, self.e_032) + self.assertEquals(S.One * self.e_123, self.e_123) + self.assertEquals(S.One * self.e_0123, self.e_0123) + + # e0 + self.assertEquals(self.e_0 * self.e_0, S.Zero) + self.assertEquals(self.e_0 * self.e_1, self.e_01) + self.assertEquals(self.e_0 * self.e_2, self.e_02) + self.assertEquals(self.e_0 * self.e_3, self.e_03) + self.assertEquals(self.e_0 * self.e_01, S.Zero) + self.assertEquals(self.e_0 * self.e_02, S.Zero) + self.assertEquals(self.e_0 * self.e_03, S.Zero) + self.assertEquals(self.e_0 * self.e_12, -self.e_021) + self.assertEquals(self.e_0 * self.e_31, -self.e_013) + self.assertEquals(self.e_0 * self.e_23, -self.e_032) + self.assertEquals(self.e_0 * self.e_021, S.Zero) + self.assertEquals(self.e_0 * self.e_013, S.Zero) + self.assertEquals(self.e_0 * self.e_032, S.Zero) + self.assertEquals(self.e_0 * self.e_123, self.e_0123) + self.assertEquals(self.e_0 * self.e_0123, S.Zero) + + # e1 + self.assertEquals(self.e_1 * self.e_0, -self.e_01) + self.assertEquals(self.e_1 * self.e_1, S.One) + self.assertEquals(self.e_1 * self.e_2, self.e_12) + self.assertEquals(self.e_1 * self.e_3, -self.e_31) + self.assertEquals(self.e_1 * self.e_01, -self.e_0) + self.assertEquals(self.e_1 * self.e_02, self.e_021) + self.assertEquals(self.e_1 * self.e_03, -self.e_013) + self.assertEquals(self.e_1 * self.e_12, self.e_2) + self.assertEquals(self.e_1 * self.e_31, -self.e_3) + self.assertEquals(self.e_1 * self.e_23, self.e_123) + self.assertEquals(self.e_1 * self.e_021, self.e_02) + self.assertEquals(self.e_1 * self.e_013, -self.e_03) + self.assertEquals(self.e_1 * self.e_032, self.e_0123) + self.assertEquals(self.e_1 * self.e_123, self.e_23) + self.assertEquals(self.e_1 * self.e_0123, self.e_032) + + # e2 + self.assertEquals(self.e_2 * self.e_0, -self.e_02) + self.assertEquals(self.e_2 * self.e_1, -self.e_12) + self.assertEquals(self.e_2 * self.e_2, S.One) + self.assertEquals(self.e_2 * self.e_3, self.e_23) + self.assertEquals(self.e_2 * self.e_01, -self.e_021) + self.assertEquals(self.e_2 * self.e_02, -self.e_0) + self.assertEquals(self.e_2 * self.e_03, self.e_032) + self.assertEquals(self.e_2 * self.e_12, -self.e_1) + self.assertEquals(self.e_2 * self.e_31, self.e_123) + self.assertEquals(self.e_2 * self.e_23, self.e_3) + self.assertEquals(self.e_2 * self.e_021, -self.e_01) + self.assertEquals(self.e_2 * self.e_013, self.e_0123) + self.assertEquals(self.e_2 * self.e_032, self.e_03) + self.assertEquals(self.e_2 * self.e_123, self.e_31) + self.assertEquals(self.e_2 * self.e_0123, self.e_013) + + # e3 + self.assertEquals(self.e_3 * self.e_0, -self.e_03) + self.assertEquals(self.e_3 * self.e_1, self.e_31) + self.assertEquals(self.e_3 * self.e_2, -self.e_23) + self.assertEquals(self.e_3 * self.e_3, S.One) + self.assertEquals(self.e_3 * self.e_01, self.e_013) + self.assertEquals(self.e_3 * self.e_02, -self.e_032) + self.assertEquals(self.e_3 * self.e_03, -self.e_0) + self.assertEquals(self.e_3 * self.e_12, self.e_123) + self.assertEquals(self.e_3 * self.e_31, self.e_1) + self.assertEquals(self.e_3 * self.e_23, -self.e_2) + self.assertEquals(self.e_3 * self.e_021, self.e_0123) + self.assertEquals(self.e_3 * self.e_013, self.e_01) + self.assertEquals(self.e_3 * self.e_032, -self.e_02) + self.assertEquals(self.e_3 * self.e_123, self.e_12) + self.assertEquals(self.e_3 * self.e_0123, self.e_021) + + # e01 + self.assertEquals(self.e_01 * self.e_0, S.Zero) + self.assertEquals(self.e_01 * self.e_1, self.e_0) + self.assertEquals(self.e_01 * self.e_2, -self.e_021) + self.assertEquals(self.e_01 * self.e_3, self.e_013) + self.assertEquals(self.e_01 * self.e_01, S.Zero) + self.assertEquals(self.e_01 * self.e_02, S.Zero) + self.assertEquals(self.e_01 * self.e_03, S.Zero) + self.assertEquals(self.e_01 * self.e_12, self.e_02) + self.assertEquals(self.e_01 * self.e_31, -self.e_03) + self.assertEquals(self.e_01 * self.e_23, self.e_0123) + self.assertEquals(self.e_01 * self.e_021, S.Zero) + self.assertEquals(self.e_01 * self.e_013, S.Zero) + self.assertEquals(self.e_01 * self.e_032, S.Zero) + self.assertEquals(self.e_01 * self.e_123, -self.e_032) + self.assertEquals(self.e_01 * self.e_0123, S.Zero) + + # e02 + self.assertEquals(self.e_02 * self.e_0, S.Zero) + self.assertEquals(self.e_02 * self.e_1, self.e_021) + self.assertEquals(self.e_02 * self.e_2, self.e_0) + self.assertEquals(self.e_02 * self.e_3, -self.e_032) + self.assertEquals(self.e_02 * self.e_01, S.Zero) + self.assertEquals(self.e_02 * self.e_02, S.Zero) + self.assertEquals(self.e_02 * self.e_03, S.Zero) + self.assertEquals(self.e_02 * self.e_12, -self.e_01) + self.assertEquals(self.e_02 * self.e_31, self.e_0123) + self.assertEquals(self.e_02 * self.e_23, self.e_03) + self.assertEquals(self.e_02 * self.e_021, S.Zero) + self.assertEquals(self.e_02 * self.e_013, S.Zero) + self.assertEquals(self.e_02 * self.e_032, S.Zero) + self.assertEquals(self.e_02 * self.e_123, -self.e_013) + self.assertEquals(self.e_02 * self.e_0123, S.Zero) + + # e03 + self.assertEquals(self.e_03 * self.e_0, S.Zero) + self.assertEquals(self.e_03 * self.e_1, -self.e_013) + self.assertEquals(self.e_03 * self.e_2, self.e_032) + self.assertEquals(self.e_03 * self.e_3, self.e_0) + self.assertEquals(self.e_03 * self.e_01, S.Zero) + self.assertEquals(self.e_03 * self.e_02, S.Zero) + self.assertEquals(self.e_03 * self.e_03, S.Zero) + self.assertEquals(self.e_03 * self.e_12, self.e_0123) + self.assertEquals(self.e_03 * self.e_31, self.e_01) + self.assertEquals(self.e_03 * self.e_23, -self.e_02) + self.assertEquals(self.e_03 * self.e_021, S.Zero) + self.assertEquals(self.e_03 * self.e_013, S.Zero) + self.assertEquals(self.e_03 * self.e_032, S.Zero) + self.assertEquals(self.e_03 * self.e_123, -self.e_021) + self.assertEquals(self.e_03 * self.e_0123, S.Zero) + + # e12 + self.assertEquals(self.e_12 * self.e_0, -self.e_021) + self.assertEquals(self.e_12 * self.e_1, -self.e_2) + self.assertEquals(self.e_12 * self.e_2, self.e_1) + self.assertEquals(self.e_12 * self.e_3, self.e_123) + self.assertEquals(self.e_12 * self.e_01, -self.e_02) + self.assertEquals(self.e_12 * self.e_02, self.e_01) + self.assertEquals(self.e_12 * self.e_03, self.e_0123) + self.assertEquals(self.e_12 * self.e_12, -S.One) + self.assertEquals(self.e_12 * self.e_31, self.e_23) + self.assertEquals(self.e_12 * self.e_23, -self.e_31) + self.assertEquals(self.e_12 * self.e_021, self.e_0) + self.assertEquals(self.e_12 * self.e_013, self.e_032) + self.assertEquals(self.e_12 * self.e_032, -self.e_013) + self.assertEquals(self.e_12 * self.e_123, -self.e_3) + self.assertEquals(self.e_12 * self.e_0123, -self.e_03) + + # e31 + self.assertEquals(self.e_31 * self.e_0, -self.e_013) + self.assertEquals(self.e_31 * self.e_1, self.e_3) + self.assertEquals(self.e_31 * self.e_2, self.e_123) + self.assertEquals(self.e_31 * self.e_3, -self.e_1) + self.assertEquals(self.e_31 * self.e_01, self.e_03) + self.assertEquals(self.e_31 * self.e_02, self.e_0123) + self.assertEquals(self.e_31 * self.e_03, -self.e_01) + self.assertEquals(self.e_31 * self.e_12, -self.e_23) + self.assertEquals(self.e_31 * self.e_31, -S.One) + self.assertEquals(self.e_31 * self.e_23, self.e_12) + self.assertEquals(self.e_31 * self.e_021, -self.e_032) + self.assertEquals(self.e_31 * self.e_013, self.e_0) + self.assertEquals(self.e_31 * self.e_032, self.e_021) + self.assertEquals(self.e_31 * self.e_123, -self.e_2) + self.assertEquals(self.e_31 * self.e_0123, -self.e_02) + + # e23 + self.assertEquals(self.e_23 * self.e_0, -self.e_032) + self.assertEquals(self.e_23 * self.e_1, self.e_123) + self.assertEquals(self.e_23 * self.e_2, -self.e_3) + self.assertEquals(self.e_23 * self.e_3, self.e_2) + self.assertEquals(self.e_23 * self.e_01, self.e_0123) + self.assertEquals(self.e_23 * self.e_02, -self.e_03) + self.assertEquals(self.e_23 * self.e_03, self.e_02) + self.assertEquals(self.e_23 * self.e_12, self.e_31) + self.assertEquals(self.e_23 * self.e_31, -self.e_12) + self.assertEquals(self.e_23 * self.e_23, -S.One) + self.assertEquals(self.e_23 * self.e_021, self.e_013) + self.assertEquals(self.e_23 * self.e_013, -self.e_021) + self.assertEquals(self.e_23 * self.e_032, self.e_0) + self.assertEquals(self.e_23 * self.e_123, -self.e_1) + self.assertEquals(self.e_23 * self.e_0123, -self.e_01) + + # e021 + self.assertEquals(self.e_021 * self.e_0, S.Zero) + self.assertEquals(self.e_021 * self.e_1, self.e_02) + self.assertEquals(self.e_021 * self.e_2, -self.e_01) + self.assertEquals(self.e_021 * self.e_3, -self.e_0123) + self.assertEquals(self.e_021 * self.e_01, S.Zero) + self.assertEquals(self.e_021 * self.e_02, S.Zero) + self.assertEquals(self.e_021 * self.e_03, S.Zero) + self.assertEquals(self.e_021 * self.e_12, self.e_0) + self.assertEquals(self.e_021 * self.e_31, self.e_032) + self.assertEquals(self.e_021 * self.e_23, -self.e_013) + self.assertEquals(self.e_021 * self.e_021, S.Zero) + self.assertEquals(self.e_021 * self.e_013, S.Zero) + self.assertEquals(self.e_021 * self.e_032, S.Zero) + self.assertEquals(self.e_021 * self.e_123, self.e_03) + self.assertEquals(self.e_021 * self.e_0123, S.Zero) + + # e013 + self.assertEquals(self.e_013 * self.e_0, S.Zero) + self.assertEquals(self.e_013 * self.e_1, -self.e_03) + self.assertEquals(self.e_013 * self.e_2, -self.e_0123) + self.assertEquals(self.e_013 * self.e_3, self.e_01) + self.assertEquals(self.e_013 * self.e_01, S.Zero) + self.assertEquals(self.e_013 * self.e_02, S.Zero) + self.assertEquals(self.e_013 * self.e_03, S.Zero) + self.assertEquals(self.e_013 * self.e_12, -self.e_032) + self.assertEquals(self.e_013 * self.e_31, self.e_0) + self.assertEquals(self.e_013 * self.e_23, self.e_021) + self.assertEquals(self.e_013 * self.e_021, S.Zero) + self.assertEquals(self.e_013 * self.e_013, S.Zero) + self.assertEquals(self.e_013 * self.e_032, S.Zero) + self.assertEquals(self.e_013 * self.e_123, self.e_02) + self.assertEquals(self.e_013 * self.e_0123, S.Zero) + + # e032 + self.assertEquals(self.e_032 * self.e_0, S.Zero) + self.assertEquals(self.e_032 * self.e_1, -self.e_0123) + self.assertEquals(self.e_032 * self.e_2, self.e_03) + self.assertEquals(self.e_032 * self.e_3, -self.e_02) + self.assertEquals(self.e_032 * self.e_01, S.Zero) + self.assertEquals(self.e_032 * self.e_02, S.Zero) + self.assertEquals(self.e_032 * self.e_03, S.Zero) + self.assertEquals(self.e_032 * self.e_12, self.e_013) + self.assertEquals(self.e_032 * self.e_31, -self.e_021) + self.assertEquals(self.e_032 * self.e_23, self.e_0) + self.assertEquals(self.e_032 * self.e_021, S.Zero) + self.assertEquals(self.e_032 * self.e_013, S.Zero) + self.assertEquals(self.e_032 * self.e_032, S.Zero) + self.assertEquals(self.e_032 * self.e_123, self.e_01) + self.assertEquals(self.e_032 * self.e_0123, S.Zero) + + # e123 + self.assertEquals(self.e_123 * self.e_0, -self.e_0123) + self.assertEquals(self.e_123 * self.e_1, self.e_23) + self.assertEquals(self.e_123 * self.e_2, self.e_31) + self.assertEquals(self.e_123 * self.e_3, self.e_12) + self.assertEquals(self.e_123 * self.e_01, self.e_032) + self.assertEquals(self.e_123 * self.e_02, self.e_013) + self.assertEquals(self.e_123 * self.e_03, self.e_021) + self.assertEquals(self.e_123 * self.e_12, -self.e_3) + self.assertEquals(self.e_123 * self.e_31, -self.e_2) + self.assertEquals(self.e_123 * self.e_23, -self.e_1) + self.assertEquals(self.e_123 * self.e_021, -self.e_03) + self.assertEquals(self.e_123 * self.e_013, -self.e_02) + self.assertEquals(self.e_123 * self.e_032, -self.e_01) + self.assertEquals(self.e_123 * self.e_123, -S.One) + self.assertEquals(self.e_123 * self.e_0123, self.e_0) + + # e0123 + self.assertEquals(self.e_0123 * self.e_0, S.Zero) + self.assertEquals(self.e_0123 * self.e_1, -self.e_032) + self.assertEquals(self.e_0123 * self.e_2, -self.e_013) + self.assertEquals(self.e_0123 * self.e_3, -self.e_021) + self.assertEquals(self.e_0123 * self.e_01, S.Zero) + self.assertEquals(self.e_0123 * self.e_02, S.Zero) + self.assertEquals(self.e_0123 * self.e_03, S.Zero) + self.assertEquals(self.e_0123 * self.e_12, -self.e_03) + self.assertEquals(self.e_0123 * self.e_31, -self.e_02) + self.assertEquals(self.e_0123 * self.e_23, -self.e_01) + self.assertEquals(self.e_0123 * self.e_021, S.Zero) + self.assertEquals(self.e_0123 * self.e_013, S.Zero) + self.assertEquals(self.e_0123 * self.e_032, S.Zero) + self.assertEquals(self.e_0123 * self.e_123, -self.e_0) + self.assertEquals(self.e_0123 * self.e_0123, S.Zero) + def test_J(self): """ Check we can join and meet using J and Jinv. @@ -119,7 +421,7 @@ def test_J(self): PGA = self.PGA for k in range(PGA.n + 1): X = PGA.mv('x', k, 'grade') - self.assertEquals(X, Jinv(J(X))) + self.assertEquals(X, Jinv(J(X)), "grade is {}".format(k)) X = PGA.mv('x', 'mv') self.assertEquals(X, Jinv(J(X))) @@ -352,11 +654,11 @@ def test_metric_angle_between_lines(self): @staticmethod def rotor_cs(alpha, l): - return cos(alpha / 2) + sin(alpha / 2) * l + return cos(-alpha / 2) + sin(-alpha / 2) * l # TODO: this feels weird... @staticmethod def rotor_exp(alpha, l): - return (alpha / 2 * l).exp() + return (-alpha / 2 * l).exp() # TODO: this feels weird... def test_motors_rotator(self): """ @@ -515,4 +817,4 @@ def test_motors_translator(self): d = Symbol('d') T = self.translator(d, l) - self.assertProjEquals(T * Px * T.rev(), self.point(x0 - d, y0, z0)) # TODO : like ganja.js but weird... + self.assertProjEquals(T * Px * T.rev(), self.point(x0 + d, y0, z0)) # TODO : like ganja.js but weird... From 2fb10bb93c32035835db69f302a8f5718090c593 Mon Sep 17 00:00:00 2001 From: Sylvain Meunier Date: Thu, 31 Oct 2019 21:38:58 +0100 Subject: [PATCH 65/78] implement custom basis (like enki) and add more tests --- galgebra/ga.py | 120 ++++++++++------- galgebra/metric.py | 3 +- galgebra/mv.py | 4 +- tests/test_pga.py | 318 +++++++++++++++++++++++++++++++++++++++++++-- 4 files changed, 385 insertions(+), 60 deletions(-) diff --git a/galgebra/ga.py b/galgebra/ga.py index 95ba1fc0..3ec4271b 100755 --- a/galgebra/ga.py +++ b/galgebra/ga.py @@ -316,7 +316,7 @@ def __init__(self, bases, **kwargs): metric.Metric.__init__(self, bases, **kwargs) self.par_coords = None - self.build_bases() + self.build_bases(kwargs.get('sign_and_indexes', None)) self.dot_mode = '|' self.basis_product_tables() @@ -528,47 +528,54 @@ def parametric(self, coords): def basis_vectors(self): return tuple(self.basis) - def build_cobases(self): + def build_cobases(self, coindexes=None): """ Cobases for building Poincare duality, this is useful for defining wedge and vee without using I nor any metric. """ - basis_indexes = tuple(self.n_range) - self.coindexes = [] - self.coindexes_lst = [] - for i in reversed(basis_indexes): - base_tuple = tuple(reversed(tuple(combinations(basis_indexes, i + 1)))) - self.coindexes.append(base_tuple) - self.coindexes_lst += list(base_tuple) - self.coindexes.append(()) - self.coindexes = tuple(self.coindexes) - - self.coblades_lst = [] - self.coblades_inv_lst = [] + # TODO: check this can be used with another GA than 3D PGA... + if coindexes is None: + basis_indexes = tuple(self.n_range) + self.coindexes = [] + self.coindexes_lst = [] + for i in reversed(basis_indexes): + base_tuple = tuple(reversed(tuple(combinations(basis_indexes, i + 1)))) + self.coindexes.append(base_tuple) + self.coindexes_lst += list(base_tuple) + self.coindexes.append(()) + self.coindexes = tuple(self.coindexes) + else: + self.coindexes = coindexes + self.coindexes_lst = [index for cograde_index in coindexes for index in cograde_index] def format_symbol_name(symbol_index): return (''.join([str(self.basis[i]) + '^' for i in symbol_index]))[:-1] - assert self.indexes[0] == () and len(self.coindexes[0]) == 1 - cobase_index = self.coindexes[0][0] - coblade = Symbol(format_symbol_name(cobase_index), commutative=False) - coblade_inv = S(1) - self.coblades_lst.append(coblade) - self.coblades_inv_lst.append(coblade_inv) + n = self.n - for grade_index, cograde_index in zip(self.indexes[1:], self.coindexes[1:]): - for base_index, cobase_index in zip(grade_index, cograde_index): - coblade_sign = 1 if Permutation(base_index + cobase_index).is_even else -1 + self.coblades_lst = [] + for cograde_index in self.coindexes: + k = len(cograde_index[0]) if len(cograde_index) > 0 else 0 + for cobase_index in cograde_index: + coblade_sign = -1 if k == n - 1 and k % 2 == 1 else 1 coblade = coblade_sign * Symbol(format_symbol_name(cobase_index), commutative=False) - coblade_inv = coblade_sign * Symbol(format_symbol_name(base_index), commutative=False) self.coblades_lst.append(coblade) - self.coblades_inv_lst.append(coblade_inv) + self.coblades_lst0 = self.coblades_lst + [S(1),] + self.coblades_inv_lst = [] + for grade_index in self.indexes: + k = len(grade_index[0]) if len(grade_index) > 0 else 0 + for base_index in grade_index: + coblade_inv_sign = -1 if k == 1 and k % 2 == 1 else 1 + coblade_inv = coblade_inv_sign * Symbol(format_symbol_name(base_index), commutative=False) + self.coblades_inv_lst.append(coblade_inv) self.coblades_inv_lst = list(reversed(self.coblades_inv_lst)) + self.coblades_inv_lst0 = self.coblades_inv_lst + [S(1),] - self.coblades_lst0 = self.coblades_lst + [S(1),] - self.coblades_inv_lst0 = [Symbol(format_symbol_name(self.indexes[-1][0]), commutative=False)] + self.coblades_inv_lst + self.mv_blades_lst0 = [] + for obj in self.blades_lst0: + self.mv_blades_lst0.append(self.mv(obj)) - def build_bases(self): + def build_bases(self, sign_and_indexes=None): """ The bases for the multivector (geometric) algebra are formed from all combinations of the bases of the vector space and the scalars. @@ -604,14 +611,18 @@ def build_bases(self): """ # index list for multivector bases and blades by grade - basis_indexes = tuple(self.n_range) - self.indexes = [()] - self.indexes_lst = [] - for i in basis_indexes: - base_tuple = tuple(combinations(basis_indexes, i + 1)) - self.indexes.append(base_tuple) - self.indexes_lst += list(base_tuple) - self.indexes = tuple(self.indexes) + if sign_and_indexes is None: + basis_indexes = tuple(self.n_range) + self.indexes = [()] + self.indexes_lst = [] + for i in basis_indexes: + base_tuple = tuple(combinations(basis_indexes, i + 1)) + self.indexes.append(base_tuple) + self.indexes_lst += list(base_tuple) + self.indexes = tuple(self.indexes) + else: + self.indexes = sign_and_indexes[1] + self.indexes_lst = [index for grade_index in self.indexes for index in grade_index] # list of non-commutative symbols for multivector bases and blades # by grade and as a flattened list @@ -634,9 +645,20 @@ def build_bases(self): self.blades_to_indexes = [] self.indexes_to_blades = [] - for (index, blade) in zip(self.indexes_lst, self.blades_lst): - self.blades_to_indexes.append((blade, index)) - self.indexes_to_blades.append((index, blade)) + if sign_and_indexes is None: + for (index, blade) in zip(self.indexes_lst, self.blades_lst): + self.blades_to_indexes.append((blade, (1, index))) + self.indexes_to_blades.append((index, blade)) + else: + basis_indexes = tuple(self.n_range) + default_indexes_lst = [] + for i in basis_indexes: + base_tuple = tuple(combinations(basis_indexes, i + 1)) + default_indexes_lst += list(base_tuple) + signs_lst = [sign for grade_sign in sign_and_indexes[0] for sign in grade_sign] + for (default_index, sign, blade) in zip(default_indexes_lst, signs_lst, self.blades_lst): + self.blades_to_indexes.append((blade, (sign, default_index))) + self.indexes_to_blades.append((default_index, sign * blade)) self.blades_to_indexes_dict = OrderedDict(self.blades_to_indexes) self.indexes_to_blades_dict = OrderedDict(self.indexes_to_blades) @@ -778,11 +800,11 @@ def geometric_product_basis_blades(self, blade12): # geometric (*) product for orthogonal basis if self.is_ortho: (blade1, blade2) = blade12 - index1 = self.blades_to_indexes_dict[blade1] - index2 = self.blades_to_indexes_dict[blade2] + sign1, index1 = self.blades_to_indexes_dict[blade1] + sign2, index2 = self.blades_to_indexes_dict[blade2] blade_index = list(index1 + index2) repeats = [] - sgn = 1 + sgn = sign1 * sign2 for i in range(1, len(blade_index)): save = blade_index[i] j = i @@ -921,15 +943,15 @@ def wedge_product_basis_blades(self, blade12): # blade12 = blade1*blade2 # outer (^) product of basis blades # this method works for both orthogonal and non-orthogonal basis (blade1, blade2) = blade12 - index1 = self.blades_to_indexes_dict[blade1] - index2 = self.blades_to_indexes_dict[blade2] + sign1, index1 = self.blades_to_indexes_dict[blade1] + sign2, index2 = self.blades_to_indexes_dict[blade2] index12 = list(index1 + index2) if len(index12) > self.n: return 0 (sgn, wedge12) = Ga.blade_reduce(index12) if sgn != 0: - return(sgn * self.indexes_to_blades_dict[tuple(wedge12)]) + return(sgn * sign1 * sign2 * self.indexes_to_blades_dict[tuple(wedge12)]) else: return 0 @@ -939,8 +961,8 @@ def dot_product_basis_blades(self, blade12): # dot (|), left (<), and right (>) products # dot product for orthogonal basis (blade1, blade2) = blade12 - index1 = self.blades_to_indexes_dict[blade1] - index2 = self.blades_to_indexes_dict[blade2] + sign1, index1 = self.blades_to_indexes_dict[blade1] + sign2, index2 = self.blades_to_indexes_dict[blade2] index = list(index1 + index2) grade1 = len(index1) grade2 = len(index2) @@ -956,7 +978,7 @@ def dot_product_basis_blades(self, blade12): if grade < 0: return 0 n = len(index) - sgn = 1 + sgn = sign1 * sign2 result = 1 ordered = False while n > grade: @@ -1076,7 +1098,7 @@ def base_blade_conversions(self): # expand blade basis in terms of base basis for blade in self.blades_lst: - index = self.blades_to_indexes_dict[blade] + sign, index = self.blades_to_indexes_dict[blade] grade = len(index) if grade == 1: blade_expansion.append(blade) diff --git a/galgebra/metric.py b/galgebra/metric.py index a42387b5..29434c5d 100755 --- a/galgebra/metric.py +++ b/galgebra/metric.py @@ -256,7 +256,8 @@ class Metric(object): 'norm': (False, 'True to normalize basis vectors'), 'debug': (False, 'True to print out debugging information'), 'gsym': (None, 'String s to use "det("+s+")" function in reciprocal basis'), - 'sig': ('e', 'Signature of metric, default is (n,0) a Euclidean metric')} + 'sig': ('e', 'Signature of metric, default is (n,0) a Euclidean metric'), + 'sign_and_indexes': (None, 'bases indexes')} @staticmethod def dot_orthogonal(V1, V2, g=None): diff --git a/galgebra/mv.py b/galgebra/mv.py index a5a46aa9..9b6fe99c 100755 --- a/galgebra/mv.py +++ b/galgebra/mv.py @@ -994,14 +994,14 @@ def get_coefs(self, grade): def J(self): # TODO: If we pick our basis smartly, we can probably use J in place of Jinv... obj = 0 - for coef, coblade in zip(self.blade_coefs(), self.Ga.coblades_lst0): + for coef, coblade in zip(self.blade_coefs(self.Ga.mv_blades_lst0), self.Ga.coblades_lst0): obj += coef * coblade return Mv(obj, ga=self.Ga) def Jinv(self): # TODO: If we pick our basis smartly, we can probably use J in place of Jinv... obj = 0 - for coef, coblade_inv in zip(self.blade_coefs(), self.Ga.coblades_inv_lst0): + for coef, coblade_inv in zip(self.blade_coefs(self.Ga.mv_blades_lst0), self.Ga.coblades_inv_lst0): obj += coef * coblade_inv return Mv(obj, ga=self.Ga) diff --git a/tests/test_pga.py b/tests/test_pga.py index 1a0a852b..5039011b 100644 --- a/tests/test_pga.py +++ b/tests/test_pga.py @@ -1,6 +1,6 @@ import unittest -from sympy import acos, asin, pi, Rational, S, simplify, sqrt, Symbol, symbols +from sympy import acos, asin, cos, sin, pi, Rational, S, simplify, sqrt, Symbol, symbols from ga import Ga from mv import Mv, J, Jinv, com @@ -20,7 +20,7 @@ def assertEquals(self, first, second, msg=None): diff = simplify(first - second) - self.assertTrue(diff == 0, "\n%s\n==\n%s\n%s" % (first, second, diff)) + self.assertTrue(diff == 0, "\n%s\n%s\n==\n%s\n%s" % (msg, first, second, diff)) def assertProjEquals(self, X, Y): """ @@ -43,10 +43,36 @@ def setUp(self): """ Setup 3D Projective Geometric Algebra aka PGA. """ - PGA, e_0, e_1, e_2, e_3 = Ga.build('e*0|1|2|3', g=[0, 1, 1, 1]) + indexes = ( + (), + ((0,), (1,), (2,), (3,)), + ((0, 1), (0, 2), (0, 3), (1, 2), (3, 1), (2, 3)), + ((0, 2, 1), (0, 1, 3), (0, 3, 2), (1, 2, 3)), + ((0, 1, 2, 3),) + ) + + # Internal products use lexical ordering for computing expressions + signs = ( + (), + (1, 1, 1, 1), + (1, 1, 1, 1, -1, 1), + (-1, 1, -1, 1), + (1,) + ) + + # TODO: build this from indexes... + coindexes = ( + ((0, 1, 2, 3),), + ((1, 2, 3), (0, 3, 2), (0, 1, 3), (0, 2, 1)), + ((2, 3), (3, 1), (1, 2), (0, 3), (0, 2), (0, 1)), + ((3,), (2,), (1,), (0,)), + (), + ) + + PGA, e_0, e_1, e_2, e_3 = Ga.build('e*0|1|2|3', g=[0, 1, 1, 1], sign_and_indexes=(signs, indexes)) # TODO: move this somewhere useful... - PGA.build_cobases() + PGA.build_cobases(coindexes=coindexes) self.PGA = PGA self.e_0 = e_0 @@ -112,6 +138,282 @@ def ideal_norm(self, X): raise ValueError("X k-vector is null") return sqrt(squared_norm) + def test_multiplication_table(self): + """ + Reproduce multiplication table. + """ + # 1 + self.assertEquals(S.One * self.e_0, self.e_0) + self.assertEquals(S.One * self.e_1, self.e_1) + self.assertEquals(S.One * self.e_2, self.e_2) + self.assertEquals(S.One * self.e_3, self.e_3) + self.assertEquals(S.One * self.e_01, self.e_01) + self.assertEquals(S.One * self.e_02, self.e_02) + self.assertEquals(S.One * self.e_03, self.e_03) + self.assertEquals(S.One * self.e_12, self.e_12) + self.assertEquals(S.One * self.e_31, self.e_31) + self.assertEquals(S.One * self.e_23, self.e_23) + self.assertEquals(S.One * self.e_021, self.e_021) + self.assertEquals(S.One * self.e_013, self.e_013) + self.assertEquals(S.One * self.e_032, self.e_032) + self.assertEquals(S.One * self.e_123, self.e_123) + self.assertEquals(S.One * self.e_0123, self.e_0123) + + # e0 + self.assertEquals(self.e_0 * self.e_0, S.Zero) + self.assertEquals(self.e_0 * self.e_1, self.e_01) + self.assertEquals(self.e_0 * self.e_2, self.e_02) + self.assertEquals(self.e_0 * self.e_3, self.e_03) + self.assertEquals(self.e_0 * self.e_01, S.Zero) + self.assertEquals(self.e_0 * self.e_02, S.Zero) + self.assertEquals(self.e_0 * self.e_03, S.Zero) + self.assertEquals(self.e_0 * self.e_12, -self.e_021) + self.assertEquals(self.e_0 * self.e_31, -self.e_013) + self.assertEquals(self.e_0 * self.e_23, -self.e_032) + self.assertEquals(self.e_0 * self.e_021, S.Zero) + self.assertEquals(self.e_0 * self.e_013, S.Zero) + self.assertEquals(self.e_0 * self.e_032, S.Zero) + self.assertEquals(self.e_0 * self.e_123, self.e_0123) + self.assertEquals(self.e_0 * self.e_0123, S.Zero) + + # e1 + self.assertEquals(self.e_1 * self.e_0, -self.e_01) + self.assertEquals(self.e_1 * self.e_1, S.One) + self.assertEquals(self.e_1 * self.e_2, self.e_12) + self.assertEquals(self.e_1 * self.e_3, -self.e_31) + self.assertEquals(self.e_1 * self.e_01, -self.e_0) + self.assertEquals(self.e_1 * self.e_02, self.e_021) + self.assertEquals(self.e_1 * self.e_03, -self.e_013) + self.assertEquals(self.e_1 * self.e_12, self.e_2) + self.assertEquals(self.e_1 * self.e_31, -self.e_3) + self.assertEquals(self.e_1 * self.e_23, self.e_123) + self.assertEquals(self.e_1 * self.e_021, self.e_02) + self.assertEquals(self.e_1 * self.e_013, -self.e_03) + self.assertEquals(self.e_1 * self.e_032, self.e_0123) + self.assertEquals(self.e_1 * self.e_123, self.e_23) + self.assertEquals(self.e_1 * self.e_0123, self.e_032) + + # e2 + self.assertEquals(self.e_2 * self.e_0, -self.e_02) + self.assertEquals(self.e_2 * self.e_1, -self.e_12) + self.assertEquals(self.e_2 * self.e_2, S.One) + self.assertEquals(self.e_2 * self.e_3, self.e_23) + self.assertEquals(self.e_2 * self.e_01, -self.e_021) + self.assertEquals(self.e_2 * self.e_02, -self.e_0) + self.assertEquals(self.e_2 * self.e_03, self.e_032) + self.assertEquals(self.e_2 * self.e_12, -self.e_1) + self.assertEquals(self.e_2 * self.e_31, self.e_123) + self.assertEquals(self.e_2 * self.e_23, self.e_3) + self.assertEquals(self.e_2 * self.e_021, -self.e_01) + self.assertEquals(self.e_2 * self.e_013, self.e_0123) + self.assertEquals(self.e_2 * self.e_032, self.e_03) + self.assertEquals(self.e_2 * self.e_123, self.e_31) + self.assertEquals(self.e_2 * self.e_0123, self.e_013) + + # e3 + self.assertEquals(self.e_3 * self.e_0, -self.e_03) + self.assertEquals(self.e_3 * self.e_1, self.e_31) + self.assertEquals(self.e_3 * self.e_2, -self.e_23) + self.assertEquals(self.e_3 * self.e_3, S.One) + self.assertEquals(self.e_3 * self.e_01, self.e_013) + self.assertEquals(self.e_3 * self.e_02, -self.e_032) + self.assertEquals(self.e_3 * self.e_03, -self.e_0) + self.assertEquals(self.e_3 * self.e_12, self.e_123) + self.assertEquals(self.e_3 * self.e_31, self.e_1) + self.assertEquals(self.e_3 * self.e_23, -self.e_2) + self.assertEquals(self.e_3 * self.e_021, self.e_0123) + self.assertEquals(self.e_3 * self.e_013, self.e_01) + self.assertEquals(self.e_3 * self.e_032, -self.e_02) + self.assertEquals(self.e_3 * self.e_123, self.e_12) + self.assertEquals(self.e_3 * self.e_0123, self.e_021) + + # e01 + self.assertEquals(self.e_01 * self.e_0, S.Zero) + self.assertEquals(self.e_01 * self.e_1, self.e_0) + self.assertEquals(self.e_01 * self.e_2, -self.e_021) + self.assertEquals(self.e_01 * self.e_3, self.e_013) + self.assertEquals(self.e_01 * self.e_01, S.Zero) + self.assertEquals(self.e_01 * self.e_02, S.Zero) + self.assertEquals(self.e_01 * self.e_03, S.Zero) + self.assertEquals(self.e_01 * self.e_12, self.e_02) + self.assertEquals(self.e_01 * self.e_31, -self.e_03) + self.assertEquals(self.e_01 * self.e_23, self.e_0123) + self.assertEquals(self.e_01 * self.e_021, S.Zero) + self.assertEquals(self.e_01 * self.e_013, S.Zero) + self.assertEquals(self.e_01 * self.e_032, S.Zero) + self.assertEquals(self.e_01 * self.e_123, -self.e_032) + self.assertEquals(self.e_01 * self.e_0123, S.Zero) + + # e02 + self.assertEquals(self.e_02 * self.e_0, S.Zero) + self.assertEquals(self.e_02 * self.e_1, self.e_021) + self.assertEquals(self.e_02 * self.e_2, self.e_0) + self.assertEquals(self.e_02 * self.e_3, -self.e_032) + self.assertEquals(self.e_02 * self.e_01, S.Zero) + self.assertEquals(self.e_02 * self.e_02, S.Zero) + self.assertEquals(self.e_02 * self.e_03, S.Zero) + self.assertEquals(self.e_02 * self.e_12, -self.e_01) + self.assertEquals(self.e_02 * self.e_31, self.e_0123) + self.assertEquals(self.e_02 * self.e_23, self.e_03) + self.assertEquals(self.e_02 * self.e_021, S.Zero) + self.assertEquals(self.e_02 * self.e_013, S.Zero) + self.assertEquals(self.e_02 * self.e_032, S.Zero) + self.assertEquals(self.e_02 * self.e_123, -self.e_013) + self.assertEquals(self.e_02 * self.e_0123, S.Zero) + + # e03 + self.assertEquals(self.e_03 * self.e_0, S.Zero) + self.assertEquals(self.e_03 * self.e_1, -self.e_013) + self.assertEquals(self.e_03 * self.e_2, self.e_032) + self.assertEquals(self.e_03 * self.e_3, self.e_0) + self.assertEquals(self.e_03 * self.e_01, S.Zero) + self.assertEquals(self.e_03 * self.e_02, S.Zero) + self.assertEquals(self.e_03 * self.e_03, S.Zero) + self.assertEquals(self.e_03 * self.e_12, self.e_0123) + self.assertEquals(self.e_03 * self.e_31, self.e_01) + self.assertEquals(self.e_03 * self.e_23, -self.e_02) + self.assertEquals(self.e_03 * self.e_021, S.Zero) + self.assertEquals(self.e_03 * self.e_013, S.Zero) + self.assertEquals(self.e_03 * self.e_032, S.Zero) + self.assertEquals(self.e_03 * self.e_123, -self.e_021) + self.assertEquals(self.e_03 * self.e_0123, S.Zero) + + # e12 + self.assertEquals(self.e_12 * self.e_0, -self.e_021) + self.assertEquals(self.e_12 * self.e_1, -self.e_2) + self.assertEquals(self.e_12 * self.e_2, self.e_1) + self.assertEquals(self.e_12 * self.e_3, self.e_123) + self.assertEquals(self.e_12 * self.e_01, -self.e_02) + self.assertEquals(self.e_12 * self.e_02, self.e_01) + self.assertEquals(self.e_12 * self.e_03, self.e_0123) + self.assertEquals(self.e_12 * self.e_12, -S.One) + self.assertEquals(self.e_12 * self.e_31, self.e_23) + self.assertEquals(self.e_12 * self.e_23, -self.e_31) + self.assertEquals(self.e_12 * self.e_021, self.e_0) + self.assertEquals(self.e_12 * self.e_013, self.e_032) + self.assertEquals(self.e_12 * self.e_032, -self.e_013) + self.assertEquals(self.e_12 * self.e_123, -self.e_3) + self.assertEquals(self.e_12 * self.e_0123, -self.e_03) + + # e31 + self.assertEquals(self.e_31 * self.e_0, -self.e_013) + self.assertEquals(self.e_31 * self.e_1, self.e_3) + self.assertEquals(self.e_31 * self.e_2, self.e_123) + self.assertEquals(self.e_31 * self.e_3, -self.e_1) + self.assertEquals(self.e_31 * self.e_01, self.e_03) + self.assertEquals(self.e_31 * self.e_02, self.e_0123) + self.assertEquals(self.e_31 * self.e_03, -self.e_01) + self.assertEquals(self.e_31 * self.e_12, -self.e_23) + self.assertEquals(self.e_31 * self.e_31, -S.One) + self.assertEquals(self.e_31 * self.e_23, self.e_12) + self.assertEquals(self.e_31 * self.e_021, -self.e_032) + self.assertEquals(self.e_31 * self.e_013, self.e_0) + self.assertEquals(self.e_31 * self.e_032, self.e_021) + self.assertEquals(self.e_31 * self.e_123, -self.e_2) + self.assertEquals(self.e_31 * self.e_0123, -self.e_02) + + # e23 + self.assertEquals(self.e_23 * self.e_0, -self.e_032) + self.assertEquals(self.e_23 * self.e_1, self.e_123) + self.assertEquals(self.e_23 * self.e_2, -self.e_3) + self.assertEquals(self.e_23 * self.e_3, self.e_2) + self.assertEquals(self.e_23 * self.e_01, self.e_0123) + self.assertEquals(self.e_23 * self.e_02, -self.e_03) + self.assertEquals(self.e_23 * self.e_03, self.e_02) + self.assertEquals(self.e_23 * self.e_12, self.e_31) + self.assertEquals(self.e_23 * self.e_31, -self.e_12) + self.assertEquals(self.e_23 * self.e_23, -S.One) + self.assertEquals(self.e_23 * self.e_021, self.e_013) + self.assertEquals(self.e_23 * self.e_013, -self.e_021) + self.assertEquals(self.e_23 * self.e_032, self.e_0) + self.assertEquals(self.e_23 * self.e_123, -self.e_1) + self.assertEquals(self.e_23 * self.e_0123, -self.e_01) + + # e021 + self.assertEquals(self.e_021 * self.e_0, S.Zero) + self.assertEquals(self.e_021 * self.e_1, self.e_02) + self.assertEquals(self.e_021 * self.e_2, -self.e_01) + self.assertEquals(self.e_021 * self.e_3, -self.e_0123) + self.assertEquals(self.e_021 * self.e_01, S.Zero) + self.assertEquals(self.e_021 * self.e_02, S.Zero) + self.assertEquals(self.e_021 * self.e_03, S.Zero) + self.assertEquals(self.e_021 * self.e_12, self.e_0) + self.assertEquals(self.e_021 * self.e_31, self.e_032) + self.assertEquals(self.e_021 * self.e_23, -self.e_013) + self.assertEquals(self.e_021 * self.e_021, S.Zero) + self.assertEquals(self.e_021 * self.e_013, S.Zero) + self.assertEquals(self.e_021 * self.e_032, S.Zero) + self.assertEquals(self.e_021 * self.e_123, self.e_03) + self.assertEquals(self.e_021 * self.e_0123, S.Zero) + + # e013 + self.assertEquals(self.e_013 * self.e_0, S.Zero) + self.assertEquals(self.e_013 * self.e_1, -self.e_03) + self.assertEquals(self.e_013 * self.e_2, -self.e_0123) + self.assertEquals(self.e_013 * self.e_3, self.e_01) + self.assertEquals(self.e_013 * self.e_01, S.Zero) + self.assertEquals(self.e_013 * self.e_02, S.Zero) + self.assertEquals(self.e_013 * self.e_03, S.Zero) + self.assertEquals(self.e_013 * self.e_12, -self.e_032) + self.assertEquals(self.e_013 * self.e_31, self.e_0) + self.assertEquals(self.e_013 * self.e_23, self.e_021) + self.assertEquals(self.e_013 * self.e_021, S.Zero) + self.assertEquals(self.e_013 * self.e_013, S.Zero) + self.assertEquals(self.e_013 * self.e_032, S.Zero) + self.assertEquals(self.e_013 * self.e_123, self.e_02) + self.assertEquals(self.e_013 * self.e_0123, S.Zero) + + # e032 + self.assertEquals(self.e_032 * self.e_0, S.Zero) + self.assertEquals(self.e_032 * self.e_1, -self.e_0123) + self.assertEquals(self.e_032 * self.e_2, self.e_03) + self.assertEquals(self.e_032 * self.e_3, -self.e_02) + self.assertEquals(self.e_032 * self.e_01, S.Zero) + self.assertEquals(self.e_032 * self.e_02, S.Zero) + self.assertEquals(self.e_032 * self.e_03, S.Zero) + self.assertEquals(self.e_032 * self.e_12, self.e_013) + self.assertEquals(self.e_032 * self.e_31, -self.e_021) + self.assertEquals(self.e_032 * self.e_23, self.e_0) + self.assertEquals(self.e_032 * self.e_021, S.Zero) + self.assertEquals(self.e_032 * self.e_013, S.Zero) + self.assertEquals(self.e_032 * self.e_032, S.Zero) + self.assertEquals(self.e_032 * self.e_123, self.e_01) + self.assertEquals(self.e_032 * self.e_0123, S.Zero) + + # e123 + self.assertEquals(self.e_123 * self.e_0, -self.e_0123) + self.assertEquals(self.e_123 * self.e_1, self.e_23) + self.assertEquals(self.e_123 * self.e_2, self.e_31) + self.assertEquals(self.e_123 * self.e_3, self.e_12) + self.assertEquals(self.e_123 * self.e_01, self.e_032) + self.assertEquals(self.e_123 * self.e_02, self.e_013) + self.assertEquals(self.e_123 * self.e_03, self.e_021) + self.assertEquals(self.e_123 * self.e_12, -self.e_3) + self.assertEquals(self.e_123 * self.e_31, -self.e_2) + self.assertEquals(self.e_123 * self.e_23, -self.e_1) + self.assertEquals(self.e_123 * self.e_021, -self.e_03) + self.assertEquals(self.e_123 * self.e_013, -self.e_02) + self.assertEquals(self.e_123 * self.e_032, -self.e_01) + self.assertEquals(self.e_123 * self.e_123, -S.One) + self.assertEquals(self.e_123 * self.e_0123, self.e_0) + + # e0123 + self.assertEquals(self.e_0123 * self.e_0, S.Zero) + self.assertEquals(self.e_0123 * self.e_1, -self.e_032) + self.assertEquals(self.e_0123 * self.e_2, -self.e_013) + self.assertEquals(self.e_0123 * self.e_3, -self.e_021) + self.assertEquals(self.e_0123 * self.e_01, S.Zero) + self.assertEquals(self.e_0123 * self.e_02, S.Zero) + self.assertEquals(self.e_0123 * self.e_03, S.Zero) + self.assertEquals(self.e_0123 * self.e_12, -self.e_03) + self.assertEquals(self.e_0123 * self.e_31, -self.e_02) + self.assertEquals(self.e_0123 * self.e_23, -self.e_01) + self.assertEquals(self.e_0123 * self.e_021, S.Zero) + self.assertEquals(self.e_0123 * self.e_013, S.Zero) + self.assertEquals(self.e_0123 * self.e_032, S.Zero) + self.assertEquals(self.e_0123 * self.e_123, -self.e_0) + self.assertEquals(self.e_0123 * self.e_0123, S.Zero) + def test_J(self): """ Check we can join and meet using J and Jinv. @@ -119,7 +421,7 @@ def test_J(self): PGA = self.PGA for k in range(PGA.n + 1): X = PGA.mv('x', k, 'grade') - self.assertEquals(X, Jinv(J(X))) + self.assertEquals(X, Jinv(J(X)), "grade is {}".format(k)) X = PGA.mv('x', 'mv') self.assertEquals(X, Jinv(J(X))) @@ -352,11 +654,11 @@ def test_metric_angle_between_lines(self): @staticmethod def rotor_cs(alpha, l): - return cos(alpha / 2) + sin(alpha / 2) * l + return cos(-alpha / 2) + sin(-alpha / 2) * l # TODO: this feels weird... @staticmethod def rotor_exp(alpha, l): - return (alpha / 2 * l).exp() + return (-alpha / 2 * l).exp() # TODO: this feels weird... def test_motors_rotator(self): """ @@ -515,4 +817,4 @@ def test_motors_translator(self): d = Symbol('d') T = self.translator(d, l) - self.assertProjEquals(T * Px * T.rev(), self.point(x0 - d, y0, z0)) # TODO : like ganja.js but weird... + self.assertProjEquals(T * Px * T.rev(), self.point(x0 + d, y0, z0)) # TODO : like ganja.js but weird... From d59b69b3b8c9de0ccebe914a36a123dad99993c9 Mon Sep 17 00:00:00 2001 From: Sylvain Meunier Date: Fri, 1 Nov 2019 22:19:00 +0100 Subject: [PATCH 66/78] leo dorst book chapter 8 exercises (in progress) --- tests/test_chapter8.py | 56 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 56 insertions(+) create mode 100644 tests/test_chapter8.py diff --git a/tests/test_chapter8.py b/tests/test_chapter8.py new file mode 100644 index 00000000..a7e882cb --- /dev/null +++ b/tests/test_chapter8.py @@ -0,0 +1,56 @@ +import unittest + +from sympy import simplify, Symbol, S + +from ga import Ga +from mv import Mv, com + + +class TestChapter8(unittest.TestCase): + + def assertEquals(self, first, second, msg=None): + """ + Compare two expressions are equals. + """ + + if isinstance(first, Mv): + first = first.obj + + if isinstance(second, Mv): + second = second.obj + + diff = simplify(first - second) + + self.assertTrue(diff == 0, "\n%s\n==\n%s\n%s" % (first, second, diff)) + + def test8_2_1(self): + """ + The commutator product. + """ + GA = Ga("e*1|2|3", g=[1, 1, 1]) + + A = GA.mv('A', 'mv') + B = GA.mv('B', 'mv') + C = GA.mv('C', 'mv') + + self.assertEquals(com(com(A, B), C) - com(A, com(B, C)), com(B, com(C, A))) + self.assertEquals(com(com(A, B), C) + com(com(C, A), B) + com(com(B, C), A), S.Zero) + + B = GA.mv('B', 2, 'blade') + X = GA.mv('A', 'mv') + self.assertEquals(B.pure_grade(), 2) + self.assertEquals(com(X, B).pure_grade(), -2) # Not pure + + E = com(X, B).rev() + self.assertEquals(E, (B.rev() * X.rev() - X.rev() * B.rev()) / 2) + self.assertEquals(E, (X.rev() * B - B * X.rev()) / 2) + self.assertEquals(E, com(X.rev(), B)) + + alpha = GA.mv('alpha', 'scalar') + self.assertEquals(alpha * X, alpha ^ X) + + a = GA.mv('a', 'vector') + self.assertEquals(a * X, (a < X) + (a ^ X)) + + A = GA.mv('A', 2, 'grade') + self.assertEquals(A * X, (A < X) + com(A, X) + (A ^ X)) From b07e3ffe0746a2e338e64ca5c998a3d16d509a89 Mon Sep 17 00:00:00 2001 From: Sylvain Meunier Date: Fri, 22 Nov 2019 22:10:06 +0100 Subject: [PATCH 67/78] Quick and easy fixes after migrating to python 3 --- galgebra/generator.py | 6 +++--- galgebra/mv.py | 1 - tests/test_chapter11.py | 5 +++-- tests/test_chapter13.py | 4 ++-- tests/test_chapter2.py | 11 ++++++----- tests/test_chapter3.py | 6 +++--- tests/test_chapter6.py | 7 +++---- tests/test_chapter7.py | 4 ++-- tests/test_chapter8.py | 4 ++-- tests/test_generator.py | 6 +++--- tests/test_pga.py | 4 ++-- 11 files changed, 29 insertions(+), 29 deletions(-) diff --git a/galgebra/generator.py b/galgebra/generator.py index 6e7afafd..85519006 100644 --- a/galgebra/generator.py +++ b/galgebra/generator.py @@ -2,8 +2,8 @@ from sympy import Symbol -from ga import Ga -from mv import J, Jinv +from .ga import Ga +from .mv import J, Jinv def create_multivector(GA, name): @@ -119,4 +119,4 @@ def expand(GA, flat_mv): if __name__ == "__main__": GA = Ga('e*1|2|3', g=[1, 1, 1]) - print format_geometric_algebra(GA) + print(format_geometric_algebra(GA)) diff --git a/galgebra/mv.py b/galgebra/mv.py index cb2eeecd..71d0cc3e 100755 --- a/galgebra/mv.py +++ b/galgebra/mv.py @@ -4,7 +4,6 @@ import copy import numbers import operator -from compiler.ast import flatten from operator import itemgetter, mul, add from itertools import combinations from sympy import Symbol, Function, S, expand, Add, Mul, Pow, Basic, \ diff --git a/tests/test_chapter11.py b/tests/test_chapter11.py index da945a54..aaf4873d 100644 --- a/tests/test_chapter11.py +++ b/tests/test_chapter11.py @@ -1,8 +1,9 @@ import unittest +from functools import reduce from sympy import simplify, sqrt, Rational, Symbol -from ga import Ga -from mv import Mv +from galgebra.ga import Ga +from galgebra.mv import Mv class TestChapter11(unittest.TestCase): diff --git a/tests/test_chapter13.py b/tests/test_chapter13.py index 629a6bf3..d0ff85fd 100644 --- a/tests/test_chapter13.py +++ b/tests/test_chapter13.py @@ -2,8 +2,8 @@ from sympy import simplify, Symbol, S -from ga import Ga -from mv import Mv +from galgebra.ga import Ga +from galgebra.mv import Mv class TestChapter13(unittest.TestCase): diff --git a/tests/test_chapter2.py b/tests/test_chapter2.py index e083d721..10546c4e 100755 --- a/tests/test_chapter2.py +++ b/tests/test_chapter2.py @@ -1,9 +1,10 @@ import unittest +from functools import reduce from itertools import product from sympy import Symbol, Matrix, solve, solve_poly_system, cos, sin -from ga import Ga -from mv import Mv +from galgebra.ga import Ga +from galgebra.mv import Mv class TestChapter2(unittest.TestCase): @@ -69,7 +70,7 @@ def test2_9_1(self): self.assertEquals(Ak.pure_grade(), k) grades = GA.grade_decomposition(Ak) self.assertEquals(len(grades), 1) - self.assertEquals(grades.keys()[0], k) + self.assertEquals(list(grades.keys())[0], k) # Check for k and l in [0, R.n] for k, l in product(range(GA.n + 1), range(GA.n + 1)): @@ -79,7 +80,7 @@ def test2_9_1(self): self.assertEquals(C.pure_grade(), 0 if C == 0 else k + l) grades = GA.grade_decomposition(C) self.assertEquals(len(grades), 1) - self.assertEquals(grades.keys()[0], 0 if C == 0 else k + l) + self.assertEquals(list(grades.keys())[0], 0 if C == 0 else k + l) def test2_9_5(self): @@ -153,7 +154,7 @@ def test2_12_1_4(self): M = (x ^ (e_1 + e_2)) - (e_2 ^ (e_1 + e_2)) # Solve the linear system - R = solve([L, M], a, b) + R = solve([L, M], a, b) # TODO: fix this... # Replace symbols x = x.subs(R) diff --git a/tests/test_chapter3.py b/tests/test_chapter3.py index a358614f..ca00ab08 100644 --- a/tests/test_chapter3.py +++ b/tests/test_chapter3.py @@ -2,8 +2,8 @@ from itertools import product from sympy import simplify, Symbol, cos, sin, sqrt -from ga import Ga -from mv import Mv, cross +from galgebra.ga import Ga +from galgebra.mv import Mv, cross class TestChapter3(unittest.TestCase): @@ -425,7 +425,7 @@ def test3_10_2_2(self): self.assertEquals(((X ^ A) * B).scalar(), (X * (A < B)).scalar()) self.assertEquals(((X ^ A) * B).scalar(), 0) # Which is false - e_1_grades = GA.grade_decomposition(e_1).keys() + e_1_grades = list(GA.grade_decomposition(e_1).keys()) self.assertEquals(len(e_1_grades), 1) self.assertEquals(e_1_grades[0], 1) diff --git a/tests/test_chapter6.py b/tests/test_chapter6.py index 37e9d7c9..3fa11975 100644 --- a/tests/test_chapter6.py +++ b/tests/test_chapter6.py @@ -2,9 +2,8 @@ from itertools import product from sympy import simplify, Symbol, solve_poly_system -from ga import Ga -from printer import GaLatexPrinter -from mv import Mv, com +from galgebra.ga import Ga +from galgebra.mv import Mv class TestChapter6(unittest.TestCase): @@ -151,7 +150,7 @@ def test6_6_2_3(self): z = GA.mv('z', 'vector') def hat(M): - M_grades = GA.grade_decomposition(M).keys() + M_grades = list(GA.grade_decomposition(M).keys()) self.assertEquals(len(M_grades), 1) return ((-1) ** M_grades[0]) * M diff --git a/tests/test_chapter7.py b/tests/test_chapter7.py index 9987afc5..25bd682a 100644 --- a/tests/test_chapter7.py +++ b/tests/test_chapter7.py @@ -1,8 +1,8 @@ import unittest from sympy import simplify, Symbol, pi, cos, sin, solve, sqrt, Rational, Mod -from ga import Ga -from mv import Mv +from galgebra.ga import Ga +from galgebra.mv import Mv class TestChapter7(unittest.TestCase): diff --git a/tests/test_chapter8.py b/tests/test_chapter8.py index a7e882cb..b1428e6c 100644 --- a/tests/test_chapter8.py +++ b/tests/test_chapter8.py @@ -2,8 +2,8 @@ from sympy import simplify, Symbol, S -from ga import Ga -from mv import Mv, com +from galgebra.ga import Ga +from galgebra.mv import Mv, com class TestChapter8(unittest.TestCase): diff --git a/tests/test_generator.py b/tests/test_generator.py index 8b056e05..f97f9722 100644 --- a/tests/test_generator.py +++ b/tests/test_generator.py @@ -3,9 +3,9 @@ from sympy import simplify -from ga import Ga -from mv import Mv, J, Jinv -from generator import format_geometric_algebra, flatten, expand +from galgebra.ga import Ga +from galgebra.mv import Mv, J, Jinv +from galgebra.generator import format_geometric_algebra, flatten, expand class TestGenerator(unittest.TestCase): diff --git a/tests/test_pga.py b/tests/test_pga.py index 5039011b..50769eee 100644 --- a/tests/test_pga.py +++ b/tests/test_pga.py @@ -2,8 +2,8 @@ from sympy import acos, asin, cos, sin, pi, Rational, S, simplify, sqrt, Symbol, symbols -from ga import Ga -from mv import Mv, J, Jinv, com +from galgebra.ga import Ga +from galgebra.mv import Mv, J, Jinv, com class TestPGA(unittest.TestCase): From 634a4b0c59468459fd2019d84a0b4f1990952b9b Mon Sep 17 00:00:00 2001 From: Sylvain Meunier Date: Fri, 22 Nov 2019 22:25:04 +0100 Subject: [PATCH 68/78] Quick and easy fixes after merging utensil commits --- galgebra/mv.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/galgebra/mv.py b/galgebra/mv.py index 019f14a9..ddc63bba 100755 --- a/galgebra/mv.py +++ b/galgebra/mv.py @@ -2582,7 +2582,7 @@ def printrref(matrix, vars="xyzuvwrs"): # Print rref of matrix with variables. print(result) def com(A, B): - return ga.Ga.com(A, B) + return A.Ga.com(A, B) def correlation(u, v, dec=3): # Compute the correlation coefficient of vectors u and v. rows, cols = u.shape From 06682cba09332101aa874e764fde26ce6d7e8e21 Mon Sep 17 00:00:00 2001 From: Sylvain Meunier Date: Sat, 23 Nov 2019 14:27:14 +0100 Subject: [PATCH 69/78] Fix test2_12_1_4 --- tests/test_chapter2.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/tests/test_chapter2.py b/tests/test_chapter2.py index 10546c4e..246f53c9 100755 --- a/tests/test_chapter2.py +++ b/tests/test_chapter2.py @@ -154,12 +154,13 @@ def test2_12_1_4(self): M = (x ^ (e_1 + e_2)) - (e_2 ^ (e_1 + e_2)) # Solve the linear system - R = solve([L, M], a, b) # TODO: fix this... + R = solve([L.obj, M.obj], a, b, dict=True) # TODO: fix this... + self.assertTrue(len(R), 1) # Replace symbols - x = x.subs(R) + x = x.subs(R[0]) - self.assertTrue(x == e_1 + 2*e_2) + self.assertEquals(x, e_1 + 2 * e_2) def test2_12_1_5(self): From 3722052dfb8eaf4fb57be3cd79521cbb6a911c4c Mon Sep 17 00:00:00 2001 From: Sylvain Meunier Date: Mon, 25 Nov 2019 21:39:14 +0100 Subject: [PATCH 70/78] Refactor assertEquals, assertNotEquals and assertProjEquals and fix a few tests --- tests/test_chapter11.py | 38 +++----------------------- tests/test_chapter13.py | 23 +++------------- tests/test_chapter2.py | 5 ++-- tests/test_chapter3.py | 22 +++------------ tests/test_chapter6.py | 23 +++------------- tests/test_chapter7.py | 22 +++------------ tests/test_chapter8.py | 28 +++---------------- tests/test_generator.py | 23 +++------------- tests/test_pga.py | 56 +++++++++++--------------------------- tests/test_utils.py | 59 +++++++++++++++++++++++++++++++++++++++++ 10 files changed, 101 insertions(+), 198 deletions(-) create mode 100644 tests/test_utils.py diff --git a/tests/test_chapter11.py b/tests/test_chapter11.py index aaf4873d..1b0c1d83 100644 --- a/tests/test_chapter11.py +++ b/tests/test_chapter11.py @@ -1,42 +1,12 @@ -import unittest +from .test_utils import TestCase from functools import reduce -from sympy import simplify, sqrt, Rational, Symbol +from sympy import sqrt, Rational, Symbol from galgebra.ga import Ga from galgebra.mv import Mv -class TestChapter11(unittest.TestCase): - - def assertEquals(self, first, second, msg=None): - """ - Compare two expressions are equals. - """ - - if isinstance(first, Mv): - first = first.obj - - if isinstance(second, Mv): - second = second.obj - - diff = simplify(first - second) - - self.assertTrue(diff == 0, "\n%s\n==\n%s\n%s" % (first, second, diff)) - - def assertNotEquals(self, first, second, msg=None): - """ - Compare two expressions are equals. - """ - - if isinstance(first, Mv): - first = first.obj - - if isinstance(second, Mv): - second = second.obj - - diff = simplify(first - second) - - self.assertTrue(diff != 0, "\n%s\n!=\n%s\n%s" % (first, second, diff)) +class TestChapter11(TestCase): def test11_4(self): """ @@ -182,4 +152,4 @@ def test11_12_2_2(self): t_value = Rational(i, 10) x_value = x_t.subs({t: t_value}) self.assertEquals(x_value ^ L, 0) - self.assertEquals(t_x.subs(zip([x_1, x_2, x_3], x_value.blade_coefs([e_1, e_2, e_3]))), t_value) + self.assertEquals(t_x.subs(list(zip([x_1, x_2, x_3], x_value.blade_coefs([e_1, e_2, e_3])))), t_value) diff --git a/tests/test_chapter13.py b/tests/test_chapter13.py index d0ff85fd..e9cc320b 100644 --- a/tests/test_chapter13.py +++ b/tests/test_chapter13.py @@ -1,27 +1,10 @@ -import unittest - -from sympy import simplify, Symbol, S +from .test_utils import TestCase +from sympy import Symbol, S from galgebra.ga import Ga -from galgebra.mv import Mv - - -class TestChapter13(unittest.TestCase): - - def assertEquals(self, first, second, msg=None): - """ - Compare two expressions are equals. - """ - - if isinstance(first, Mv): - first = first.obj - - if isinstance(second, Mv): - second = second.obj - diff = simplify(first - second) - self.assertTrue(diff == 0, "\n%s\n==\n%s\n%s" % (first, second, diff)) +class TestChapter13(TestCase): def test_13_1_2(self): """ diff --git a/tests/test_chapter2.py b/tests/test_chapter2.py index 246f53c9..dd5fbc99 100755 --- a/tests/test_chapter2.py +++ b/tests/test_chapter2.py @@ -1,4 +1,4 @@ -import unittest +from .test_utils import TestCase from functools import reduce from itertools import product @@ -6,7 +6,8 @@ from galgebra.ga import Ga from galgebra.mv import Mv -class TestChapter2(unittest.TestCase): + +class TestChapter2(TestCase): def test2_3_2(self): """ diff --git a/tests/test_chapter3.py b/tests/test_chapter3.py index ca00ab08..80743548 100644 --- a/tests/test_chapter3.py +++ b/tests/test_chapter3.py @@ -1,26 +1,12 @@ -import unittest +from .test_utils import TestCase from itertools import product -from sympy import simplify, Symbol, cos, sin, sqrt +from sympy import Symbol, cos, sin, sqrt from galgebra.ga import Ga -from galgebra.mv import Mv, cross +from galgebra.mv import cross -class TestChapter3(unittest.TestCase): - - def assertEquals(self, first, second, msg=None): - """ - Compare two expressions are equals. - """ - - if isinstance(first, Mv): - first = first.obj - - if isinstance(second, Mv): - second = second.obj - - self.assertTrue(simplify(first - second) == 0, "%s == %s" % (first, second)) - +class TestChapter3(TestCase): def test_3_1_2(self): """ diff --git a/tests/test_chapter6.py b/tests/test_chapter6.py index 3fa11975..cf458d91 100644 --- a/tests/test_chapter6.py +++ b/tests/test_chapter6.py @@ -1,28 +1,11 @@ -import unittest +from .test_utils import TestCase from itertools import product -from sympy import simplify, Symbol, solve_poly_system +from sympy import Symbol, solve_poly_system from galgebra.ga import Ga -from galgebra.mv import Mv -class TestChapter6(unittest.TestCase): - - def assertEquals(self, first, second, msg=None): - """ - Compare two expressions are equals. - """ - - if isinstance(first, Mv): - first = first.obj - - if isinstance(second, Mv): - second = second.obj - - diff = simplify(first - second) - - self.assertTrue(diff == 0, "\n%s\n==\n%s\n%s" % (first, second, diff)) - +class TestChapter6(TestCase): def test6_1_4_1(self): """ diff --git a/tests/test_chapter7.py b/tests/test_chapter7.py index 25bd682a..33075d4c 100644 --- a/tests/test_chapter7.py +++ b/tests/test_chapter7.py @@ -1,26 +1,10 @@ -import unittest +from .test_utils import TestCase -from sympy import simplify, Symbol, pi, cos, sin, solve, sqrt, Rational, Mod +from sympy import Symbol, pi, cos, sin, solve, sqrt, Rational, Mod from galgebra.ga import Ga -from galgebra.mv import Mv -class TestChapter7(unittest.TestCase): - - def assertEquals(self, first, second, msg=None): - """ - Compare two expressions are equals. - """ - - if isinstance(first, Mv): - first = first.obj - - if isinstance(second, Mv): - second = second.obj - - diff = simplify(first - second) - - self.assertTrue(diff == 0, "\n%s\n==\n%s\n%s" % (first, second, diff)) +class TestChapter7(TestCase): def test7_9_1(self): """ diff --git a/tests/test_chapter8.py b/tests/test_chapter8.py index 591b3326..8f1d79ba 100644 --- a/tests/test_chapter8.py +++ b/tests/test_chapter8.py @@ -1,32 +1,10 @@ -import unittest - -from sympy import simplify, Symbol, S +from .test_utils import TestCase, com +from sympy import S from galgebra.ga import Ga -from galgebra.mv import Mv - - -def com(A, B): - """I like free functions...""" - return Ga.com(A, B) - - -class TestChapter8(unittest.TestCase): - - def assertEquals(self, first, second, msg=None): - """ - Compare two expressions are equals. - """ - - if isinstance(first, Mv): - first = first.obj - - if isinstance(second, Mv): - second = second.obj - diff = simplify(first - second) - self.assertTrue(diff == 0, "\n%s\n==\n%s\n%s" % (first, second, diff)) +class TestChapter8(TestCase): def test8_2_1(self): """ diff --git a/tests/test_generator.py b/tests/test_generator.py index f97f9722..dc29b3c9 100644 --- a/tests/test_generator.py +++ b/tests/test_generator.py @@ -1,29 +1,12 @@ -import unittest +from .test_utils import TestCase import importlib -from sympy import simplify - from galgebra.ga import Ga -from galgebra.mv import Mv, J, Jinv +from galgebra.mv import J, Jinv from galgebra.generator import format_geometric_algebra, flatten, expand -class TestGenerator(unittest.TestCase): - - def assertEquals(self, first, second, msg=None): - """ - Compare two expressions are equals. - """ - - if isinstance(first, Mv): - first = first.obj - - if isinstance(second, Mv): - second = second.obj - - diff = simplify(first - second) - - self.assertTrue(diff == 0, "\n%s\n==\n%s\n%s" % (first, second, diff)) +class TestGenerator(TestCase): def setUp(self): diff --git a/tests/test_pga.py b/tests/test_pga.py index 50769eee..2cf095e1 100644 --- a/tests/test_pga.py +++ b/tests/test_pga.py @@ -1,43 +1,11 @@ -import unittest - -from sympy import acos, asin, cos, sin, pi, Rational, S, simplify, sqrt, Symbol, symbols +from .test_utils import TestCase, com +from sympy import acos, asin, cos, sin, pi, Rational, S, sqrt, Symbol, symbols from galgebra.ga import Ga -from galgebra.mv import Mv, J, Jinv, com - - -class TestPGA(unittest.TestCase): - - def assertEquals(self, first, second, msg=None): - """ - Compare two expressions are equals. - """ - if isinstance(first, Mv): - first = first.obj +from galgebra.mv import J, Jinv - if isinstance(second, Mv): - second = second.obj - diff = simplify(first - second) - - self.assertTrue(diff == 0, "\n%s\n%s\n==\n%s\n%s" % (msg, first, second, diff)) - - def assertProjEquals(self, X, Y): - """ - Compare two points, two planes or two lines up to a scalar. - """ - assert isinstance(X, Mv) - assert isinstance(Y, Mv) - - X /= self.norm(X) - Y /= self.norm(Y) - - # We can't easily retrieve the sign, so we test both - diff = simplify(X.obj - Y.obj) - if diff != S.Zero: - diff = simplify(X.obj + Y.obj) - - self.assertTrue(diff == S.Zero, "\n%s\n==\n%s" % (X, Y)) +class TestPGA(TestCase): def setUp(self): """ @@ -421,7 +389,7 @@ def test_J(self): PGA = self.PGA for k in range(PGA.n + 1): X = PGA.mv('x', k, 'grade') - self.assertEquals(X, Jinv(J(X)), "grade is {}".format(k)) + self.assertEquals(X, Jinv(J(X))) X = PGA.mv('x', 'mv') self.assertEquals(X, Jinv(J(X))) @@ -632,8 +600,13 @@ def test_metric_common_normal_line(self): l0 /= self.norm(l0) l1 /= self.norm(l1) - self.assertEquals(acos(l0 | ln), S.Half * pi) - self.assertEquals(acos(l1 | ln), S.Half * pi) + dot0 = l0 | ln + dot1 = l1 | ln + self.assertTrue(dot0.is_scalar()) + self.assertTrue(dot1.is_scalar()) + + self.assertEquals(acos(dot0.scalar()), S.Half * pi) + self.assertEquals(acos(dot1.scalar()), S.Half * pi) def test_metric_angle_between_lines(self): """ @@ -650,7 +623,10 @@ def test_metric_angle_between_lines(self): l0 /= self.norm(l0) l1 /= self.norm(l1) - self.assertEquals(acos(l0 | l1), pi / 2) + dot = l0 | l1 + self.assertTrue(dot.is_scalar()) + + self.assertEquals(acos(dot.scalar()), pi / 2) @staticmethod def rotor_cs(alpha, l): diff --git a/tests/test_utils.py b/tests/test_utils.py new file mode 100644 index 00000000..92aff5f2 --- /dev/null +++ b/tests/test_utils.py @@ -0,0 +1,59 @@ +import unittest + +from sympy import S, simplify +from galgebra.ga import Ga +from galgebra.mv import Mv + + +def com(A, B): + """I like free functions...""" + return Ga.com(A, B) + + +class TestCase(unittest.TestCase): + + def assertEquals(self, first, second): + """ + Compare two expressions are equals. + """ + if isinstance(first, Mv): + first = first.obj + + if isinstance(second, Mv): + second = second.obj + + diff = simplify(first - second) + + self.assertTrue(diff == 0, "\n%s\n==\n%s\n%s" % (first, second, diff)) + + def assertProjEquals(self, X, Y): + """ + Compare two points, two planes or two lines up to a scalar. + """ + assert isinstance(X, Mv) + assert isinstance(Y, Mv) + + X /= self.norm(X) + Y /= self.norm(Y) + + # We can't easily retrieve the sign, so we test both + diff = simplify(X.obj - Y.obj) + if diff != S.Zero: + diff = simplify(X.obj + Y.obj) + + self.assertTrue(diff == S.Zero, "\n%s\n==\n%s" % (X, Y)) + + def assertNotEquals(self, first, second): + """ + Compare two expressions are not equals. + """ + if isinstance(first, Mv): + first = first.obj + + if isinstance(second, Mv): + second = second.obj + + diff = simplify(first - second) + + self.assertTrue(diff != 0, "\n%s\n!=\n%s\n%s" % (first, second, diff)) + From 7c2069efadefd145ff4580aa4f399a013f6bfa64 Mon Sep 17 00:00:00 2001 From: Sylvain Meunier Date: Mon, 25 Nov 2019 21:48:49 +0100 Subject: [PATCH 71/78] Symbols are real --- tests/test_chapter11.py | 30 +++++++------- tests/test_chapter13.py | 24 +++++------ tests/test_chapter2.py | 90 +++++++++++++++++++---------------------- tests/test_chapter3.py | 7 +--- tests/test_chapter6.py | 45 +++++++++------------ tests/test_chapter7.py | 13 ++---- tests/test_pga.py | 26 ++++++------ 7 files changed, 107 insertions(+), 128 deletions(-) diff --git a/tests/test_chapter11.py b/tests/test_chapter11.py index 1b0c1d83..8a2d9c12 100644 --- a/tests/test_chapter11.py +++ b/tests/test_chapter11.py @@ -14,13 +14,13 @@ def test11_4(self): """ GA, e_0, e_1, e_2, e_3 = Ga.build("e*0|1|2|3", g='-1 0 0 0, 0 1 0 0, 0 0 1 0, 0 0 0 1') - p0 = Symbol('p0') - q0 = Symbol('q0') - r0 = Symbol('r0') + p0 = Symbol('p0', real=True) + q0 = Symbol('q0', real=True) + r0 = Symbol('r0', real=True) - p = GA.mv((p0, Symbol('p1'), Symbol('p2'), Symbol('p3')), 'vector') - q = GA.mv((q0, Symbol('q1'), Symbol('q2'), Symbol('q3')), 'vector') - r = GA.mv((r0, Symbol('r1'), Symbol('r2'), Symbol('r3')), 'vector') + p = GA.mv((p0, Symbol('p1', real=True), Symbol('p2', real=True), Symbol('p3', real=True)), 'vector') + q = GA.mv((q0, Symbol('q1', real=True), Symbol('q2', real=True), Symbol('q3', real=True)), 'vector') + r = GA.mv((r0, Symbol('r1', real=True), Symbol('r2', real=True), Symbol('r3', real=True)), 'vector') p_inf = p.subs({p0: 0}) q_inf = q.subs({q0: 0}) @@ -57,13 +57,13 @@ def test11_6(self): self.assertEquals(Ip, e_0 ^ Ir) self.assertEquals(Ip, e_0 * Ir) - p = GA.mv([1] + [Symbol('p%d' % i) for i in range(1, GA.n)], 'vector') + p = GA.mv([1] + [Symbol('p%d' % i, real=True) for i in range(1, GA.n)], 'vector') v = [ - GA.mv([0] + [Symbol('q%d' % i) for i in range(1, GA.n)], 'vector'), - GA.mv([0] + [Symbol('r%d' % i) for i in range(1, GA.n)], 'vector'), - GA.mv([0] + [Symbol('s%d' % i) for i in range(1, GA.n)], 'vector'), - GA.mv([0] + [Symbol('t%d' % i) for i in range(1, GA.n)], 'vector'), + GA.mv([0] + [Symbol('q%d' % i, real=True) for i in range(1, GA.n)], 'vector'), + GA.mv([0] + [Symbol('r%d' % i, real=True) for i in range(1, GA.n)], 'vector'), + GA.mv([0] + [Symbol('s%d' % i, real=True) for i in range(1, GA.n)], 'vector'), + GA.mv([0] + [Symbol('t%d' % i, real=True) for i in range(1, GA.n)], 'vector'), ] # We test available finite k-flats @@ -136,10 +136,10 @@ def test11_12_2_2(self): p = e_0 + e_1 - 3 * e_2 L = u ^ p - t = Symbol('t') - x_1 = Symbol('x_1') - x_2 = Symbol('x_2') - x_3 = Symbol('x_3') + t = Symbol('t', real=True) + x_1 = Symbol('x_1', real=True) + x_2 = Symbol('x_2', real=True) + x_3 = Symbol('x_3', real=True) x = e_0 + x_1 * e_1 + x_2 * e_2 + x_3 * e_3 # x(t) diff --git a/tests/test_chapter13.py b/tests/test_chapter13.py index e9cc320b..f23efcea 100644 --- a/tests/test_chapter13.py +++ b/tests/test_chapter13.py @@ -26,8 +26,8 @@ def vector(x, y, z): def point(v): return o + v + S.Half * v * v * inf - p = point(vector(Symbol('px'), Symbol('py'), Symbol('pz'))) - q = point(vector(Symbol('qx'), Symbol('qy'), Symbol('qz'))) + p = point(vector(Symbol('px', real=True), Symbol('py', real=True), Symbol('pz', real=True))) + q = point(vector(Symbol('qx', real=True), Symbol('qy', real=True), Symbol('qz', real=True))) self.assertEquals(p | q, -S.Half * q * q + p | q - S.Half * p * p) self.assertEquals(p | q, -S.Half * (q - p) * (q - p)) @@ -53,8 +53,8 @@ def vector(x, y, z): def point(alpha, v): return alpha * (o + v + S.Half * v * v * inf) - alpha = Symbol('alpha') - p = point(alpha, vector(Symbol('px'), Symbol('py'), Symbol('pz'))) + alpha = Symbol('alpha', real=True) + p = point(alpha, vector(Symbol('px', real=True), Symbol('py', real=True), Symbol('pz', real=True))) self.assertEquals(p | p, S.Zero) self.assertEquals(inf | p, -alpha) @@ -62,10 +62,10 @@ def point(alpha, v): def dual_plane(n, delta): return n + delta * inf - nx = Symbol('nx') - ny = Symbol('ny') - nz = Symbol('nz') - p = dual_plane(vector(nx, ny, nz), Symbol('delta')) + nx = Symbol('nx', real=True) + ny = Symbol('ny', real=True) + nz = Symbol('nz', real=True) + p = dual_plane(vector(nx, ny, nz), Symbol('delta', real=True)) self.assertEquals(p | p, nx * nx + ny * ny + nz * nz) self.assertEquals(inf | p, S.Zero) @@ -76,10 +76,10 @@ def dual_sphere(alpha, c, r): def dual_im_sphere(alpha, c, r): return alpha * (c + S.Half * r * r * inf) - cx = Symbol('cx') - cy = Symbol('cy') - cz = Symbol('cz') - r = Symbol('r') + cx = Symbol('cx', real=True) + cy = Symbol('cy', real=True) + cz = Symbol('cz', real=True) + r = Symbol('r', real=True) c = point(1, vector(cx, cy, cz)) self.assertEquals(c * c, S.Zero) diff --git a/tests/test_chapter2.py b/tests/test_chapter2.py index dd5fbc99..31711930 100755 --- a/tests/test_chapter2.py +++ b/tests/test_chapter2.py @@ -18,7 +18,7 @@ def test2_3_2(self): a = GA.mv('a', 'vector') b = GA.mv('b', 'vector') c = GA.mv('c', 'vector') - alpha = Symbol('alpha') + alpha = Symbol('alpha', real=True) self.assertEquals(a ^ b, -b ^ a) self.assertEquals(a ^ (alpha * b), alpha * (a ^ b)) @@ -32,15 +32,15 @@ def test_2_4_2(self): """ GA, e_1, e_2, e_3 = Ga.build('e*1|2|3') - a_1 = Symbol('a_1') - a_2 = Symbol('a_2') - a_3 = Symbol('a_3') - b_1 = Symbol('b_1') - b_2 = Symbol('b_2') - b_3 = Symbol('b_3') - c_1 = Symbol('c_1') - c_2 = Symbol('c_2') - c_3 = Symbol('c_3') + a_1 = Symbol('a_1', real=True) + a_2 = Symbol('a_2', real=True) + a_3 = Symbol('a_3', real=True) + b_1 = Symbol('b_1', real=True) + b_2 = Symbol('b_2', real=True) + b_3 = Symbol('b_3', real=True) + c_1 = Symbol('c_1', real=True) + c_2 = Symbol('c_2', real=True) + c_3 = Symbol('c_3', real=True) a = GA.mv((a_1, a_2, a_3), 'vector') b = GA.mv((b_1, b_2, b_3), 'vector') @@ -146,8 +146,8 @@ def test2_12_1_4(self): GA, e_1, e_2 = Ga.build('e*1|2') # x is defined in the basis {e_1, e_2} - a = Symbol('a') - b = Symbol('b') + a = Symbol('a', real=True) + b = Symbol('b', real=True) x = a * e_1 + b * e_2 # x lies on L and M (eq. L == 0 and M == 0) @@ -189,9 +189,9 @@ def test2_12_2_1(self): GA, e_1, e_2 = Ga.build('e*1|2', g='1 0, 0 1') # TODO: use alpha, beta and theta instead of a, b and t (it crashes sympy) - a = Symbol('a') - b = Symbol('b') - t = Symbol('t') + a = Symbol('a', real=True) + b = Symbol('b', real=True) + t = Symbol('t', real=True) x = a * e_1 y = b * (cos(t) * e_1 + sin(t) * e_2) B = x ^ y @@ -213,9 +213,9 @@ def test2_12_2_2(self): """ GA, e_1, e_2 = Ga.build('e*1|2', g='1 0, 0 1') - a = Symbol('a') - b = Symbol('b') - t = Symbol('t') + a = Symbol('a', real=True) + b = Symbol('b', real=True) + t = Symbol('t', real=True) x = a * e_1 y = b * (cos(t) * e_1 + sin(t) * e_2) @@ -235,22 +235,22 @@ def test2_12_2_4(self): """ GA, e_1, e_2, e_3 = Ga.build('e*1|2|3') - a_1 = Symbol('a_1') - a_2 = Symbol('a_2') - a_3 = Symbol('a_3') - b_1 = Symbol('b_1') - b_2 = Symbol('b_2') - b_3 = Symbol('b_3') - c_1 = Symbol('c_1') - c_2 = Symbol('c_2') - c_3 = Symbol('c_3') + a_1 = Symbol('a_1', real=True) + a_2 = Symbol('a_2', real=True) + a_3 = Symbol('a_3', real=True) + b_1 = Symbol('b_1', real=True) + b_2 = Symbol('b_2', real=True) + b_3 = Symbol('b_3', real=True) + c_1 = Symbol('c_1', real=True) + c_2 = Symbol('c_2', real=True) + c_3 = Symbol('c_3', real=True) a = a_1 * e_1 + a_2 * e_2 + a_3 * e_3 b = b_1 * e_1 + b_2 * e_2 + b_3 * e_3 c = c_1 * e_1 + c_2 * e_2 + c_3 * e_3 - x_1 = Symbol('x_1') - x_2 = Symbol('x_2') - x_3 = Symbol('x_3') + x_1 = Symbol('x_1', real=True) + x_2 = Symbol('x_2', real=True) + x_3 = Symbol('x_3', real=True) x = x_1 * a + x_2 * b + x_3 * c # Solve x_1 @@ -280,16 +280,16 @@ def test2_12_2_5(self): B = (e_1 ^ e_2) + (e_3 ^ e_4) # C is the product of a and b vectors - a_1 = Symbol('a_1') - a_2 = Symbol('a_2') - a_3 = Symbol('a_3') - a_4 = Symbol('a_4') + a_1 = Symbol('a_1', real=True) + a_2 = Symbol('a_2', real=True) + a_3 = Symbol('a_3', real=True) + a_4 = Symbol('a_4', real=True) a = a_1 * e_1 + a_2 * e_2 + a_3 * e_3 + a_4 * e_4 - b_1 = Symbol('b_1') - b_2 = Symbol('b_2') - b_3 = Symbol('b_3') - b_4 = Symbol('b_4') + b_1 = Symbol('b_1', real=True) + b_2 = Symbol('b_2', real=True) + b_3 = Symbol('b_3', real=True) + b_4 = Symbol('b_4', real=True) b = b_1 * e_1 + b_2 * e_2 + b_3 * e_3 + b_4 * e_4 C = a ^ b @@ -326,10 +326,10 @@ def test2_12_2_6(self): B = (e_1 ^ e_2) + (e_3 ^ e_4) # x - x_1 = Symbol('x_1') - x_2 = Symbol('x_2') - x_3 = Symbol('x_3') - x_4 = Symbol('x_4') + x_1 = Symbol('x_1', real=True) + x_2 = Symbol('x_2', real=True) + x_3 = Symbol('x_3', real=True) + x_4 = Symbol('x_4', real=True) x = x_1 * e_1 + x_2 * e_2 + x_3 * e_3 + x_4 * e_4 # Solve x ^ B = 0 @@ -358,9 +358,3 @@ def test2_12_2_9(self): Ak = GA.mv('A', 'blade', k) Bl = GA.mv('B', 'blade', l) self.assertEquals(Ak ^ Bl, (-1)**(k * l) * (Bl ^ Ak)) - - -if __name__ == '__main__': - - unittest.main() - diff --git a/tests/test_chapter3.py b/tests/test_chapter3.py index 80743548..a03ae771 100644 --- a/tests/test_chapter3.py +++ b/tests/test_chapter3.py @@ -63,7 +63,7 @@ def test_3_2_2(self): for (k, A), (l, B), (m, C) in product(A_blades, B_blades, C_blades): self.assertEquals(A < (B + C), (A < B) + (A < C)) - alpha = Symbol("alpha") + alpha = Symbol('alpha', real=True) for (k, A), (l, B) in product(A_blades, B_blades): self.assertEquals((alpha * A) < B, alpha * (A < B)) self.assertEquals((alpha * A) < B, A < (alpha * B)) @@ -466,8 +466,3 @@ def test3_10_2_11(self): self.assertEquals(xx, b * (a < c) - c * (a < b)) Ga.dual_mode() - - -if __name__ == '__main__': - - unittest.main() diff --git a/tests/test_chapter6.py b/tests/test_chapter6.py index cf458d91..f5cdc19a 100644 --- a/tests/test_chapter6.py +++ b/tests/test_chapter6.py @@ -61,12 +61,12 @@ def test6_1_4_2(self): self.assertEquals(e_12 * e_2, e_1) self.assertEquals(e_12 * e_12, -1) - a_1 = Symbol('a_1') - a_2 = Symbol('a_2') + a_1 = Symbol('a_1', real=True) + a_2 = Symbol('a_2', real=True) a = GA.mv((a_1, a_2), 'vector') - b_1 = Symbol('b_1') - b_2 = Symbol('b_2') + b_1 = Symbol('b_1', real=True) + b_2 = Symbol('b_2', real=True) b = GA.mv((b_1, b_2), 'vector') self.assertEquals(a * b, a_1 * b_1 + a_2 * b_2 + (a_1 * b_2 - a_2 * b_1) * (e_1 ^ e_2)) @@ -195,16 +195,16 @@ def test6_6_2_4(self): R = (e_1 * (e_1 + e_2) * (e_2 + e_3) * (e_1 + e_4)).get_grade(2) # A 2-blade - a_1 = Symbol('a_1') - a_2 = Symbol('a_2') - a_3 = Symbol('a_3') - a_4 = Symbol('a_4') + a_1 = Symbol('a_1', real=True) + a_2 = Symbol('a_2', real=True) + a_3 = Symbol('a_3', real=True) + a_4 = Symbol('a_4', real=True) a = GA.mv((a_1, a_2, a_3, a_4), 'vector') - b_1 = Symbol('b_1') - b_2 = Symbol('b_2') - b_3 = Symbol('b_3') - b_4 = Symbol('b_4') + b_1 = Symbol('b_1', real=True) + b_2 = Symbol('b_2', real=True) + b_3 = Symbol('b_3', real=True) + b_4 = Symbol('b_4', real=True) b = GA.mv((b_1, b_2, b_3, b_4), 'vector') S = a ^ b @@ -277,16 +277,16 @@ def proj(X, B): R = X - P # A 2-blade - a_1 = Symbol('a_1') - a_2 = Symbol('a_2') - a_3 = Symbol('a_3') - a_4 = Symbol('a_4') + a_1 = Symbol('a_1', real=True) + a_2 = Symbol('a_2', real=True) + a_3 = Symbol('a_3', real=True) + a_4 = Symbol('a_4', real=True) a = GA.mv((a_1, a_2, a_3, a_4), 'vector') - b_1 = Symbol('b_1') - b_2 = Symbol('b_2') - b_3 = Symbol('b_3') - b_4 = Symbol('b_4') + b_1 = Symbol('b_1', real=True) + b_2 = Symbol('b_2', real=True) + b_3 = Symbol('b_3', real=True) + b_4 = Symbol('b_4', real=True) b = GA.mv((b_1, b_2, b_3, b_4), 'vector') S = a ^ b @@ -302,8 +302,3 @@ def proj(X, B): # TODO: use solve if sympy fix it result = solve_poly_system(system, unknowns) self.assertTrue(result is None) - - -if __name__ == '__main__': - - unittest.main() diff --git a/tests/test_chapter7.py b/tests/test_chapter7.py index 33075d4c..cc26061c 100644 --- a/tests/test_chapter7.py +++ b/tests/test_chapter7.py @@ -26,12 +26,12 @@ def test7_9_1(self): self.assertEquals(R2R1 * (e_1 ^ e_2) * R2R1.rev(), -e_2 ^ e_3) # .4 Compute the axis and angle of R2 R1 - a_1 = Symbol('a_1') - a_2 = Symbol('a_2') - a_3 = Symbol('a_3') + a_1 = Symbol('a_1', real=True) + a_2 = Symbol('a_2', real=True) + a_3 = Symbol('a_3', real=True) a = GA.mv((a_1, a_2, a_3), 'vector') - theta = Symbol('theta') + theta = Symbol('theta', real=True) A = a.dual() R = cos(theta / 2) + A * sin(theta / 2) @@ -81,8 +81,3 @@ def hat(k, M): # Reset Ga.dual_mode() - - -if __name__ == '__main__': - - unittest.main() diff --git a/tests/test_pga.py b/tests/test_pga.py index 2cf095e1..7dbefc1c 100644 --- a/tests/test_pga.py +++ b/tests/test_pga.py @@ -415,10 +415,10 @@ def test_geometry_incidence_planes_meet_into_points_2(self): """ Planes meet into points. """ - P1 = self.point(*symbols('x1 y1 z1')) - P2 = self.point(*symbols('x2 y2 z2')) - P3 = self.point(*symbols('x3 y3 z3')) - P4 = self.point(*symbols('x4 y4 z4')) + P1 = self.point(*symbols('x1 y1 z1', real=True)) + P2 = self.point(*symbols('x2 y2 z2', real=True)) + P3 = self.point(*symbols('x3 y3 z3', real=True)) + P4 = self.point(*symbols('x4 y4 z4', real=True)) p123 = Jinv(J(P1) ^ J(P2) ^ J(P3)) p124 = Jinv(J(P1) ^ J(P2) ^ J(P4)) p234 = Jinv(J(P2) ^ J(P3) ^ J(P4)) @@ -434,13 +434,13 @@ def test_geometry_incidence_points_join_into_planes(self): """ Points join into planes. """ - x1, y1, z1 = symbols('x1 y1 z1') + x1, y1, z1 = symbols('x1 y1 z1', real=True) P1 = self.point(x1, y1, z1) - x2, y2, z2 = symbols('x2 y2 z2') + x2, y2, z2 = symbols('x2 y2 z2', real=True) P2 = self.point(x2, y2, z2) - x3, y3, z3 = symbols('x3 y3 z3') + x3, y3, z3 = symbols('x3 y3 z3', real=True) P3 = self.point(x3, y3, z3) pp = Jinv(J(P1) ^ J(P2) ^ J(P3)) @@ -530,7 +530,7 @@ def test_metric_distance_between_parallel_planes(self): """ We can measure the distance between two parallel and normalized planes using the meeting line norm. """ - nx, ny, nz, x, y = symbols('nx ny nz x y') + nx, ny, nz, x, y = symbols('nx ny nz x y', real=True) n_norm = sqrt(nx * nx + ny * ny + nz * nz) p1 = self.plane(nx / n_norm, ny / n_norm, nz / n_norm, -x) @@ -580,16 +580,16 @@ def test_metric_common_normal_line(self): """ We can find the common normal line of two normalized lines. """ - x0, y0, z0 = symbols('x0 y0 z0') + x0, y0, z0 = symbols('x0 y0 z0', real=True) P0 = self.point(x0, y0, z0) - x1, y1, z1 = symbols('x1 y1 z1') + x1, y1, z1 = symbols('x1 y1 z1', real=True) P1 = self.point(x1, y1, z1) - x2, y2, z2 = symbols('x2 y2 z2') + x2, y2, z2 = symbols('x2 y2 z2', real=True) P2 = self.point(x2, y2, z2) - x3, y3, z3 = symbols('x3 y3 z3') + x3, y3, z3 = symbols('x3 y3 z3', real=True) P3 = self.point(x3, y3, z3) l0 = Jinv(J(P0) ^ J(P1)) @@ -784,7 +784,7 @@ def test_motors_translator(self): """ Translate anything by a normalized line. """ - x0, y0, z0 = symbols('x0 y0 z0') + x0, y0, z0 = symbols('x0 y0 z0', real=True) Px = self.point(x0, y0, z0) P0 = self.point(0, 0, 0) From 4fce49f657c25f27e1607f6a2b78403ab30310ea Mon Sep 17 00:00:00 2001 From: Sylvain Meunier Date: Mon, 25 Nov 2019 21:52:46 +0100 Subject: [PATCH 72/78] Fix PEP8 --- tests/test_chapter11.py | 9 +-- tests/test_chapter2.py | 29 +++------- tests/test_chapter3.py | 120 ++++++++++++++++++++-------------------- tests/test_chapter6.py | 13 +---- tests/test_chapter8.py | 2 +- tests/test_generator.py | 10 ---- tests/test_pga.py | 22 ++++---- tests/test_utils.py | 5 +- 8 files changed, 91 insertions(+), 119 deletions(-) diff --git a/tests/test_chapter11.py b/tests/test_chapter11.py index 8a2d9c12..b058f263 100644 --- a/tests/test_chapter11.py +++ b/tests/test_chapter11.py @@ -72,9 +72,10 @@ def test11_6(self): X = (p ^ A) self.assertNotEquals(X, 0) M = e_0_inv < (e_0 ^ X) + # Very slow - #d = (e_0_inv < (e_0 ^ X)) / (e_0_inv < X) - #d_inv = d.inv() + # d = (e_0_inv < (e_0 ^ X)) / (e_0_inv < X) + # d_inv = d.inv() def hat(A): return ((-1) ** A.pure_grade()) * A @@ -87,8 +88,8 @@ def hat(A): self.assertEquals(Xd, p < ((A < Ir_inv) * e_0_inv)) self.assertEquals(Xd, hat(A < Ir_inv) - e_0_inv * (p < hat(A < Ir_inv))) # Very slow - #self.assertEquals(Xd, hat(A < Ir_inv) + e_0_inv * hat(M < Ir_inv)) - #self.assertEquals(Xd, (e_0_inv - d_inv) * hat(M < Ir_inv)) + # self.assertEquals(Xd, hat(A < Ir_inv) + e_0_inv * hat(M < Ir_inv)) + # self.assertEquals(Xd, (e_0_inv - d_inv) * hat(M < Ir_inv)) Ga.dual_mode() diff --git a/tests/test_chapter2.py b/tests/test_chapter2.py index 31711930..94b3c90d 100755 --- a/tests/test_chapter2.py +++ b/tests/test_chapter2.py @@ -25,7 +25,6 @@ def test2_3_2(self): self.assertEquals(GA.mv(alpha, 'scalar') ^ b, alpha * b) self.assertEquals(a ^ (b + c), (a ^ b) + (a ^ c)) - def test_2_4_2(self): """ Associativity of the outer product and calculating determinants. @@ -57,7 +56,6 @@ def test_2_4_2(self): self.assertEquals(a ^ b ^ c, m.det() * (e_1 ^ e_2 ^ e_3)) - def test2_9_1(self): """ Blades and grades. Be careful ga.grade_decomposition and Mv.pure_grade can't tell if a multivector is a blade, @@ -83,7 +81,6 @@ def test2_9_1(self): self.assertEquals(len(grades), 1) self.assertEquals(list(grades.keys())[0], 0 if C == 0 else k + l) - def test2_9_5(self): """ Reversion and grade involution. @@ -97,7 +94,6 @@ def test2_9_5(self): self.assertEquals(A_rev, A.rev()) self.assertEquals(A_rev, ((-1) ** ((k * (k - 1)) / 2)) * A) - def test2_12_1_1(self): """ Compute the outer products of the following 3-space expressions, @@ -105,13 +101,13 @@ def test2_12_1_1(self): """ GA, e_1, e_2, e_3 = Ga.build('e*1|2|3') self.assertTrue((e_1 + e_2) ^ (e_1 + e_3) == (-e_1 ^ e_2) + (e_1 ^ e_3) + (e_2 ^ e_3)) - self.assertTrue((e_1 + e_2 + e_3) ^ (2*e_1) == -2*(e_1 ^ e_2) - 2*(e_1 ^ e_3)) + self.assertTrue((e_1 + e_2 + e_3) ^ (2 * e_1) == -2 * (e_1 ^ e_2) - 2 * (e_1 ^ e_3)) self.assertTrue((e_1 - e_2) ^ (e_1 - e_3) == (e_1 ^ e_2) - (e_1 ^ e_3) + (e_2 ^ e_3)) - self.assertTrue((e_1 + e_2) ^ (0.5*e_1 + 2*e_2 + 3*e_3) == 1.5*(e_1 ^ e_2) + 3*(e_1 ^ e_3) + 3*(e_2 ^ e_3)) + self.assertTrue( + (e_1 + e_2) ^ (0.5 * e_1 + 2 * e_2 + 3 * e_3) == 1.5 * (e_1 ^ e_2) + 3 * (e_1 ^ e_3) + 3 * (e_2 ^ e_3)) self.assertTrue((e_1 ^ e_2) ^ (e_1 + e_3) == (e_1 ^ e_2 ^ e_3)) self.assertTrue((e_1 + e_2) ^ ((e_1 ^ e_2) + (e_2 ^ e_3)) == (e_1 ^ e_2 ^ e_3)) - def test2_12_1_2(self): """ Given the 2-blade B = e_1 ^ (e_2 - e_3) that represents a plane, @@ -122,8 +118,7 @@ def test2_12_1_2(self): self.assertTrue(e_1 ^ B == 0) self.assertFalse((e_1 + e_2) ^ B == 0) self.assertFalse((e_1 + e_2 + e_3) ^ B == 0) - self.assertTrue((2*e_1 - e_2 + e_3) ^ B == 0) - + self.assertTrue((2 * e_1 - e_2 + e_3) ^ B == 0) def test2_12_1_3(self): """ @@ -131,12 +126,11 @@ def test2_12_1_3(self): and b = -e_1 - e_2 (relative to the area of e_1 ^ e_2) ? """ GA, e_1, e_2, e_3 = Ga.build('e*1|2|3') - a = e_1 + 2*e_2 + a = e_1 + 2 * e_2 b = -e_1 - e_2 B = a ^ b self.assertTrue(B == 1 * (e_1 ^ e_2)) - def test2_12_1_4(self): """ Compute the intersection of the non-homogeneous line L with position vector e_1 @@ -155,7 +149,7 @@ def test2_12_1_4(self): M = (x ^ (e_1 + e_2)) - (e_2 ^ (e_1 + e_2)) # Solve the linear system - R = solve([L.obj, M.obj], a, b, dict=True) # TODO: fix this... + R = solve([L.obj, M.obj], a, b, dict=True) # TODO: fix this... self.assertTrue(len(R), 1) # Replace symbols @@ -163,7 +157,6 @@ def test2_12_1_4(self): self.assertEquals(x, e_1 + 2 * e_2) - def test2_12_1_5(self): """ Compute (2 + 3 * e_3) ^ (e_1 + (e_2 ^ e_3) using the grade based defining equations of section 2.9.4. @@ -179,7 +172,6 @@ def test2_12_1_5(self): self.assertTrue(C == (2 * e_1 + 3 * (e_3 ^ e_1) + 2 * (e_2 ^ e_3))) - def test2_12_2_1(self): """ In R2 with Euclidean metric, choose an orthonormal basis {e_1, e_2} in the plane of a and b such that e1 is parallel to a. @@ -207,7 +199,6 @@ def test2_12_2_1(self): area = Matrix([x, y]).det() self.assertTrue(area == (a * b * sin(t))) - def test2_12_2_2(self): """ """ @@ -228,7 +219,6 @@ def test2_12_2_2(self): self.assertTrue(cross == (a * b * sin(t))) - def test2_12_2_4(self): """ Solve the linear system of equation using the outer product. @@ -268,7 +258,6 @@ def test2_12_2_4(self): self.assertTrue((x ^ c ^ a) == x_2 * (b ^ c ^ a)) self.assertTrue((x ^ c ^ a) * (b ^ c ^ a).inv() == GA.mv(x_2, 'scalar')) - def test2_12_2_5(self): """ Consider R4 with basis {e_1, e_2, e_3, e_4}. Show that the 2-vector B = (e_1 ^ e_2) + (e_3 ^ e_4) @@ -315,7 +304,6 @@ def test2_12_2_5(self): result = solve_poly_system(system, unknowns) self.assertTrue(result is None) - def test2_12_2_6(self): """ Show that B = e1 ^ e2 + e3 ^ e4 of the previous exercise doesn't contain any other vector than 0. @@ -348,13 +336,12 @@ def test2_12_2_6(self): self.assertTrue(result[2] == 0) self.assertTrue(result[3] == 0) - def test2_12_2_9(self): """ Prove Ak ^ Bl = (-1**kl) Bl ^ Ak. """ - for GA in [Ga('e*1|2'), Ga('e*1|2|3'), Ga('e*1|2|3|4')]: #, Ga('e*1|2|3|4|5')]: + for GA in [Ga('e*1|2'), Ga('e*1|2|3'), Ga('e*1|2|3|4')]: # , Ga('e*1|2|3|4|5')]: for k, l in product(range(GA.n + 1), range(GA.n + 1)): Ak = GA.mv('A', 'blade', k) Bl = GA.mv('B', 'blade', l) - self.assertEquals(Ak ^ Bl, (-1)**(k * l) * (Bl ^ Ak)) + self.assertEquals(Ak ^ Bl, (-1) ** (k * l) * (Bl ^ Ak)) diff --git a/tests/test_chapter3.py b/tests/test_chapter3.py index a03ae771..9e98517a 100644 --- a/tests/test_chapter3.py +++ b/tests/test_chapter3.py @@ -23,7 +23,6 @@ def test_3_1_2(self): else: self.assertTrue((A * B).scalar() == 0) - def test_3_2_2(self): """ Computing the contraction explicitly. @@ -50,7 +49,7 @@ def test_3_2_2(self): # vector and the outer product of 2 blades of various grades (scalars, vectors, 2-vectors...) k, A = A_blades[1] for (l, B), (m, C) in product(B_blades, C_blades): - self.assertEquals(A < (B ^ C), ((A < B) ^ C) + (-1)**l * (B ^ (A < C))) + self.assertEquals(A < (B ^ C), ((A < B) ^ C) + (-1) ** l * (B ^ (A < C))) # vector and the outer product of 2 blades of various grades (scalars, vectors, 2-vectors...) for (k, A), (l, B), (m, C) in product(A_blades, B_blades, C_blades): @@ -74,7 +73,6 @@ def test_3_2_2(self): self.assertEquals(A < B, (A_minus1 ^ a) < B) self.assertEquals(A < B, A_minus1 < (a < B)) - def test_3_4(self): """ The other contraction. @@ -92,14 +90,13 @@ def test_3_4(self): self.assertEquals(len(C_grades), 1) self.assertTrue(C == 0 or C_grades[0] == l - k) - def test_3_5_2(self): """ The inverse of a blade. """ Ga.dual_mode("Iinv+") - for GA in [Ga('e*1|2'), Ga('e*1|2|3'), Ga('e*1|2|3|4')]: # , Ga('e*1|2|3|4|5')]: + for GA in [Ga('e*1|2'), Ga('e*1|2|3'), Ga('e*1|2|3|4')]: # , Ga('e*1|2|3|4|5')]: A_grade_and_blades = [(k, GA.mv('A', 'blade', k)) for k in range(GA.n + 1)] for k, A in A_grade_and_blades: inv_A = A.inv() @@ -117,7 +114,6 @@ def test_3_5_2(self): Ga.dual_mode() - def test_3_5_3(self): """ Orthogonal complement and duality. @@ -125,8 +121,9 @@ def test_3_5_3(self): Ga.dual_mode("Iinv+") # some blades by grades for each space - spaces = [([GA.mv('A', 'blade', k) for k in range(GA.n + 1)], GA) for GA in [Ga('e*1|2'), Ga('e*1|2|3'), Ga('e*1|2|3|4')]] - + spaces = [([GA.mv('A', 'blade', k) for k in range(GA.n + 1)], GA) for GA in + [Ga('e*1|2'), Ga('e*1|2|3'), Ga('e*1|2|3|4')]] + # dualization for blades, GA in spaces: for A in blades: @@ -144,7 +141,6 @@ def test_3_5_3(self): Ga.dual_mode() - def test_3_5_4(self): """ The duality relationships. @@ -163,7 +159,6 @@ def test_3_5_4(self): Ga.dual_mode() - def test_3_6(self): """ Orthogonal projection of subspaces. @@ -184,7 +179,6 @@ def P(X, B): for X, B in product(X_blades, B_blades): self.assertEquals(X < B, P(X, B) < B) - def test3_7_2(self): """ The cross product incorporated. @@ -217,7 +211,6 @@ def test3_7_2(self): Ga.dual_mode() - def test_3_10_1_1(self): """ Let a = e_1 + e_2 and b = e_2 + e_3 in a 3D Euclidean space R(3,0) with an orthonormal basis {e_1, e_2, e_3}. @@ -229,7 +222,7 @@ def test_3_10_1_1(self): GA, e_1, e_2, e_3 = Ga.build('e*1|2|3', g='1 0 0, 0 1 0, 0 0 1') a = e_1 + e_2 b = e_2 + e_3 - + # a self.assertEquals(e_1 < a, (e_1 < e_1) + (e_1 < e_2)) self.assertEquals(e_1 < e_1, e_1 | e_1) @@ -237,123 +230,133 @@ def test_3_10_1_1(self): self.assertEquals(e_1 < e_2, e_1 | e_2) self.assertEquals(e_1 < e_2, 0) self.assertEquals(e_1 < a, 1) - + # b - self.assertEquals(e_1 < (a ^ b), ((e_1 < a) ^ b) - (a ^ (e_1 < b))) - self.assertEquals((e_1 < a) ^ b, b) # reusing drill a + self.assertEquals(e_1 < (a ^ b), ((e_1 < a) ^ b) - (a ^ (e_1 < b))) + self.assertEquals((e_1 < a) ^ b, b) # reusing drill a self.assertEquals(e_1 < b, (e_1 < e_2) + (e_1 < e_3)) self.assertEquals(e_1 < b, 0) self.assertEquals(e_1 < (a ^ b), b) self.assertEquals(e_1 < (a ^ b), e_2 + e_3) - + # c self.assertEquals((a ^ b) < e_1, a < (b < e_1)) self.assertEquals((a ^ b) < e_1, a < ((e_2 < e_1) + (e_3 < e_1))) self.assertEquals((a ^ b) < e_1, 0) - + # d self.assertEquals(((2 * a) + b) < (a + b), ((2 * a) < a) + ((2 * a) < b) + (b < a) + (b < b)) self.assertEquals(((2 * a) + b) < (a + b), ((2 * (a < a)) + (2 * (a < b)) + (b < a) + (b < b))) self.assertEquals(((2 * a) + b) < (a + b), 4 + 2 + 1 + 2) - + # e self.assertEquals(a < (e_1 ^ e_2 ^ e_3), a < ((e_1 ^ e_2) ^ e_3)) self.assertEquals(a < (e_1 ^ e_2 ^ e_3), ((a < (e_1 ^ e_2)) ^ e_3) + ((e_1 ^ e_2) ^ (a < e_3))) - self.assertEquals(a < (e_1 ^ e_2 ^ e_3), (((e_1 < (e_1 ^ e_2)) + (e_2 < (e_1 ^ e_2))) ^ e_3) + ((e_1 ^ e_2) ^ ((e_1 < e_3) + (e_2 < e_3)))) - self.assertEquals(((e_1 < (e_1 ^ e_2)) + (e_2 < (e_1 ^ e_2))) ^ e_3, (((e_1 < e_1) ^ e_2) - (e_1 ^ (e_1 < e_2)) + ((e_2 < e_1) ^ e_2) - (e_1 ^ (e_2 < e_2))) ^ e_3) + self.assertEquals(a < (e_1 ^ e_2 ^ e_3), (((e_1 < (e_1 ^ e_2)) + (e_2 < (e_1 ^ e_2))) ^ e_3) + ( + (e_1 ^ e_2) ^ ((e_1 < e_3) + (e_2 < e_3)))) + self.assertEquals(((e_1 < (e_1 ^ e_2)) + (e_2 < (e_1 ^ e_2))) ^ e_3, + (((e_1 < e_1) ^ e_2) - (e_1 ^ (e_1 < e_2)) + ((e_2 < e_1) ^ e_2) - (e_1 ^ (e_2 < e_2))) ^ e_3) self.assertEquals(((e_1 < (e_1 ^ e_2)) + (e_2 < (e_1 ^ e_2))) ^ e_3, (e_2 - e_1) ^ e_3) - self.assertEquals(((e_1 < (e_1 ^ e_2)) + (e_2 < (e_1 ^ e_2))) ^ e_3, (e_2 ^ e_3) - (e_1 ^ e_3)) + self.assertEquals(((e_1 < (e_1 ^ e_2)) + (e_2 < (e_1 ^ e_2))) ^ e_3, (e_2 ^ e_3) - (e_1 ^ e_3)) self.assertEquals(e_1 < e_3, 0) self.assertEquals(e_2 < e_3, 0) self.assertEquals(((e_1 ^ e_2) ^ ((e_1 < e_3) + (e_2 < e_3))), 0) self.assertEquals(a < (e_1 ^ e_2 ^ e_3), (e_2 ^ e_3) + (e_3 ^ e_1)) - + # f - self.assertEquals(a < GA.I_inv(), a < (-e_1 ^ e_2 ^ e_3)) # equals because we fixed the metric - self.assertEquals(a < GA.I_inv(), a < ((e_1 ^ e_2) ^ (-e_3))) # almost last drill + self.assertEquals(a < GA.I_inv(), a < (-e_1 ^ e_2 ^ e_3)) # equals because we fixed the metric + self.assertEquals(a < GA.I_inv(), a < ((e_1 ^ e_2) ^ (-e_3))) # almost last drill self.assertEquals(a < GA.I_inv(), -(e_2 ^ e_3) - (e_3 ^ e_1)) - + # g self.assertEquals((a ^ b) < GA.I_inv(), -((b ^ a) < GA.I_inv())) self.assertEquals((a ^ b) < GA.I_inv(), -(b < (a < GA.I_inv()))) - self.assertEquals((a ^ b) < GA.I_inv(), -((e_2 + e_3) < (-(e_2 ^ e_3) + (e_1 ^ e_3)))) + self.assertEquals((a ^ b) < GA.I_inv(), -((e_2 + e_3) < (-(e_2 ^ e_3) + (e_1 ^ e_3)))) self.assertEquals((a ^ b) < GA.I_inv(), ((e_2 + e_3) < (e_2 ^ e_3)) - ((e_2 + e_3) < (e_1 ^ e_3))) - self.assertEquals((a ^ b) < GA.I_inv(), ((e_2 < (e_2 ^ e_3)) + (e_3 < (e_2 ^ e_3)) - (e_2 < (e_1 ^ e_3)) - (e_3 < (e_1 ^ e_3)))) + self.assertEquals((a ^ b) < GA.I_inv(), + ((e_2 < (e_2 ^ e_3)) + (e_3 < (e_2 ^ e_3)) - (e_2 < (e_1 ^ e_3)) - (e_3 < (e_1 ^ e_3)))) self.assertEquals(e_2 < (e_2 ^ e_3), e_3) self.assertEquals(e_3 < (e_2 ^ e_3), -e_2) self.assertEquals(e_2 < (e_1 ^ e_3), 0) self.assertEquals(e_3 < (e_1 ^ e_3), -e_1) self.assertEquals((a ^ b) < GA.I_inv(), e_3 - e_2 + e_1) - + # h self.assertEquals(a < (b < GA.I_inv()), (a ^ b) < GA.I_inv()) self.assertEquals(a < (b < GA.I_inv()), e_3 - e_2 + e_1) Ga.dual_mode() - - + def test_3_10_1_2(self): """ Compute the cosine of the angle between the following subspaces given on an orthonormal basis of a Euclidean space. """ Ga.dual_mode("Iinv+") - + GA, e_1, e_2, e_3, e_4 = Ga.build('e*1|2|3|4', g='1 0 0 0, 0 1 0 0, 0 0 1 0, 0 0 0 1') alpha = Symbol('alpha', real=True) theta = Symbol('theta', real=True) - + # e_1 and alpha * e_1 A = e_1 B = alpha * e_1 - cosine = (A | B.rev()) / (A.norm() * B.norm()) + cosine = (A | B.rev()) / (A.norm() * B.norm()) self.assertEquals(A | B.rev(), e_1 | (alpha * e_1)) - self.assertEquals(A.norm() * B.norm(), alpha) + self.assertEquals(A.norm() * B.norm(), alpha) self.assertEquals(cosine, (e_1 | (alpha * e_1)) / alpha) self.assertEquals(cosine, (alpha * (e_1 | e_1)) / alpha) self.assertEquals(cosine, 1) - + # (e_1 + e_2) ^ e_3 and e_1 ^ e_3 A = (e_1 + e_2) ^ e_3 B = e_1 ^ e_3 num = A | B.rev() den = A.norm() * B.norm() - cosine = num / den + cosine = num / den self.assertEquals(num, ((e_1 ^ e_3) + (e_2 ^ e_3)) | (e_3 ^ e_1)) self.assertEquals(num, ((e_1 ^ e_3) | (e_3 ^ e_1)) + ((e_2 ^ e_3) | (e_3 ^ e_1))) self.assertEquals(((e_1 ^ e_3) | (e_3 ^ e_1)), 1) self.assertEquals(((e_2 ^ e_3) | (e_3 ^ e_1)), 0) - self.assertEquals(num, 1) - self.assertEquals(den, ((e_1 + e_2) ^ e_3).norm() * (e_1 ^ e_3).norm()) - self.assertEquals(den, ((e_1 ^ e_3) + (e_2 ^ e_3)).norm()) - den2 = ((e_1 ^ e_3) + (e_2 ^ e_3)).norm2() - self.assertEquals(den2, ((e_1 ^ e_3) + (e_2 ^ e_3)) | ((e_1 ^ e_3) + (e_2 ^ e_3)).rev()) - self.assertEquals(den2, ((e_1 ^ e_3) + (e_2 ^ e_3)) | ((e_3 ^ e_1) + (e_3 ^ e_2))) - self.assertEquals(den2, ((e_1 ^ e_3) | (e_3 ^ e_1)) + ((e_1 ^ e_3) | (e_3 ^ e_2)) + ((e_2 ^ e_3) | (e_3 ^ e_1)) + ((e_2 ^ e_3) | (e_3 ^ e_2))) - self.assertEquals(den2, 1 + 0 + 0 + 1) + self.assertEquals(num, 1) + self.assertEquals(den, ((e_1 + e_2) ^ e_3).norm() * (e_1 ^ e_3).norm()) + self.assertEquals(den, ((e_1 ^ e_3) + (e_2 ^ e_3)).norm()) + den2 = ((e_1 ^ e_3) + (e_2 ^ e_3)).norm2() + self.assertEquals(den2, ((e_1 ^ e_3) + (e_2 ^ e_3)) | ((e_1 ^ e_3) + (e_2 ^ e_3)).rev()) + self.assertEquals(den2, ((e_1 ^ e_3) + (e_2 ^ e_3)) | ((e_3 ^ e_1) + (e_3 ^ e_2))) + self.assertEquals(den2, + ((e_1 ^ e_3) | (e_3 ^ e_1)) + ((e_1 ^ e_3) | (e_3 ^ e_2)) + ((e_2 ^ e_3) | (e_3 ^ e_1)) + ( + (e_2 ^ e_3) | (e_3 ^ e_2))) + self.assertEquals(den2, 1 + 0 + 0 + 1) self.assertEquals(cosine, 1 / sqrt(2)) - + # (cos(theta) * e_1 + sin(theta) * e_2) ^ e_3 and e_2 ^ e_3 A = (cos(theta) * e_1 + sin(theta) * e_2) ^ e_3 B = e_2 ^ e_3 num = A | B.rev() den = A.norm() * B.norm() - cosine = num / den + cosine = num / den self.assertEquals(num, (cos(theta) * (e_1 ^ e_3) + sin(theta) * (e_2 ^ e_3)) | (e_3 ^ e_2)) self.assertEquals(num, ((cos(theta) * (e_1 ^ e_3)) | (e_3 ^ e_2)) + ((sin(theta) * (e_2 ^ e_3)) | (e_3 ^ e_2))) self.assertEquals((cos(theta) * (e_1 ^ e_3)) | (e_3 ^ e_2), 0) self.assertEquals((sin(theta) * (e_2 ^ e_3)) | (e_3 ^ e_2), sin(theta)) self.assertEquals(num, sin(theta)) self.assertEquals(den, ((cos(theta) * e_1 + sin(theta) * e_2) ^ e_3).norm() * (e_2 ^ e_3).norm()) - self.assertEquals(den, ((cos(theta) * e_1 + sin(theta) * e_2) ^ e_3).norm()) + self.assertEquals(den, ((cos(theta) * e_1 + sin(theta) * e_2) ^ e_3).norm()) den2 = ((cos(theta) * e_1 + sin(theta) * e_2) ^ e_3).norm2() - self.assertEquals(den2, ((cos(theta) * e_1 + sin(theta) * e_2) ^ e_3) | ((cos(theta) * e_1 + sin(theta) * e_2) ^ e_3).rev()) - self.assertEquals(den2, (cos(theta) * (e_1 ^ e_3) + sin(theta) * (e_2 ^ e_3)) | (cos(theta) * (e_1 ^ e_3) + sin(theta) * (e_2 ^ e_3)).rev()) - self.assertEquals(den2, (cos(theta) * (e_1 ^ e_3) + sin(theta) * (e_2 ^ e_3)) | (cos(theta) * (e_3 ^ e_1) + sin(theta) * (e_3 ^ e_2))) - self.assertEquals(den2, ((cos(theta) * (e_1 ^ e_3)) | (cos(theta) * (e_3 ^ e_1))) + ((cos(theta) * (e_1 ^ e_3)) | (sin(theta) * (e_3 ^ e_2))) + ((sin(theta) * (e_2 ^ e_3)) | (cos(theta) * (e_3 ^ e_1))) + ((sin(theta) * (e_2 ^ e_3)) | (sin(theta) * (e_3 ^ e_2)))) + self.assertEquals(den2, ((cos(theta) * e_1 + sin(theta) * e_2) ^ e_3) | ( + (cos(theta) * e_1 + sin(theta) * e_2) ^ e_3).rev()) + self.assertEquals(den2, (cos(theta) * (e_1 ^ e_3) + sin(theta) * (e_2 ^ e_3)) | ( + cos(theta) * (e_1 ^ e_3) + sin(theta) * (e_2 ^ e_3)).rev()) + self.assertEquals(den2, (cos(theta) * (e_1 ^ e_3) + sin(theta) * (e_2 ^ e_3)) | ( + cos(theta) * (e_3 ^ e_1) + sin(theta) * (e_3 ^ e_2))) + self.assertEquals(den2, ((cos(theta) * (e_1 ^ e_3)) | (cos(theta) * (e_3 ^ e_1))) + ( + (cos(theta) * (e_1 ^ e_3)) | (sin(theta) * (e_3 ^ e_2))) + ( + (sin(theta) * (e_2 ^ e_3)) | (cos(theta) * (e_3 ^ e_1))) + ( + (sin(theta) * (e_2 ^ e_3)) | (sin(theta) * (e_3 ^ e_2)))) self.assertEquals(den2, cos(theta) * cos(theta) + sin(theta) * sin(theta)) - self.assertEquals(den2, 1) + self.assertEquals(den2, 1) self.assertEquals(cosine, sin(theta)) - + # e_1 ^ e_2 and e_3 ^ e_4 A = e_1 ^ e_2 B = e_3 ^ e_4 @@ -361,12 +364,11 @@ def test_3_10_1_2(self): den = A.norm() * B.norm() cosine = num / den self.assertEquals(num, (e_1 ^ e_2) | (e_4 ^ e_3)) - self.assertEquals(num, 0) + self.assertEquals(num, 0) self.assertEquals(cosine, 0) Ga.dual_mode() - def test3_10_2_1(self): """ In 2-D Euclidean space R(2,0) with orthogonal basis {e1, e2}, let us determine the value of the contraction @@ -395,7 +397,6 @@ def test3_10_2_1(self): self.assertEquals(((X ^ A) * B).scalar(), (X * (A < B)).scalar()) self.assertEquals(((X ^ A) * B).scalar(), 0) - def test3_10_2_2(self): """ Change the metric such that e2 . e2 == 0. Show that you can't determine the coefficient of e2 in the value @@ -409,7 +410,7 @@ def test3_10_2_2(self): B = e_1 ^ e_2 X = e_2 self.assertEquals(((X ^ A) * B).scalar(), (X * (A < B)).scalar()) - self.assertEquals(((X ^ A) * B).scalar(), 0) # Which is false + self.assertEquals(((X ^ A) * B).scalar(), 0) # Which is false e_1_grades = list(GA.grade_decomposition(e_1).keys()) self.assertEquals(len(e_1_grades), 1) @@ -419,8 +420,7 @@ def test3_10_2_2(self): self.assertEquals(e_1 < (e_1 ^ e_2), ((e_1 < e_1) ^ e_2) + ((-1) ** e_1_grades[0]) * (e_1 ^ (e_1 < e_2))) # We can't use the definition a < b = a . b using GAlgebra so we solved it ourselves... self.assertEquals(e_1 < (e_1 ^ e_2), (GA.mv(1) ^ e_2) + ((-1) ** e_1_grades[0]) * (e_1 ^ GA.mv(0))) - self.assertEquals(e_1 < (e_1 ^ e_2), e_2) # Which is true - + self.assertEquals(e_1 < (e_1 ^ e_2), e_2) # Which is true def test3_10_2_3(self): """ diff --git a/tests/test_chapter6.py b/tests/test_chapter6.py index f5cdc19a..0d08c607 100644 --- a/tests/test_chapter6.py +++ b/tests/test_chapter6.py @@ -32,7 +32,6 @@ def test6_1_4_1(self): e_23 = e_2 * e_3 self.assertEquals(e_23 * e_23, -1) - def test6_1_4_2(self): """ The geometric product for vectors on a basis. @@ -71,7 +70,6 @@ def test6_1_4_2(self): self.assertEquals(a * b, a_1 * b_1 + a_2 * b_2 + (a_1 * b_2 - a_2 * b_1) * (e_1 ^ e_2)) - def test6_3_1(self): """ The subspace products from symmetry. @@ -103,7 +101,6 @@ def hat(k, M): M = GA.mv('m', 'mv') self.assertEquals(a * M, (a < M) + (a ^ M)) - def test6_3_2(self): """ The subspace products as selected grades. @@ -120,7 +117,6 @@ def test6_3_2(self): self.assertEquals(A > B, 0 if l > k else (A * B).get_grade(k - l)) # TODO: scalar product - def test6_6_2_3(self): """ The outer product can be defined as the completely antisymmetric summed average of all permutations @@ -136,7 +132,7 @@ def hat(M): M_grades = list(GA.grade_decomposition(M).keys()) self.assertEquals(len(M_grades), 1) return ((-1) ** M_grades[0]) * M - + self.assertEquals(x * y * z, ((x < y) + (x ^ y)) * z) self.assertEquals(x * y * z, ((x < y) * z) + ((x ^ y) * z)) self.assertEquals(x * y * z, ((x < y) * z) - (z < hat(x ^ y)) + (z ^ hat(x ^ y))) @@ -180,11 +176,11 @@ def hat(M): self.assertEquals(x < (y ^ z), ((x < y) ^ z) - (y ^ (x < z))) self.assertEquals(y < (z ^ x), ((y < z) ^ x) - (z ^ (y < x))) - self.assertEquals(((z < x) ^ y) - (x ^ (z < y)) + ((x < y) ^ z) - (y ^ (x < z)) + ((y < z) ^ x) - (z ^ (y < x)), 0) + self.assertEquals(((z < x) ^ y) - (x ^ (z < y)) + ((x < y) ^ z) - (y ^ (x < z)) + ((y < z) ^ x) - (z ^ (y < x)), + 0) self.assertEquals(x * y * z - y * x * z + y * z * x - z * y * x + z * x * y - x * z * y, 6 * (x ^ y ^ z)) - def test6_6_2_4(self): """ The parts of a certain grade of a geometric product of blades are not necessarily blades. @@ -221,7 +217,6 @@ def test6_6_2_4(self): result = solve_poly_system(system, unknowns) self.assertTrue(result is None) - def test6_6_2_6(self): """ Prove (X ^ A) * B = X * (A < B) using the grade based definition of ^, * and <. @@ -238,7 +233,6 @@ def test6_6_2_6(self): self.assertTrue((X * (A * B).get_grade(l - k)).get_grade(0) != 0 if j == l - k else True) self.assertEquals(((X * A).get_grade(j + k) * B).get_grade(0), (X * (A * B).get_grade(l - k)).get_grade(0)) - def test6_6_2_7(self): """ In the formula (x < (1/A)) * A, show we can replace the geometric product by a contraction, so that @@ -258,7 +252,6 @@ def test6_6_2_7(self): for k, A in A_grade_and_blades: self.assertEquals((x < A.inv()) * A, (x < A.inv()) < A) - def test6_6_2_9(self): """ In a 4D space with orthonormal basis {e1, e2, e3, e4}, project the 2-blade X = (e1 + e2) ^ (e3 + e4) onto diff --git a/tests/test_chapter8.py b/tests/test_chapter8.py index 8f1d79ba..3f6e056d 100644 --- a/tests/test_chapter8.py +++ b/tests/test_chapter8.py @@ -22,7 +22,7 @@ def test8_2_1(self): B = GA.mv('B', 2, 'blade') X = GA.mv('A', 'mv') self.assertEquals(B.pure_grade(), 2) - self.assertEquals(com(X, B).pure_grade(), -2) # Not pure + self.assertEquals(com(X, B).pure_grade(), -2) # Not pure E = com(X, B).rev() self.assertEquals(E, (B.rev() * X.rev() - X.rev() * B.rev()) / 2) diff --git a/tests/test_generator.py b/tests/test_generator.py index dc29b3c9..f6871402 100644 --- a/tests/test_generator.py +++ b/tests/test_generator.py @@ -9,7 +9,6 @@ class TestGenerator(TestCase): def setUp(self): - Ga.dual_mode("Iinv+") GA = Ga('e*1|2|3|4', g=[0, 1, 1, 1]) GA.build_cobases() @@ -21,7 +20,6 @@ def setUp(self): self.flat_GA = flat_GA def test_add(self): - GA = self.GA flat_GA = self.flat_GA X = GA.mv('x', 'mv') @@ -30,7 +28,6 @@ def test_add(self): self.assertEquals(X + Y, expand(GA, flatten(flat_GA, X) + flatten(flat_GA, Y))) def test_sub(self): - GA = self.GA flat_GA = self.flat_GA X = GA.mv('x', 'mv') @@ -39,7 +36,6 @@ def test_sub(self): self.assertEquals(X - Y, expand(GA, flatten(flat_GA, X) - flatten(flat_GA, Y))) def test_mul(self): - GA = self.GA flat_GA = self.flat_GA X = GA.mv('x', 'mv') @@ -48,7 +44,6 @@ def test_mul(self): self.assertEquals(X * Y, expand(GA, flatten(flat_GA, X) * flatten(flat_GA, Y))) def test_and(self): - GA = self.GA flat_GA = self.flat_GA X = GA.mv('x', 'mv') @@ -57,7 +52,6 @@ def test_and(self): self.assertEquals(Jinv(J(X) ^ J(Y)), expand(GA, flatten(flat_GA, X) & flatten(flat_GA, Y))) def test_xor(self): - GA = self.GA flat_GA = self.flat_GA X = GA.mv('x', 'mv') @@ -66,7 +60,6 @@ def test_xor(self): self.assertEquals(X ^ Y, expand(GA, flatten(flat_GA, X) ^ flatten(flat_GA, Y))) def test_lshift(self): - GA = self.GA flat_GA = self.flat_GA X = GA.mv('x', 'mv') @@ -75,7 +68,6 @@ def test_lshift(self): self.assertEquals(X << Y, expand(GA, flatten(flat_GA, X) << flatten(flat_GA, Y))) def test_rshift(self): - GA = self.GA flat_GA = self.flat_GA X = GA.mv('x', 'mv') @@ -84,7 +76,6 @@ def test_rshift(self): self.assertEquals(X >> Y, expand(GA, flatten(flat_GA, X) >> flatten(flat_GA, Y))) def test_meet_and_join(self): - GA = self.GA flat_GA = self.flat_GA X = GA.mv('x', 'mv') @@ -94,7 +85,6 @@ def test_meet_and_join(self): self.assertEquals(X ^ Y, expand(GA, flatten(flat_GA, X).join(flatten(flat_GA, Y)))) def test_rev(self): - GA = self.GA flat_GA = self.flat_GA X = GA.mv('x', 'mv') diff --git a/tests/test_pga.py b/tests/test_pga.py index 7dbefc1c..0cabb54c 100644 --- a/tests/test_pga.py +++ b/tests/test_pga.py @@ -425,10 +425,10 @@ def test_geometry_incidence_planes_meet_into_points_2(self): p314 = Jinv(J(P3) ^ J(P1) ^ J(P4)) # TODO: find a way to meet planes faster... - #self.assertProjEquals(p123 ^ p124 ^ p314, P1) - #self.assertProjEquals(p123 ^ p124 ^ p234, P2) - #self.assertProjEquals(p123 ^ p234 ^ p314, P3) - #self.assertProjEquals(p124 ^ p234 ^ p314, P4) + # self.assertProjEquals(p123 ^ p124 ^ p314, P1) + # self.assertProjEquals(p123 ^ p124 ^ p234, P2) + # self.assertProjEquals(p123 ^ p234 ^ p314, P3) + # self.assertProjEquals(p124 ^ p234 ^ p314, P4) def test_geometry_incidence_points_join_into_planes(self): """ @@ -536,7 +536,7 @@ def test_metric_distance_between_parallel_planes(self): p1 = self.plane(nx / n_norm, ny / n_norm, nz / n_norm, -x) p2 = self.plane(nx / n_norm, ny / n_norm, nz / n_norm, -y) - self.assertEquals(self.ideal_norm(p1 ^ p2), sqrt((x - y)**2)) + self.assertEquals(self.ideal_norm(p1 ^ p2), sqrt((x - y) ** 2)) def test_metric_oriented_distance_between_point_and_plane(self): """ @@ -552,7 +552,7 @@ def test_metric_oriented_distance_between_point_and_plane(self): self.assertEquals(Jinv(J(p0) ^ J(P0)), x0 - d0) # TODO: We could assume d0 - x0 is not null for simplifying further - self.assertEquals(self.ideal_norm(P0 ^ p0), sqrt((d0 - x0)**2)) + self.assertEquals(self.ideal_norm(P0 ^ p0), sqrt((d0 - x0) ** 2)) def test_metric_oriented_distance_between_point_and_line(self): """ @@ -617,8 +617,8 @@ def test_metric_angle_between_lines(self): P1 = self.point(x1, y1, 0) P2 = self.point(-y1, x1, 0) - l0 = Jinv(J(P0) ^ J(P1)) # TODO: this feels weird... but ganja does the same - l1 = Jinv(J(P2) ^ J(P0)) # + l0 = Jinv(J(P0) ^ J(P1)) # TODO: this feels weird... but ganja does the same + l1 = Jinv(J(P2) ^ J(P0)) # l0 /= self.norm(l0) l1 /= self.norm(l1) @@ -630,11 +630,11 @@ def test_metric_angle_between_lines(self): @staticmethod def rotor_cs(alpha, l): - return cos(-alpha / 2) + sin(-alpha / 2) * l # TODO: this feels weird... + return cos(-alpha / 2) + sin(-alpha / 2) * l # TODO: this feels weird... @staticmethod def rotor_exp(alpha, l): - return (-alpha / 2 * l).exp() # TODO: this feels weird... + return (-alpha / 2 * l).exp() # TODO: this feels weird... def test_motors_rotator(self): """ @@ -793,4 +793,4 @@ def test_motors_translator(self): d = Symbol('d') T = self.translator(d, l) - self.assertProjEquals(T * Px * T.rev(), self.point(x0 + d, y0, z0)) # TODO : like ganja.js but weird... + self.assertProjEquals(T * Px * T.rev(), self.point(x0 + d, y0, z0)) # TODO : like ganja.js but weird... diff --git a/tests/test_utils.py b/tests/test_utils.py index 92aff5f2..f7d0fe96 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -6,7 +6,9 @@ def com(A, B): - """I like free functions...""" + """ + I like free functions... + """ return Ga.com(A, B) @@ -56,4 +58,3 @@ def assertNotEquals(self, first, second): diff = simplify(first - second) self.assertTrue(diff != 0, "\n%s\n!=\n%s\n%s" % (first, second, diff)) - From 1043951b199277c7d0806d541e4ec3a278bbe36e Mon Sep 17 00:00:00 2001 From: Sylvain Meunier Date: Mon, 25 Nov 2019 22:03:11 +0100 Subject: [PATCH 73/78] Use assertEqual, assertNotEqual and assertProjEqual --- tests/test_chapter11.py | 36 +-- tests/test_chapter13.py | 24 +- tests/test_chapter2.py | 98 +++--- tests/test_chapter3.py | 288 ++++++++--------- tests/test_chapter6.py | 192 +++++------ tests/test_chapter7.py | 20 +- tests/test_chapter8.py | 20 +- tests/test_generator.py | 20 +- tests/test_pga.py | 698 ++++++++++++++++++++-------------------- tests/test_utils.py | 6 +- 10 files changed, 701 insertions(+), 701 deletions(-) diff --git a/tests/test_chapter11.py b/tests/test_chapter11.py index b058f263..5914e457 100644 --- a/tests/test_chapter11.py +++ b/tests/test_chapter11.py @@ -30,9 +30,9 @@ def test11_4(self): q = q.subs({q0: 1}) r = r.subs({r0: 1}) - self.assertEquals(p ^ q ^ r, p ^ (q - p) ^ (r - p)) - self.assertEquals(p ^ q ^ r, p ^ (q_inf - p_inf) ^ (r_inf - p_inf)) - self.assertEquals(p ^ q ^ r, ((p + q + r) / 3) ^ ((p ^ q) + (q ^ r) + (r ^ p))) + self.assertEqual(p ^ q ^ r, p ^ (q - p) ^ (r - p)) + self.assertEqual(p ^ q ^ r, p ^ (q_inf - p_inf) ^ (r_inf - p_inf)) + self.assertEqual(p ^ q ^ r, ((p + q + r) / 3) ^ ((p ^ q) + (q ^ r) + (r ^ p))) def test11_6(self): """ @@ -54,8 +54,8 @@ def test11_6(self): Ip_inv = GA.I_inv() Ir = e_0_inv < Ip Ir_inv = Ir.inv() - self.assertEquals(Ip, e_0 ^ Ir) - self.assertEquals(Ip, e_0 * Ir) + self.assertEqual(Ip, e_0 ^ Ir) + self.assertEqual(Ip, e_0 * Ir) p = GA.mv([1] + [Symbol('p%d' % i, real=True) for i in range(1, GA.n)], 'vector') @@ -70,7 +70,7 @@ def test11_6(self): for k in range(1, GA.n): A = reduce(Mv.__xor__, v[:k]) X = (p ^ A) - self.assertNotEquals(X, 0) + self.assertNotEqual(X, 0) M = e_0_inv < (e_0 ^ X) # Very slow @@ -80,13 +80,13 @@ def test11_6(self): def hat(A): return ((-1) ** A.pure_grade()) * A - self.assertEquals(hat(A < Ir_inv), ((-1) ** (GA.n - 1)) * (hat(A) < Ir_inv)) + self.assertEqual(hat(A < Ir_inv), ((-1) ** (GA.n - 1)) * (hat(A) < Ir_inv)) Xd = (p ^ A).dual() - self.assertEquals(Xd, (p ^ A) < Ip_inv) - self.assertEquals(Xd, p < (A < Ip_inv)) - self.assertEquals(Xd, p < ((A < Ir_inv) * e_0_inv)) - self.assertEquals(Xd, hat(A < Ir_inv) - e_0_inv * (p < hat(A < Ir_inv))) + self.assertEqual(Xd, (p ^ A) < Ip_inv) + self.assertEqual(Xd, p < (A < Ip_inv)) + self.assertEqual(Xd, p < ((A < Ir_inv) * e_0_inv)) + self.assertEqual(Xd, hat(A < Ir_inv) - e_0_inv * (p < hat(A < Ir_inv))) # Very slow # self.assertEquals(Xd, hat(A < Ir_inv) + e_0_inv * hat(M < Ir_inv)) # self.assertEquals(Xd, (e_0_inv - d_inv) * hat(M < Ir_inv)) @@ -104,15 +104,15 @@ def test11_12_1(self): p = e_0 + e_1 q = e_0 + e_2 d = e_2 - e_1 - self.assertEquals(p ^ q, p ^ d) - self.assertEquals(p ^ q, q ^ d) + self.assertEqual(p ^ q, p ^ d) + self.assertEqual(p ^ q, q ^ d) r = e_0 + 2 * (e_2 - e_1) e = 2 * (e_2 - e_1) s = e_0 + 3 * (e_2 - e_1) t = 2 * (e_0 + e_2) - self.assertEquals(2 * (p ^ q), p ^ e) - self.assertEquals(2 * (p ^ q), p ^ t) + self.assertEqual(2 * (p ^ q), p ^ e) + self.assertEqual(2 * (p ^ q), p ^ t) def test11_12_2_1(self): """ @@ -124,7 +124,7 @@ def test11_12_2_1(self): L = (e_1 + 2 * e_2 - e_3) ^ (e_0 + e_1 - 3 * e_2) d = (e_0.inv() < (e_0 ^ L)) / (e_0.inv() < L) - self.assertEquals(d.norm(), sqrt(Rational(35, 6))) + self.assertEqual(d.norm(), sqrt(Rational(35, 6))) def test11_12_2_2(self): """ @@ -152,5 +152,5 @@ def test11_12_2_2(self): for i in range(11): t_value = Rational(i, 10) x_value = x_t.subs({t: t_value}) - self.assertEquals(x_value ^ L, 0) - self.assertEquals(t_x.subs(list(zip([x_1, x_2, x_3], x_value.blade_coefs([e_1, e_2, e_3])))), t_value) + self.assertEqual(x_value ^ L, 0) + self.assertEqual(t_x.subs(list(zip([x_1, x_2, x_3], x_value.blade_coefs([e_1, e_2, e_3])))), t_value) diff --git a/tests/test_chapter13.py b/tests/test_chapter13.py index f23efcea..21587899 100644 --- a/tests/test_chapter13.py +++ b/tests/test_chapter13.py @@ -29,8 +29,8 @@ def point(v): p = point(vector(Symbol('px', real=True), Symbol('py', real=True), Symbol('pz', real=True))) q = point(vector(Symbol('qx', real=True), Symbol('qy', real=True), Symbol('qz', real=True))) - self.assertEquals(p | q, -S.Half * q * q + p | q - S.Half * p * p) - self.assertEquals(p | q, -S.Half * (q - p) * (q - p)) + self.assertEqual(p | q, -S.Half * q * q + p | q - S.Half * p * p) + self.assertEqual(p | q, -S.Half * (q - p) * (q - p)) def test_13_1_3(self): """ @@ -55,8 +55,8 @@ def point(alpha, v): alpha = Symbol('alpha', real=True) p = point(alpha, vector(Symbol('px', real=True), Symbol('py', real=True), Symbol('pz', real=True))) - self.assertEquals(p | p, S.Zero) - self.assertEquals(inf | p, -alpha) + self.assertEqual(p | p, S.Zero) + self.assertEqual(inf | p, -alpha) # Dual plane def dual_plane(n, delta): @@ -66,8 +66,8 @@ def dual_plane(n, delta): ny = Symbol('ny', real=True) nz = Symbol('nz', real=True) p = dual_plane(vector(nx, ny, nz), Symbol('delta', real=True)) - self.assertEquals(p | p, nx * nx + ny * ny + nz * nz) - self.assertEquals(inf | p, S.Zero) + self.assertEqual(p | p, nx * nx + ny * ny + nz * nz) + self.assertEqual(inf | p, S.Zero) # Dual sphere def dual_sphere(alpha, c, r): @@ -82,13 +82,13 @@ def dual_im_sphere(alpha, c, r): r = Symbol('r', real=True) c = point(1, vector(cx, cy, cz)) - self.assertEquals(c * c, S.Zero) - self.assertEquals(-inf | c, S.One) + self.assertEqual(c * c, S.Zero) + self.assertEqual(-inf | c, S.One) s = dual_sphere(alpha, c, r) - self.assertEquals(s | s, alpha * alpha * r * r) - self.assertEquals(-inf | s, alpha) + self.assertEqual(s | s, alpha * alpha * r * r) + self.assertEqual(-inf | s, alpha) im_s = dual_im_sphere(alpha, c, r) - self.assertEquals(im_s | im_s, -alpha * alpha * r * r) - self.assertEquals(-inf | im_s, alpha) + self.assertEqual(im_s | im_s, -alpha * alpha * r * r) + self.assertEqual(-inf | im_s, alpha) diff --git a/tests/test_chapter2.py b/tests/test_chapter2.py index 94b3c90d..efd3ac74 100755 --- a/tests/test_chapter2.py +++ b/tests/test_chapter2.py @@ -2,7 +2,7 @@ from functools import reduce from itertools import product -from sympy import Symbol, Matrix, solve, solve_poly_system, cos, sin +from sympy import S, Symbol, Matrix, solve, solve_poly_system, cos, sin from galgebra.ga import Ga from galgebra.mv import Mv @@ -20,10 +20,10 @@ def test2_3_2(self): c = GA.mv('c', 'vector') alpha = Symbol('alpha', real=True) - self.assertEquals(a ^ b, -b ^ a) - self.assertEquals(a ^ (alpha * b), alpha * (a ^ b)) - self.assertEquals(GA.mv(alpha, 'scalar') ^ b, alpha * b) - self.assertEquals(a ^ (b + c), (a ^ b) + (a ^ c)) + self.assertEqual(a ^ b, -b ^ a) + self.assertEqual(a ^ (alpha * b), alpha * (a ^ b)) + self.assertEqual(GA.mv(alpha, 'scalar') ^ b, alpha * b) + self.assertEqual(a ^ (b + c), (a ^ b) + (a ^ c)) def test_2_4_2(self): """ @@ -45,8 +45,8 @@ def test_2_4_2(self): b = GA.mv((b_1, b_2, b_3), 'vector') c = GA.mv((c_1, c_2, c_3), 'vector') - self.assertEquals(a ^ b ^ c, a ^ (b ^ c)) - self.assertEquals(a ^ b ^ c, (a ^ b) ^ c) + self.assertEqual(a ^ b ^ c, a ^ (b ^ c)) + self.assertEqual(a ^ b ^ c, (a ^ b) ^ c) m = Matrix([ [a_1, b_1, c_1], @@ -54,7 +54,7 @@ def test_2_4_2(self): [a_3, b_3, c_3] ]) - self.assertEquals(a ^ b ^ c, m.det() * (e_1 ^ e_2 ^ e_3)) + self.assertEqual(a ^ b ^ c, m.det() * (e_1 ^ e_2 ^ e_3)) def test2_9_1(self): """ @@ -66,20 +66,20 @@ def test2_9_1(self): # Check for k in [0, R.n] for k in range(GA.n + 1): Ak = GA.mv('A', 'blade', k) - self.assertEquals(Ak.pure_grade(), k) + self.assertEqual(Ak.pure_grade(), k) grades = GA.grade_decomposition(Ak) - self.assertEquals(len(grades), 1) - self.assertEquals(list(grades.keys())[0], k) + self.assertEqual(len(grades), 1) + self.assertEqual(list(grades.keys())[0], k) # Check for k and l in [0, R.n] for k, l in product(range(GA.n + 1), range(GA.n + 1)): Ak = GA.mv('A', 'blade', k) Bl = GA.mv('B', 'blade', l) C = Ak ^ Bl - self.assertEquals(C.pure_grade(), 0 if C == 0 else k + l) + self.assertEqual(C.pure_grade(), 0 if C == 0 else k + l) grades = GA.grade_decomposition(C) - self.assertEquals(len(grades), 1) - self.assertEquals(list(grades.keys())[0], 0 if C == 0 else k + l) + self.assertEqual(len(grades), 1) + self.assertEqual(list(grades.keys())[0], 0 if C == 0 else k + l) def test2_9_5(self): """ @@ -91,8 +91,8 @@ def test2_9_5(self): a = [GA.mv('a%d' % i, 'vector') for i in range(k)] A = reduce(Mv.__xor__, a) A_rev = reduce(Mv.__xor__, reversed(a)) - self.assertEquals(A_rev, A.rev()) - self.assertEquals(A_rev, ((-1) ** ((k * (k - 1)) / 2)) * A) + self.assertEqual(A_rev, A.rev()) + self.assertEqual(A_rev, ((-1) ** ((k * (k - 1)) / 2)) * A) def test2_12_1_1(self): """ @@ -100,13 +100,13 @@ def test2_12_1_1(self): giving the result relative to the basis {1, e_1, e_2, e_3, e_1^e_2, e_1^e_3, e_2^e_3, e_1^e_2^e_3}. """ GA, e_1, e_2, e_3 = Ga.build('e*1|2|3') - self.assertTrue((e_1 + e_2) ^ (e_1 + e_3) == (-e_1 ^ e_2) + (e_1 ^ e_3) + (e_2 ^ e_3)) - self.assertTrue((e_1 + e_2 + e_3) ^ (2 * e_1) == -2 * (e_1 ^ e_2) - 2 * (e_1 ^ e_3)) - self.assertTrue((e_1 - e_2) ^ (e_1 - e_3) == (e_1 ^ e_2) - (e_1 ^ e_3) + (e_2 ^ e_3)) - self.assertTrue( - (e_1 + e_2) ^ (0.5 * e_1 + 2 * e_2 + 3 * e_3) == 1.5 * (e_1 ^ e_2) + 3 * (e_1 ^ e_3) + 3 * (e_2 ^ e_3)) - self.assertTrue((e_1 ^ e_2) ^ (e_1 + e_3) == (e_1 ^ e_2 ^ e_3)) - self.assertTrue((e_1 + e_2) ^ ((e_1 ^ e_2) + (e_2 ^ e_3)) == (e_1 ^ e_2 ^ e_3)) + self.assertEqual((e_1 + e_2) ^ (e_1 + e_3), (-e_1 ^ e_2) + (e_1 ^ e_3) + (e_2 ^ e_3)) + self.assertEqual((e_1 + e_2 + e_3) ^ (2 * e_1), -2 * (e_1 ^ e_2) - 2 * (e_1 ^ e_3)) + self.assertEqual((e_1 - e_2) ^ (e_1 - e_3), (e_1 ^ e_2) - (e_1 ^ e_3) + (e_2 ^ e_3)) + self.assertEqual( + (e_1 + e_2) ^ (0.5 * e_1 + 2 * e_2 + 3 * e_3), 1.5 * (e_1 ^ e_2) + 3 * (e_1 ^ e_3) + 3 * (e_2 ^ e_3)) + self.assertEqual((e_1 ^ e_2) ^ (e_1 + e_3), (e_1 ^ e_2 ^ e_3)) + self.assertEqual((e_1 + e_2) ^ ((e_1 ^ e_2) + (e_2 ^ e_3)), (e_1 ^ e_2 ^ e_3)) def test2_12_1_2(self): """ @@ -115,10 +115,10 @@ def test2_12_1_2(self): """ GA, e_1, e_2, e_3 = Ga.build('e*1|2|3') B = e_1 ^ (e_2 - e_3) - self.assertTrue(e_1 ^ B == 0) - self.assertFalse((e_1 + e_2) ^ B == 0) - self.assertFalse((e_1 + e_2 + e_3) ^ B == 0) - self.assertTrue((2 * e_1 - e_2 + e_3) ^ B == 0) + self.assertEqual(e_1 ^ B, 0) + self.assertNotEqual((e_1 + e_2) ^ B, 0) + self.assertNotEqual((e_1 + e_2 + e_3) ^ B, 0) + self.assertEqual((2 * e_1 - e_2 + e_3) ^ B, 0) def test2_12_1_3(self): """ @@ -129,7 +129,7 @@ def test2_12_1_3(self): a = e_1 + 2 * e_2 b = -e_1 - e_2 B = a ^ b - self.assertTrue(B == 1 * (e_1 ^ e_2)) + self.assertEqual(B, 1 * (e_1 ^ e_2)) def test2_12_1_4(self): """ @@ -150,12 +150,12 @@ def test2_12_1_4(self): # Solve the linear system R = solve([L.obj, M.obj], a, b, dict=True) # TODO: fix this... - self.assertTrue(len(R), 1) + self.assertEqual(len(R), 1) # Replace symbols x = x.subs(R[0]) - self.assertEquals(x, e_1 + 2 * e_2) + self.assertEqual(x, e_1 + 2 * e_2) def test2_12_1_5(self): """ @@ -170,7 +170,7 @@ def test2_12_1_5(self): for k, l in product(range(GA.n + 1), range(GA.n + 1)): C += A.get_grade(k) ^ B.get_grade(l) - self.assertTrue(C == (2 * e_1 + 3 * (e_3 ^ e_1) + 2 * (e_2 ^ e_3))) + self.assertEqual(C, (2 * e_1 + 3 * (e_3 ^ e_1) + 2 * (e_2 ^ e_3))) def test2_12_2_1(self): """ @@ -187,17 +187,17 @@ def test2_12_2_1(self): x = a * e_1 y = b * (cos(t) * e_1 + sin(t) * e_2) B = x ^ y - self.assertTrue(B == (a * b * sin(t) * (e_1 ^ e_2))) + self.assertEqual(B, (a * b * sin(t) * (e_1 ^ e_2))) # Retrieve the parallelogram area from the 2-vector area = B.norm() - self.assertTrue(area == (a * b * sin(t))) + self.assertEqual(area, (a * b * sin(t))) # Compute the parallelogram area using the determinant x = [a, 0] y = [b * cos(t), b * sin(t)] area = Matrix([x, y]).det() - self.assertTrue(area == (a * b * sin(t))) + self.assertEqual(area, (a * b * sin(t))) def test2_12_2_2(self): """ @@ -217,7 +217,7 @@ def test2_12_2_2(self): cross = x_1 * y_2 - y_1 * x_2 - self.assertTrue(cross == (a * b * sin(t))) + self.assertEqual(cross, (a * b * sin(t))) def test2_12_2_4(self): """ @@ -244,19 +244,19 @@ def test2_12_2_4(self): x = x_1 * a + x_2 * b + x_3 * c # Solve x_1 - self.assertTrue((x ^ a) == (x_2 * (b ^ a) + x_3 * (c ^ a))) - self.assertTrue((x ^ a ^ b) == x_3 * (c ^ a ^ b)) - self.assertTrue((x ^ a ^ b) * (c ^ a ^ b).inv() == GA.mv(x_3, 'scalar')) + self.assertEqual((x ^ a), (x_2 * (b ^ a) + x_3 * (c ^ a))) + self.assertEqual((x ^ a ^ b), x_3 * (c ^ a ^ b)) + self.assertEqual((x ^ a ^ b) * (c ^ a ^ b).inv(), GA.mv(x_3, 'scalar')) # Solve x_2 - self.assertTrue((x ^ b) == (x_1 * (a ^ b) + x_3 * (c ^ b))) - self.assertTrue((x ^ b ^ c) == x_1 * (a ^ b ^ c)) - self.assertTrue((x ^ b ^ c) * (a ^ b ^ c).inv() == GA.mv(x_1, 'scalar')) + self.assertEqual((x ^ b), (x_1 * (a ^ b) + x_3 * (c ^ b))) + self.assertEqual((x ^ b ^ c), x_1 * (a ^ b ^ c)) + self.assertEqual((x ^ b ^ c) * (a ^ b ^ c).inv(), GA.mv(x_1, 'scalar')) # Solve x_3 - self.assertTrue((x ^ c) == (x_1 * (a ^ c) + x_2 * (b ^ c))) - self.assertTrue((x ^ c ^ a) == x_2 * (b ^ c ^ a)) - self.assertTrue((x ^ c ^ a) * (b ^ c ^ a).inv() == GA.mv(x_2, 'scalar')) + self.assertEqual((x ^ c), (x_1 * (a ^ c) + x_2 * (b ^ c))) + self.assertEqual((x ^ c ^ a), x_2 * (b ^ c ^ a)) + self.assertEqual((x ^ c ^ a) * (b ^ c ^ a).inv(), GA.mv(x_2, 'scalar')) def test2_12_2_5(self): """ @@ -331,10 +331,10 @@ def test2_12_2_6(self): result = solve_poly_system(system, unknowns) result = result[0] - self.assertTrue(result[0] == 0) - self.assertTrue(result[1] == 0) - self.assertTrue(result[2] == 0) - self.assertTrue(result[3] == 0) + self.assertEqual(result[0], S.Zero) + self.assertEqual(result[1], S.Zero) + self.assertEqual(result[2], S.Zero) + self.assertEqual(result[3], S.Zero) def test2_12_2_9(self): """ @@ -344,4 +344,4 @@ def test2_12_2_9(self): for k, l in product(range(GA.n + 1), range(GA.n + 1)): Ak = GA.mv('A', 'blade', k) Bl = GA.mv('B', 'blade', l) - self.assertEquals(Ak ^ Bl, (-1) ** (k * l) * (Bl ^ Ak)) + self.assertEqual(Ak ^ Bl, (-1) ** (k * l) * (Bl ^ Ak)) diff --git a/tests/test_chapter3.py b/tests/test_chapter3.py index 9e98517a..4bd4bd01 100644 --- a/tests/test_chapter3.py +++ b/tests/test_chapter3.py @@ -1,7 +1,7 @@ from .test_utils import TestCase from itertools import product -from sympy import Symbol, cos, sin, sqrt +from sympy import S, Symbol, cos, sin, sqrt from galgebra.ga import Ga from galgebra.mv import cross @@ -19,9 +19,9 @@ def test_3_1_2(self): # GAlgebra doesn't define any scalar product but rely on the geometric product instead for (k, A), (l, B) in product(A_blades, B_blades): if k == l: - self.assertTrue((A * B).scalar() != 0) + self.assertNotEqual((A * B).scalar(), S.Zero) else: - self.assertTrue((A * B).scalar() == 0) + self.assertEqual((A * B).scalar(), S.Zero) def test_3_2_2(self): """ @@ -35,43 +35,43 @@ def test_3_2_2(self): # scalar and blades of various grades k, A = A_blades[0] for l, B in B_blades: - self.assertEquals(A < B, A * B) + self.assertEqual(A < B, A * B) k, A = A_blades[0] for l, B in B_blades: - self.assertEquals(B < A, 0 if l > 0 else (A * B).scalar()) + self.assertEqual(B < A, 0 if l > 0 else (A * B).scalar()) # vectors k, A = A_blades[1] l, B = B_blades[1] - self.assertEquals(A < B, (A * B).scalar()) + self.assertEqual(A < B, (A * B).scalar()) # vector and the outer product of 2 blades of various grades (scalars, vectors, 2-vectors...) k, A = A_blades[1] for (l, B), (m, C) in product(B_blades, C_blades): - self.assertEquals(A < (B ^ C), ((A < B) ^ C) + (-1) ** l * (B ^ (A < C))) + self.assertEqual(A < (B ^ C), ((A < B) ^ C) + (-1) ** l * (B ^ (A < C))) # vector and the outer product of 2 blades of various grades (scalars, vectors, 2-vectors...) for (k, A), (l, B), (m, C) in product(A_blades, B_blades, C_blades): - self.assertEquals((A ^ B) < C, A < (B < C)) + self.assertEqual((A ^ B) < C, A < (B < C)) # distributive properties for (k, A), (l, B), (m, C) in product(A_blades, B_blades, C_blades): - self.assertEquals((A + B) < C, (A < C) + (B < C)) + self.assertEqual((A + B) < C, (A < C) + (B < C)) for (k, A), (l, B), (m, C) in product(A_blades, B_blades, C_blades): - self.assertEquals(A < (B + C), (A < B) + (A < C)) + self.assertEqual(A < (B + C), (A < B) + (A < C)) alpha = Symbol('alpha', real=True) for (k, A), (l, B) in product(A_blades, B_blades): - self.assertEquals((alpha * A) < B, alpha * (A < B)) - self.assertEquals((alpha * A) < B, A < (alpha * B)) + self.assertEqual((alpha * A) < B, alpha * (A < B)) + self.assertEqual((alpha * A) < B, A < (alpha * B)) a = GA.mv('a', 'blade', 1) for (k, A_minus1), (l, B) in product(A_blades[:-1], B_blades): A = A_minus1 ^ a - self.assertEquals(A < B, (A_minus1 ^ a) < B) - self.assertEquals(A < B, A_minus1 < (a < B)) + self.assertEqual(A < B, (A_minus1 ^ a) < B) + self.assertEqual(A < B, A_minus1 < (a < B)) def test_3_4(self): """ @@ -82,12 +82,12 @@ def test_3_4(self): B_grade_and_blades = [(l, GA.mv('B', 'blade', l)) for l in range(GA.n + 1)] for (k, A), (l, B) in product(A_grade_and_blades, B_grade_and_blades): - self.assertEquals(B > A, ((-1) ** (k * (l - 1))) * (A < B)) + self.assertEqual(B > A, ((-1) ** (k * (l - 1))) * (A < B)) for (k, A), (l, B) in product(A_grade_and_blades, B_grade_and_blades): C = B > A C_grades = GA.grade_decomposition(C).keys() - self.assertEquals(len(C_grades), 1) + self.assertEqual(len(C_grades), 1) self.assertTrue(C == 0 or C_grades[0] == l - k) def test_3_5_2(self): @@ -103,14 +103,14 @@ def test_3_5_2(self): rev_A = A.rev() rev_sign = ((-1) ** (k * (k - 1) / 2)) norm2 = A.norm2() - self.assertEquals(rev_A, rev_sign * A) - self.assertEquals(inv_A, rev_A / norm2) + self.assertEqual(rev_A, rev_sign * A) + self.assertEqual(inv_A, rev_A / norm2) # We compute the scalar product using the geometric product - self.assertEquals(inv_A, rev_A / (A * rev_A).scalar()) - self.assertEquals(inv_A, rev_sign * (A / norm2)) + self.assertEqual(inv_A, rev_A / (A * rev_A).scalar()) + self.assertEqual(inv_A, rev_sign * (A / norm2)) for k, A in A_grade_and_blades: - self.assertEquals(A < A.inv(), 1) + self.assertEqual(A < A.inv(), 1) Ga.dual_mode() @@ -127,17 +127,17 @@ def test_3_5_3(self): # dualization for blades, GA in spaces: for A in blades: - self.assertEquals(A.dual(), A < GA.I_inv()) + self.assertEqual(A.dual(), A < GA.I_inv()) # dualization sign for blades, R in spaces: for A in blades: - self.assertEquals(A.dual().dual(), ((-1) ** (GA.n * (GA.n - 1) / 2)) * A) + self.assertEqual(A.dual().dual(), ((-1) ** (GA.n * (GA.n - 1) / 2)) * A) # undualization for blades, R in spaces: for A in blades: - self.assertEquals(A, A.dual() < GA.I()) + self.assertEqual(A, A.dual() < GA.I()) Ga.dual_mode() @@ -152,10 +152,10 @@ def test_3_5_4(self): B_blades = [GA.mv('B', 'blade', l) for l in range(GA.n + 1)] for A, B in product(A_blades, B_blades): - self.assertEquals((A ^ B).dual(), A < B.dual()) + self.assertEqual((A ^ B).dual(), A < B.dual()) for A, B in product(A_blades, B_blades): - self.assertEquals((A < B).dual(), A ^ B.dual()) + self.assertEqual((A < B).dual(), A ^ B.dual()) Ga.dual_mode() @@ -173,11 +173,11 @@ def P(X, B): # a projection should be idempotent for X, B in product(X_blades, B_blades): - self.assertEquals(P(X, B), P(P(X, B), B)) + self.assertEqual(P(X, B), P(P(X, B), B)) # with the contraction for X, B in product(X_blades, B_blades): - self.assertEquals(X < B, P(X, B) < B) + self.assertEqual(X < B, P(X, B) < B) def test3_7_2(self): """ @@ -193,21 +193,21 @@ def test3_7_2(self): B = b < GA.I() # Cross product definition - self.assertEquals(GA.I_inv(), -GA.I()) - self.assertEquals(cross(a, b), (a ^ b).dual()) + self.assertEqual(GA.I_inv(), -GA.I()) + self.assertEqual(cross(a, b), (a ^ b).dual()) # About velocities - self.assertEquals(cross(a, b), (b ^ a) < GA.I()) - self.assertEquals(cross(a, b), (a ^ b) < GA.I_inv()) - self.assertEquals(cross(a, b), -(b ^ a).dual()) - self.assertEquals(cross(a, b), -b < a.dual()) - self.assertEquals(cross(a, b), b < A) + self.assertEqual(cross(a, b), (b ^ a) < GA.I()) + self.assertEqual(cross(a, b), (a ^ b) < GA.I_inv()) + self.assertEqual(cross(a, b), -(b ^ a).dual()) + self.assertEqual(cross(a, b), -b < a.dual()) + self.assertEqual(cross(a, b), b < A) # Intersecting planes - self.assertEquals(cross(a, b), ((A < GA.I_inv()) ^ (B < GA.I_inv())) < GA.I_inv()) - self.assertEquals(cross(a, b), (B < GA.I_inv()) < ((A < GA.I_inv()) < GA.I())) - self.assertEquals(cross(a, b), (B < GA.I_inv()) < A) - self.assertEquals(cross(a, b), B.dual() < A) + self.assertEqual(cross(a, b), ((A < GA.I_inv()) ^ (B < GA.I_inv())) < GA.I_inv()) + self.assertEqual(cross(a, b), (B < GA.I_inv()) < ((A < GA.I_inv()) < GA.I())) + self.assertEqual(cross(a, b), (B < GA.I_inv()) < A) + self.assertEqual(cross(a, b), B.dual() < A) Ga.dual_mode() @@ -224,66 +224,66 @@ def test_3_10_1_1(self): b = e_2 + e_3 # a - self.assertEquals(e_1 < a, (e_1 < e_1) + (e_1 < e_2)) - self.assertEquals(e_1 < e_1, e_1 | e_1) - self.assertEquals(e_1 < e_1, 1) - self.assertEquals(e_1 < e_2, e_1 | e_2) - self.assertEquals(e_1 < e_2, 0) - self.assertEquals(e_1 < a, 1) + self.assertEqual(e_1 < a, (e_1 < e_1) + (e_1 < e_2)) + self.assertEqual(e_1 < e_1, e_1 | e_1) + self.assertEqual(e_1 < e_1, 1) + self.assertEqual(e_1 < e_2, e_1 | e_2) + self.assertEqual(e_1 < e_2, 0) + self.assertEqual(e_1 < a, 1) # b - self.assertEquals(e_1 < (a ^ b), ((e_1 < a) ^ b) - (a ^ (e_1 < b))) - self.assertEquals((e_1 < a) ^ b, b) # reusing drill a - self.assertEquals(e_1 < b, (e_1 < e_2) + (e_1 < e_3)) - self.assertEquals(e_1 < b, 0) - self.assertEquals(e_1 < (a ^ b), b) - self.assertEquals(e_1 < (a ^ b), e_2 + e_3) + self.assertEqual(e_1 < (a ^ b), ((e_1 < a) ^ b) - (a ^ (e_1 < b))) + self.assertEqual((e_1 < a) ^ b, b) # reusing drill a + self.assertEqual(e_1 < b, (e_1 < e_2) + (e_1 < e_3)) + self.assertEqual(e_1 < b, 0) + self.assertEqual(e_1 < (a ^ b), b) + self.assertEqual(e_1 < (a ^ b), e_2 + e_3) # c - self.assertEquals((a ^ b) < e_1, a < (b < e_1)) - self.assertEquals((a ^ b) < e_1, a < ((e_2 < e_1) + (e_3 < e_1))) - self.assertEquals((a ^ b) < e_1, 0) + self.assertEqual((a ^ b) < e_1, a < (b < e_1)) + self.assertEqual((a ^ b) < e_1, a < ((e_2 < e_1) + (e_3 < e_1))) + self.assertEqual((a ^ b) < e_1, 0) # d - self.assertEquals(((2 * a) + b) < (a + b), ((2 * a) < a) + ((2 * a) < b) + (b < a) + (b < b)) - self.assertEquals(((2 * a) + b) < (a + b), ((2 * (a < a)) + (2 * (a < b)) + (b < a) + (b < b))) - self.assertEquals(((2 * a) + b) < (a + b), 4 + 2 + 1 + 2) + self.assertEqual(((2 * a) + b) < (a + b), ((2 * a) < a) + ((2 * a) < b) + (b < a) + (b < b)) + self.assertEqual(((2 * a) + b) < (a + b), ((2 * (a < a)) + (2 * (a < b)) + (b < a) + (b < b))) + self.assertEqual(((2 * a) + b) < (a + b), 4 + 2 + 1 + 2) # e - self.assertEquals(a < (e_1 ^ e_2 ^ e_3), a < ((e_1 ^ e_2) ^ e_3)) - self.assertEquals(a < (e_1 ^ e_2 ^ e_3), ((a < (e_1 ^ e_2)) ^ e_3) + ((e_1 ^ e_2) ^ (a < e_3))) - self.assertEquals(a < (e_1 ^ e_2 ^ e_3), (((e_1 < (e_1 ^ e_2)) + (e_2 < (e_1 ^ e_2))) ^ e_3) + ( + self.assertEqual(a < (e_1 ^ e_2 ^ e_3), a < ((e_1 ^ e_2) ^ e_3)) + self.assertEqual(a < (e_1 ^ e_2 ^ e_3), ((a < (e_1 ^ e_2)) ^ e_3) + ((e_1 ^ e_2) ^ (a < e_3))) + self.assertEqual(a < (e_1 ^ e_2 ^ e_3), (((e_1 < (e_1 ^ e_2)) + (e_2 < (e_1 ^ e_2))) ^ e_3) + ( (e_1 ^ e_2) ^ ((e_1 < e_3) + (e_2 < e_3)))) - self.assertEquals(((e_1 < (e_1 ^ e_2)) + (e_2 < (e_1 ^ e_2))) ^ e_3, - (((e_1 < e_1) ^ e_2) - (e_1 ^ (e_1 < e_2)) + ((e_2 < e_1) ^ e_2) - (e_1 ^ (e_2 < e_2))) ^ e_3) - self.assertEquals(((e_1 < (e_1 ^ e_2)) + (e_2 < (e_1 ^ e_2))) ^ e_3, (e_2 - e_1) ^ e_3) - self.assertEquals(((e_1 < (e_1 ^ e_2)) + (e_2 < (e_1 ^ e_2))) ^ e_3, (e_2 ^ e_3) - (e_1 ^ e_3)) - self.assertEquals(e_1 < e_3, 0) - self.assertEquals(e_2 < e_3, 0) - self.assertEquals(((e_1 ^ e_2) ^ ((e_1 < e_3) + (e_2 < e_3))), 0) - self.assertEquals(a < (e_1 ^ e_2 ^ e_3), (e_2 ^ e_3) + (e_3 ^ e_1)) + self.assertEqual(((e_1 < (e_1 ^ e_2)) + (e_2 < (e_1 ^ e_2))) ^ e_3, + (((e_1 < e_1) ^ e_2) - (e_1 ^ (e_1 < e_2)) + ((e_2 < e_1) ^ e_2) - (e_1 ^ (e_2 < e_2))) ^ e_3) + self.assertEqual(((e_1 < (e_1 ^ e_2)) + (e_2 < (e_1 ^ e_2))) ^ e_3, (e_2 - e_1) ^ e_3) + self.assertEqual(((e_1 < (e_1 ^ e_2)) + (e_2 < (e_1 ^ e_2))) ^ e_3, (e_2 ^ e_3) - (e_1 ^ e_3)) + self.assertEqual(e_1 < e_3, 0) + self.assertEqual(e_2 < e_3, 0) + self.assertEqual(((e_1 ^ e_2) ^ ((e_1 < e_3) + (e_2 < e_3))), 0) + self.assertEqual(a < (e_1 ^ e_2 ^ e_3), (e_2 ^ e_3) + (e_3 ^ e_1)) # f - self.assertEquals(a < GA.I_inv(), a < (-e_1 ^ e_2 ^ e_3)) # equals because we fixed the metric - self.assertEquals(a < GA.I_inv(), a < ((e_1 ^ e_2) ^ (-e_3))) # almost last drill - self.assertEquals(a < GA.I_inv(), -(e_2 ^ e_3) - (e_3 ^ e_1)) + self.assertEqual(a < GA.I_inv(), a < (-e_1 ^ e_2 ^ e_3)) # equals because we fixed the metric + self.assertEqual(a < GA.I_inv(), a < ((e_1 ^ e_2) ^ (-e_3))) # almost last drill + self.assertEqual(a < GA.I_inv(), -(e_2 ^ e_3) - (e_3 ^ e_1)) # g - self.assertEquals((a ^ b) < GA.I_inv(), -((b ^ a) < GA.I_inv())) - self.assertEquals((a ^ b) < GA.I_inv(), -(b < (a < GA.I_inv()))) - self.assertEquals((a ^ b) < GA.I_inv(), -((e_2 + e_3) < (-(e_2 ^ e_3) + (e_1 ^ e_3)))) - self.assertEquals((a ^ b) < GA.I_inv(), ((e_2 + e_3) < (e_2 ^ e_3)) - ((e_2 + e_3) < (e_1 ^ e_3))) - self.assertEquals((a ^ b) < GA.I_inv(), - ((e_2 < (e_2 ^ e_3)) + (e_3 < (e_2 ^ e_3)) - (e_2 < (e_1 ^ e_3)) - (e_3 < (e_1 ^ e_3)))) - self.assertEquals(e_2 < (e_2 ^ e_3), e_3) - self.assertEquals(e_3 < (e_2 ^ e_3), -e_2) - self.assertEquals(e_2 < (e_1 ^ e_3), 0) - self.assertEquals(e_3 < (e_1 ^ e_3), -e_1) - self.assertEquals((a ^ b) < GA.I_inv(), e_3 - e_2 + e_1) + self.assertEqual((a ^ b) < GA.I_inv(), -((b ^ a) < GA.I_inv())) + self.assertEqual((a ^ b) < GA.I_inv(), -(b < (a < GA.I_inv()))) + self.assertEqual((a ^ b) < GA.I_inv(), -((e_2 + e_3) < (-(e_2 ^ e_3) + (e_1 ^ e_3)))) + self.assertEqual((a ^ b) < GA.I_inv(), ((e_2 + e_3) < (e_2 ^ e_3)) - ((e_2 + e_3) < (e_1 ^ e_3))) + self.assertEqual((a ^ b) < GA.I_inv(), + ((e_2 < (e_2 ^ e_3)) + (e_3 < (e_2 ^ e_3)) - (e_2 < (e_1 ^ e_3)) - (e_3 < (e_1 ^ e_3)))) + self.assertEqual(e_2 < (e_2 ^ e_3), e_3) + self.assertEqual(e_3 < (e_2 ^ e_3), -e_2) + self.assertEqual(e_2 < (e_1 ^ e_3), 0) + self.assertEqual(e_3 < (e_1 ^ e_3), -e_1) + self.assertEqual((a ^ b) < GA.I_inv(), e_3 - e_2 + e_1) # h - self.assertEquals(a < (b < GA.I_inv()), (a ^ b) < GA.I_inv()) - self.assertEquals(a < (b < GA.I_inv()), e_3 - e_2 + e_1) + self.assertEqual(a < (b < GA.I_inv()), (a ^ b) < GA.I_inv()) + self.assertEqual(a < (b < GA.I_inv()), e_3 - e_2 + e_1) Ga.dual_mode() @@ -301,11 +301,11 @@ def test_3_10_1_2(self): A = e_1 B = alpha * e_1 cosine = (A | B.rev()) / (A.norm() * B.norm()) - self.assertEquals(A | B.rev(), e_1 | (alpha * e_1)) - self.assertEquals(A.norm() * B.norm(), alpha) - self.assertEquals(cosine, (e_1 | (alpha * e_1)) / alpha) - self.assertEquals(cosine, (alpha * (e_1 | e_1)) / alpha) - self.assertEquals(cosine, 1) + self.assertEqual(A | B.rev(), e_1 | (alpha * e_1)) + self.assertEqual(A.norm() * B.norm(), alpha) + self.assertEqual(cosine, (e_1 | (alpha * e_1)) / alpha) + self.assertEqual(cosine, (alpha * (e_1 | e_1)) / alpha) + self.assertEqual(cosine, 1) # (e_1 + e_2) ^ e_3 and e_1 ^ e_3 A = (e_1 + e_2) ^ e_3 @@ -313,21 +313,21 @@ def test_3_10_1_2(self): num = A | B.rev() den = A.norm() * B.norm() cosine = num / den - self.assertEquals(num, ((e_1 ^ e_3) + (e_2 ^ e_3)) | (e_3 ^ e_1)) - self.assertEquals(num, ((e_1 ^ e_3) | (e_3 ^ e_1)) + ((e_2 ^ e_3) | (e_3 ^ e_1))) - self.assertEquals(((e_1 ^ e_3) | (e_3 ^ e_1)), 1) - self.assertEquals(((e_2 ^ e_3) | (e_3 ^ e_1)), 0) - self.assertEquals(num, 1) - self.assertEquals(den, ((e_1 + e_2) ^ e_3).norm() * (e_1 ^ e_3).norm()) - self.assertEquals(den, ((e_1 ^ e_3) + (e_2 ^ e_3)).norm()) + self.assertEqual(num, ((e_1 ^ e_3) + (e_2 ^ e_3)) | (e_3 ^ e_1)) + self.assertEqual(num, ((e_1 ^ e_3) | (e_3 ^ e_1)) + ((e_2 ^ e_3) | (e_3 ^ e_1))) + self.assertEqual(((e_1 ^ e_3) | (e_3 ^ e_1)), 1) + self.assertEqual(((e_2 ^ e_3) | (e_3 ^ e_1)), 0) + self.assertEqual(num, 1) + self.assertEqual(den, ((e_1 + e_2) ^ e_3).norm() * (e_1 ^ e_3).norm()) + self.assertEqual(den, ((e_1 ^ e_3) + (e_2 ^ e_3)).norm()) den2 = ((e_1 ^ e_3) + (e_2 ^ e_3)).norm2() - self.assertEquals(den2, ((e_1 ^ e_3) + (e_2 ^ e_3)) | ((e_1 ^ e_3) + (e_2 ^ e_3)).rev()) - self.assertEquals(den2, ((e_1 ^ e_3) + (e_2 ^ e_3)) | ((e_3 ^ e_1) + (e_3 ^ e_2))) - self.assertEquals(den2, - ((e_1 ^ e_3) | (e_3 ^ e_1)) + ((e_1 ^ e_3) | (e_3 ^ e_2)) + ((e_2 ^ e_3) | (e_3 ^ e_1)) + ( + self.assertEqual(den2, ((e_1 ^ e_3) + (e_2 ^ e_3)) | ((e_1 ^ e_3) + (e_2 ^ e_3)).rev()) + self.assertEqual(den2, ((e_1 ^ e_3) + (e_2 ^ e_3)) | ((e_3 ^ e_1) + (e_3 ^ e_2))) + self.assertEqual(den2, + ((e_1 ^ e_3) | (e_3 ^ e_1)) + ((e_1 ^ e_3) | (e_3 ^ e_2)) + ((e_2 ^ e_3) | (e_3 ^ e_1)) + ( (e_2 ^ e_3) | (e_3 ^ e_2))) - self.assertEquals(den2, 1 + 0 + 0 + 1) - self.assertEquals(cosine, 1 / sqrt(2)) + self.assertEqual(den2, 1 + 0 + 0 + 1) + self.assertEqual(cosine, 1 / sqrt(2)) # (cos(theta) * e_1 + sin(theta) * e_2) ^ e_3 and e_2 ^ e_3 A = (cos(theta) * e_1 + sin(theta) * e_2) ^ e_3 @@ -335,27 +335,27 @@ def test_3_10_1_2(self): num = A | B.rev() den = A.norm() * B.norm() cosine = num / den - self.assertEquals(num, (cos(theta) * (e_1 ^ e_3) + sin(theta) * (e_2 ^ e_3)) | (e_3 ^ e_2)) - self.assertEquals(num, ((cos(theta) * (e_1 ^ e_3)) | (e_3 ^ e_2)) + ((sin(theta) * (e_2 ^ e_3)) | (e_3 ^ e_2))) - self.assertEquals((cos(theta) * (e_1 ^ e_3)) | (e_3 ^ e_2), 0) - self.assertEquals((sin(theta) * (e_2 ^ e_3)) | (e_3 ^ e_2), sin(theta)) - self.assertEquals(num, sin(theta)) - self.assertEquals(den, ((cos(theta) * e_1 + sin(theta) * e_2) ^ e_3).norm() * (e_2 ^ e_3).norm()) - self.assertEquals(den, ((cos(theta) * e_1 + sin(theta) * e_2) ^ e_3).norm()) + self.assertEqual(num, (cos(theta) * (e_1 ^ e_3) + sin(theta) * (e_2 ^ e_3)) | (e_3 ^ e_2)) + self.assertEqual(num, ((cos(theta) * (e_1 ^ e_3)) | (e_3 ^ e_2)) + ((sin(theta) * (e_2 ^ e_3)) | (e_3 ^ e_2))) + self.assertEqual((cos(theta) * (e_1 ^ e_3)) | (e_3 ^ e_2), 0) + self.assertEqual((sin(theta) * (e_2 ^ e_3)) | (e_3 ^ e_2), sin(theta)) + self.assertEqual(num, sin(theta)) + self.assertEqual(den, ((cos(theta) * e_1 + sin(theta) * e_2) ^ e_3).norm() * (e_2 ^ e_3).norm()) + self.assertEqual(den, ((cos(theta) * e_1 + sin(theta) * e_2) ^ e_3).norm()) den2 = ((cos(theta) * e_1 + sin(theta) * e_2) ^ e_3).norm2() - self.assertEquals(den2, ((cos(theta) * e_1 + sin(theta) * e_2) ^ e_3) | ( + self.assertEqual(den2, ((cos(theta) * e_1 + sin(theta) * e_2) ^ e_3) | ( (cos(theta) * e_1 + sin(theta) * e_2) ^ e_3).rev()) - self.assertEquals(den2, (cos(theta) * (e_1 ^ e_3) + sin(theta) * (e_2 ^ e_3)) | ( + self.assertEqual(den2, (cos(theta) * (e_1 ^ e_3) + sin(theta) * (e_2 ^ e_3)) | ( cos(theta) * (e_1 ^ e_3) + sin(theta) * (e_2 ^ e_3)).rev()) - self.assertEquals(den2, (cos(theta) * (e_1 ^ e_3) + sin(theta) * (e_2 ^ e_3)) | ( + self.assertEqual(den2, (cos(theta) * (e_1 ^ e_3) + sin(theta) * (e_2 ^ e_3)) | ( cos(theta) * (e_3 ^ e_1) + sin(theta) * (e_3 ^ e_2))) - self.assertEquals(den2, ((cos(theta) * (e_1 ^ e_3)) | (cos(theta) * (e_3 ^ e_1))) + ( + self.assertEqual(den2, ((cos(theta) * (e_1 ^ e_3)) | (cos(theta) * (e_3 ^ e_1))) + ( (cos(theta) * (e_1 ^ e_3)) | (sin(theta) * (e_3 ^ e_2))) + ( (sin(theta) * (e_2 ^ e_3)) | (cos(theta) * (e_3 ^ e_1))) + ( (sin(theta) * (e_2 ^ e_3)) | (sin(theta) * (e_3 ^ e_2)))) - self.assertEquals(den2, cos(theta) * cos(theta) + sin(theta) * sin(theta)) - self.assertEquals(den2, 1) - self.assertEquals(cosine, sin(theta)) + self.assertEqual(den2, cos(theta) * cos(theta) + sin(theta) * sin(theta)) + self.assertEqual(den2, 1) + self.assertEqual(cosine, sin(theta)) # e_1 ^ e_2 and e_3 ^ e_4 A = e_1 ^ e_2 @@ -363,9 +363,9 @@ def test_3_10_1_2(self): num = A | B.rev() den = A.norm() * B.norm() cosine = num / den - self.assertEquals(num, (e_1 ^ e_2) | (e_4 ^ e_3)) - self.assertEquals(num, 0) - self.assertEquals(cosine, 0) + self.assertEqual(num, (e_1 ^ e_2) | (e_4 ^ e_3)) + self.assertEqual(num, 0) + self.assertEqual(cosine, 0) Ga.dual_mode() @@ -382,20 +382,20 @@ def test3_10_2_1(self): B = e_1 ^ e_2 X = GA.mv(1) - self.assertEquals(((X ^ A) * B).scalar(), (X * (A < B)).scalar()) - self.assertEquals(((X ^ A) * B).scalar(), 0) + self.assertEqual(((X ^ A) * B).scalar(), (X * (A < B)).scalar()) + self.assertEqual(((X ^ A) * B).scalar(), 0) X = e_1 - self.assertEquals(((X ^ A) * B).scalar(), (X * (A < B)).scalar()) - self.assertEquals(((X ^ A) * B).scalar(), 0) + self.assertEqual(((X ^ A) * B).scalar(), (X * (A < B)).scalar()) + self.assertEqual(((X ^ A) * B).scalar(), 0) X = e_2 - self.assertEquals(((X ^ A) * B).scalar(), (X * (A < B)).scalar()) - self.assertEquals(((X ^ A) * B).scalar(), 1) + self.assertEqual(((X ^ A) * B).scalar(), (X * (A < B)).scalar()) + self.assertEqual(((X ^ A) * B).scalar(), 1) X = e_1 ^ e_2 - self.assertEquals(((X ^ A) * B).scalar(), (X * (A < B)).scalar()) - self.assertEquals(((X ^ A) * B).scalar(), 0) + self.assertEqual(((X ^ A) * B).scalar(), (X * (A < B)).scalar()) + self.assertEqual(((X ^ A) * B).scalar(), 0) def test3_10_2_2(self): """ @@ -409,18 +409,18 @@ def test3_10_2_2(self): A = e_1 B = e_1 ^ e_2 X = e_2 - self.assertEquals(((X ^ A) * B).scalar(), (X * (A < B)).scalar()) - self.assertEquals(((X ^ A) * B).scalar(), 0) # Which is false + self.assertEqual(((X ^ A) * B).scalar(), (X * (A < B)).scalar()) + self.assertEqual(((X ^ A) * B).scalar(), 0) # Which is false e_1_grades = list(GA.grade_decomposition(e_1).keys()) - self.assertEquals(len(e_1_grades), 1) - self.assertEquals(e_1_grades[0], 1) + self.assertEqual(len(e_1_grades), 1) + self.assertEqual(e_1_grades[0], 1) # We use the explicit definition of the contraction instead - self.assertEquals(e_1 < (e_1 ^ e_2), ((e_1 < e_1) ^ e_2) + ((-1) ** e_1_grades[0]) * (e_1 ^ (e_1 < e_2))) + self.assertEqual(e_1 < (e_1 ^ e_2), ((e_1 < e_1) ^ e_2) + ((-1) ** e_1_grades[0]) * (e_1 ^ (e_1 < e_2))) # We can't use the definition a < b = a . b using GAlgebra so we solved it ourselves... - self.assertEquals(e_1 < (e_1 ^ e_2), (GA.mv(1) ^ e_2) + ((-1) ** e_1_grades[0]) * (e_1 ^ GA.mv(0))) - self.assertEquals(e_1 < (e_1 ^ e_2), e_2) # Which is true + self.assertEqual(e_1 < (e_1 ^ e_2), (GA.mv(1) ^ e_2) + ((-1) ** e_1_grades[0]) * (e_1 ^ GA.mv(0))) + self.assertEqual(e_1 < (e_1 ^ e_2), e_2) # Which is true def test3_10_2_3(self): """ @@ -435,10 +435,10 @@ def test3_10_2_3(self): for A, B, C in product(A_blades, B_blades, C_blades): M = C > (B ^ A) - self.assertEquals(M, ((A.rev() ^ B.rev()) < C.rev()).rev()) - self.assertEquals(M, (A.rev() < (B.rev() < C.rev())).rev()) - self.assertEquals(M, (B.rev() < C.rev()).rev() > A) - self.assertEquals(M, (C > B) > A) + self.assertEqual(M, ((A.rev() ^ B.rev()) < C.rev()).rev()) + self.assertEqual(M, (A.rev() < (B.rev() < C.rev())).rev()) + self.assertEqual(M, (B.rev() < C.rev()).rev() > A) + self.assertEqual(M, (C > B) > A) M = C > (B > A) # TODO @@ -456,13 +456,13 @@ def test3_10_2_11(self): c = GA.mv('c', 'vector') xx = cross(a, cross(b, c)) - self.assertEquals(xx, (a ^ (b ^ c).dual()).dual()) - self.assertEquals(xx, a < (b ^ c).dual().dual()) - self.assertEquals(xx, -a < (b ^ c)) - self.assertEquals(xx, ((-a < b) ^ c) - (b ^ (-a < c))) - self.assertEquals(xx, (b ^ (a < c)) - (c ^ (a < b))) + self.assertEqual(xx, (a ^ (b ^ c).dual()).dual()) + self.assertEqual(xx, a < (b ^ c).dual().dual()) + self.assertEqual(xx, -a < (b ^ c)) + self.assertEqual(xx, ((-a < b) ^ c) - (b ^ (-a < c))) + self.assertEqual(xx, (b ^ (a < c)) - (c ^ (a < b))) self.assertTrue((a < c).is_scalar()) self.assertTrue((a < b).is_scalar()) - self.assertEquals(xx, b * (a < c) - c * (a < b)) + self.assertEqual(xx, b * (a < c) - c * (a < b)) Ga.dual_mode() diff --git a/tests/test_chapter6.py b/tests/test_chapter6.py index 0d08c607..8ecd238e 100644 --- a/tests/test_chapter6.py +++ b/tests/test_chapter6.py @@ -13,24 +13,24 @@ def test6_1_4_1(self): """ GA, e_1, e_2, e_3 = Ga.build('e*1|2|3', g='1 0 0, 0 1 0, 0 0 1') - self.assertEquals(e_1 * e_1, 1) - self.assertEquals(e_1 * e_2, e_1 ^ e_2) - self.assertEquals(e_1 * e_3, e_1 ^ e_3) - self.assertEquals(e_2 * e_1, -e_1 ^ e_2) - self.assertEquals(e_2 * e_2, 1) - self.assertEquals(e_2 * e_3, e_2 ^ e_3) - self.assertEquals(e_3 * e_1, -e_1 ^ e_3) - self.assertEquals(e_3 * e_2, -e_2 ^ e_3) - self.assertEquals(e_3 * e_3, 1) + self.assertEqual(e_1 * e_1, 1) + self.assertEqual(e_1 * e_2, e_1 ^ e_2) + self.assertEqual(e_1 * e_3, e_1 ^ e_3) + self.assertEqual(e_2 * e_1, -e_1 ^ e_2) + self.assertEqual(e_2 * e_2, 1) + self.assertEqual(e_2 * e_3, e_2 ^ e_3) + self.assertEqual(e_3 * e_1, -e_1 ^ e_3) + self.assertEqual(e_3 * e_2, -e_2 ^ e_3) + self.assertEqual(e_3 * e_3, 1) e_12 = e_1 * e_2 - self.assertEquals(e_12 * e_12, -1) + self.assertEqual(e_12 * e_12, -1) e_13 = e_1 * e_3 - self.assertEquals(e_13 * e_13, -1) + self.assertEqual(e_13 * e_13, -1) e_23 = e_2 * e_3 - self.assertEquals(e_23 * e_23, -1) + self.assertEqual(e_23 * e_23, -1) def test6_1_4_2(self): """ @@ -40,25 +40,25 @@ def test6_1_4_2(self): ONE = GA.mv(1, 'scalar') e_12 = e_1 ^ e_2 - self.assertEquals(ONE * ONE, 1) - self.assertEquals(ONE * e_1, e_1) - self.assertEquals(ONE * e_2, e_2) - self.assertEquals(ONE * e_12, e_12) + self.assertEqual(ONE * ONE, 1) + self.assertEqual(ONE * e_1, e_1) + self.assertEqual(ONE * e_2, e_2) + self.assertEqual(ONE * e_12, e_12) - self.assertEquals(e_1 * ONE, e_1) - self.assertEquals(e_1 * e_1, 1) - self.assertEquals(e_1 * e_2, e_12) - self.assertEquals(e_1 * e_12, e_2) + self.assertEqual(e_1 * ONE, e_1) + self.assertEqual(e_1 * e_1, 1) + self.assertEqual(e_1 * e_2, e_12) + self.assertEqual(e_1 * e_12, e_2) - self.assertEquals(e_2 * ONE, e_2) - self.assertEquals(e_2 * e_1, -e_12) - self.assertEquals(e_2 * e_2, 1) - self.assertEquals(e_2 * e_12, -e_1) + self.assertEqual(e_2 * ONE, e_2) + self.assertEqual(e_2 * e_1, -e_12) + self.assertEqual(e_2 * e_2, 1) + self.assertEqual(e_2 * e_12, -e_1) - self.assertEquals(e_12 * ONE, e_12) - self.assertEquals(e_12 * e_1, -e_2) - self.assertEquals(e_12 * e_2, e_1) - self.assertEquals(e_12 * e_12, -1) + self.assertEqual(e_12 * ONE, e_12) + self.assertEqual(e_12 * e_1, -e_2) + self.assertEqual(e_12 * e_2, e_1) + self.assertEqual(e_12 * e_12, -1) a_1 = Symbol('a_1', real=True) a_2 = Symbol('a_2', real=True) @@ -68,7 +68,7 @@ def test6_1_4_2(self): b_2 = Symbol('b_2', real=True) b = GA.mv((b_1, b_2), 'vector') - self.assertEquals(a * b, a_1 * b_1 + a_2 * b_2 + (a_1 * b_2 - a_2 * b_1) * (e_1 ^ e_2)) + self.assertEqual(a * b, a_1 * b_1 + a_2 * b_2 + (a_1 * b_2 - a_2 * b_1) * (e_1 ^ e_2)) def test6_3_1(self): """ @@ -82,24 +82,24 @@ def hat(k, M): return ((-1) ** k) * M for k, B in B_grade_and_blades: - self.assertEquals(a ^ B, 0.5 * (a * B + hat(k, B) * a)) - self.assertEquals(B ^ a, 0.5 * (B * a + a * hat(k, B))) - self.assertEquals(a < B, 0.5 * (a * B - hat(k, B) * a)) - self.assertEquals(B > a, 0.5 * (B * a - a * hat(k, B))) - self.assertEquals(a * B, (a < B) + (a ^ B)) + self.assertEqual(a ^ B, 0.5 * (a * B + hat(k, B) * a)) + self.assertEqual(B ^ a, 0.5 * (B * a + a * hat(k, B))) + self.assertEqual(a < B, 0.5 * (a * B - hat(k, B) * a)) + self.assertEqual(B > a, 0.5 * (B * a - a * hat(k, B))) + self.assertEqual(a * B, (a < B) + (a ^ B)) for k, B in B_grade_and_blades: - self.assertEquals(hat(k, B) * a, (hat(k, B) > a) + (hat(k, B) ^ a)) - self.assertEquals(hat(k, B) * a, -(a < B) + (a ^ B)) - self.assertEquals(hat(k, B) * a, a * B - 2 * (a < B)) - self.assertEquals(hat(k, B) * a, -(a * B) + 2 * (a ^ B)) + self.assertEqual(hat(k, B) * a, (hat(k, B) > a) + (hat(k, B) ^ a)) + self.assertEqual(hat(k, B) * a, -(a < B) + (a ^ B)) + self.assertEqual(hat(k, B) * a, a * B - 2 * (a < B)) + self.assertEqual(hat(k, B) * a, -(a * B) + 2 * (a ^ B)) for k, B in B_grade_and_blades: - self.assertEquals(a * B, hat(k, B) * a + 2 * (a < B)) - self.assertEquals(a * B, -hat(k, B) * a + 2 * (a ^ B)) + self.assertEqual(a * B, hat(k, B) * a + 2 * (a < B)) + self.assertEqual(a * B, -hat(k, B) * a + 2 * (a ^ B)) M = GA.mv('m', 'mv') - self.assertEquals(a * M, (a < M) + (a ^ M)) + self.assertEqual(a * M, (a < M) + (a ^ M)) def test6_3_2(self): """ @@ -112,9 +112,9 @@ def test6_3_2(self): B_grade_and_blades = [(l, GA.mv('B%d' % l, 'grade', l)) for l in range(GA.n + 1)] for (k, A), (l, B) in product(A_grade_and_blades, B_grade_and_blades): - self.assertEquals(A ^ B, (A * B).get_grade(k + l)) - self.assertEquals(A < B, 0 if k > l else (A * B).get_grade(l - k)) - self.assertEquals(A > B, 0 if l > k else (A * B).get_grade(k - l)) + self.assertEqual(A ^ B, (A * B).get_grade(k + l)) + self.assertEqual(A < B, 0 if k > l else (A * B).get_grade(l - k)) + self.assertEqual(A > B, 0 if l > k else (A * B).get_grade(k - l)) # TODO: scalar product def test6_6_2_3(self): @@ -130,56 +130,56 @@ def test6_6_2_3(self): def hat(M): M_grades = list(GA.grade_decomposition(M).keys()) - self.assertEquals(len(M_grades), 1) + self.assertEqual(len(M_grades), 1) return ((-1) ** M_grades[0]) * M - self.assertEquals(x * y * z, ((x < y) + (x ^ y)) * z) - self.assertEquals(x * y * z, ((x < y) * z) + ((x ^ y) * z)) - self.assertEquals(x * y * z, ((x < y) * z) - (z < hat(x ^ y)) + (z ^ hat(x ^ y))) - self.assertEquals(hat(x ^ y), x ^ y) - self.assertEquals(x * y * z, ((x < y) * z) - (z < (x ^ y)) + (z ^ x ^ y)) - self.assertEquals(x * y * z, ((x < y) * z) - (z < (x ^ y)) + (x ^ y ^ z)) - - self.assertEquals(y * x * z, ((y < x) + (y ^ x)) * z) - self.assertEquals(y * x * z, ((y < x) * z) + ((y ^ x) * z)) - self.assertEquals(y * x * z, ((y < x) * z) - (z < hat(y ^ x)) + (z ^ hat(y ^ x))) - self.assertEquals(hat(y ^ x), y ^ x) - self.assertEquals(y * x * z, ((y < x) * z) - (z < (y ^ x)) + (z ^ y ^ x)) - self.assertEquals(y * x * z, ((x < y) * z) + (z < (x ^ y)) - (x ^ y ^ z)) - - self.assertEquals(y * z * x, ((y < z) + (y ^ z)) * x) - self.assertEquals(y * z * x, ((y < z) * x) - (x < hat(y ^ z)) + (x ^ hat(y ^ z))) - self.assertEquals(y * z * x, ((y < z) * x) - (x < (y ^ z)) + (x ^ y ^ z)) - - self.assertEquals(z * y * x, ((z < y) + (z ^ y)) * x) - self.assertEquals(z * y * x, ((z < y) * x) - (x < hat(z ^ y)) + (x ^ hat(z ^ y))) - self.assertEquals(z * y * x, ((z < y) * x) - (x < (z ^ y)) - (x ^ y ^ z)) - self.assertEquals(z * y * x, ((y < z) * x) + (x < (y ^ z)) - (x ^ y ^ z)) - - self.assertEquals(z * x * y, ((z < x) + (z ^ x)) * y) - self.assertEquals(z * x * y, ((z < x) * y) + ((z ^ x) * y)) - self.assertEquals(z * x * y, ((z < x) * y) - (y < hat(z ^ x)) + (y ^ hat(z ^ x))) - self.assertEquals(z * x * y, ((z < x) * y) - (y < (z ^ x)) + (y ^ z ^ x)) - self.assertEquals(z * x * y, ((z < x) * y) - (y < (z ^ x)) + (x ^ y ^ z)) - - self.assertEquals(x * z * y, ((x < z) + (x ^ z)) * y) - self.assertEquals(x * z * y, ((x < z) * y) + ((x ^ z) * y)) - self.assertEquals(x * z * y, ((x < z) * y) - (y < hat(x ^ z)) + (y ^ hat(x ^ z))) - self.assertEquals(x * z * y, ((x < z) * y) - (y < (x ^ z)) + (y ^ x ^ z)) - self.assertEquals(x * z * y, ((z < x) * y) + (y < (z ^ x)) - (x ^ y ^ z)) - - self.assertEquals(x * y * z - y * x * z, 2 * ((x ^ y ^ z) - (z < (x ^ y)))) - self.assertEquals(y * z * x - z * y * x, 2 * ((x ^ y ^ z) - (x < (y ^ z)))) - self.assertEquals(z * x * y - x * z * y, 2 * ((x ^ y ^ z) - (y < (z ^ x)))) - - self.assertEquals(z < (x ^ y), ((z < x) ^ y) - (x ^ (z < y))) - self.assertEquals(x < (y ^ z), ((x < y) ^ z) - (y ^ (x < z))) - self.assertEquals(y < (z ^ x), ((y < z) ^ x) - (z ^ (y < x))) - - self.assertEquals(((z < x) ^ y) - (x ^ (z < y)) + ((x < y) ^ z) - (y ^ (x < z)) + ((y < z) ^ x) - (z ^ (y < x)), - 0) - - self.assertEquals(x * y * z - y * x * z + y * z * x - z * y * x + z * x * y - x * z * y, 6 * (x ^ y ^ z)) + self.assertEqual(x * y * z, ((x < y) + (x ^ y)) * z) + self.assertEqual(x * y * z, ((x < y) * z) + ((x ^ y) * z)) + self.assertEqual(x * y * z, ((x < y) * z) - (z < hat(x ^ y)) + (z ^ hat(x ^ y))) + self.assertEqual(hat(x ^ y), x ^ y) + self.assertEqual(x * y * z, ((x < y) * z) - (z < (x ^ y)) + (z ^ x ^ y)) + self.assertEqual(x * y * z, ((x < y) * z) - (z < (x ^ y)) + (x ^ y ^ z)) + + self.assertEqual(y * x * z, ((y < x) + (y ^ x)) * z) + self.assertEqual(y * x * z, ((y < x) * z) + ((y ^ x) * z)) + self.assertEqual(y * x * z, ((y < x) * z) - (z < hat(y ^ x)) + (z ^ hat(y ^ x))) + self.assertEqual(hat(y ^ x), y ^ x) + self.assertEqual(y * x * z, ((y < x) * z) - (z < (y ^ x)) + (z ^ y ^ x)) + self.assertEqual(y * x * z, ((x < y) * z) + (z < (x ^ y)) - (x ^ y ^ z)) + + self.assertEqual(y * z * x, ((y < z) + (y ^ z)) * x) + self.assertEqual(y * z * x, ((y < z) * x) - (x < hat(y ^ z)) + (x ^ hat(y ^ z))) + self.assertEqual(y * z * x, ((y < z) * x) - (x < (y ^ z)) + (x ^ y ^ z)) + + self.assertEqual(z * y * x, ((z < y) + (z ^ y)) * x) + self.assertEqual(z * y * x, ((z < y) * x) - (x < hat(z ^ y)) + (x ^ hat(z ^ y))) + self.assertEqual(z * y * x, ((z < y) * x) - (x < (z ^ y)) - (x ^ y ^ z)) + self.assertEqual(z * y * x, ((y < z) * x) + (x < (y ^ z)) - (x ^ y ^ z)) + + self.assertEqual(z * x * y, ((z < x) + (z ^ x)) * y) + self.assertEqual(z * x * y, ((z < x) * y) + ((z ^ x) * y)) + self.assertEqual(z * x * y, ((z < x) * y) - (y < hat(z ^ x)) + (y ^ hat(z ^ x))) + self.assertEqual(z * x * y, ((z < x) * y) - (y < (z ^ x)) + (y ^ z ^ x)) + self.assertEqual(z * x * y, ((z < x) * y) - (y < (z ^ x)) + (x ^ y ^ z)) + + self.assertEqual(x * z * y, ((x < z) + (x ^ z)) * y) + self.assertEqual(x * z * y, ((x < z) * y) + ((x ^ z) * y)) + self.assertEqual(x * z * y, ((x < z) * y) - (y < hat(x ^ z)) + (y ^ hat(x ^ z))) + self.assertEqual(x * z * y, ((x < z) * y) - (y < (x ^ z)) + (y ^ x ^ z)) + self.assertEqual(x * z * y, ((z < x) * y) + (y < (z ^ x)) - (x ^ y ^ z)) + + self.assertEqual(x * y * z - y * x * z, 2 * ((x ^ y ^ z) - (z < (x ^ y)))) + self.assertEqual(y * z * x - z * y * x, 2 * ((x ^ y ^ z) - (x < (y ^ z)))) + self.assertEqual(z * x * y - x * z * y, 2 * ((x ^ y ^ z) - (y < (z ^ x)))) + + self.assertEqual(z < (x ^ y), ((z < x) ^ y) - (x ^ (z < y))) + self.assertEqual(x < (y ^ z), ((x < y) ^ z) - (y ^ (x < z))) + self.assertEqual(y < (z ^ x), ((y < z) ^ x) - (z ^ (y < x))) + + self.assertEqual(((z < x) ^ y) - (x ^ (z < y)) + ((x < y) ^ z) - (y ^ (x < z)) + ((y < z) ^ x) - (z ^ (y < x)), + 0) + + self.assertEqual(x * y * z - y * x * z + y * z * x - z * y * x + z * x * y - x * z * y, 6 * (x ^ y ^ z)) def test6_6_2_4(self): """ @@ -227,11 +227,11 @@ def test6_6_2_6(self): B_grade_blades = [(l, GA.mv('B', 'blade', l)) for l in range(GA.n + 1)] for (j, X), (k, A), (l, B) in product(X_grade_blades, A_grade_blades, B_grade_blades): - self.assertEquals((X * A).get_grade(j + k), X ^ A) - self.assertEquals((A * B).get_grade(l - k), 0 if k > l else A < B) + self.assertEqual((X * A).get_grade(j + k), X ^ A) + self.assertEqual((A * B).get_grade(l - k), 0 if k > l else A < B) self.assertTrue(((X * A).get_grade(j + k) * B).get_grade(0) != 0 if j + k == l else True) self.assertTrue((X * (A * B).get_grade(l - k)).get_grade(0) != 0 if j == l - k else True) - self.assertEquals(((X * A).get_grade(j + k) * B).get_grade(0), (X * (A * B).get_grade(l - k)).get_grade(0)) + self.assertEqual(((X * A).get_grade(j + k) * B).get_grade(0), (X * (A * B).get_grade(l - k)).get_grade(0)) def test6_6_2_7(self): """ @@ -250,7 +250,7 @@ def test6_6_2_7(self): x = GA.mv('x', 'vector') A_grade_and_blades = [(k, GA.mv('A', 'blade', k)) for k in range(GA.n + 1)] for k, A in A_grade_and_blades: - self.assertEquals((x < A.inv()) * A, (x < A.inv()) < A) + self.assertEqual((x < A.inv()) * A, (x < A.inv()) < A) def test6_6_2_9(self): """ diff --git a/tests/test_chapter7.py b/tests/test_chapter7.py index cc26061c..937ee7d2 100644 --- a/tests/test_chapter7.py +++ b/tests/test_chapter7.py @@ -16,14 +16,14 @@ def test7_9_1(self): # .1 Compute R1 R1 = ((e_1 ^ e_2) * (-pi / 4)).exp() - self.assertEquals(R1 * e_1 * R1.rev(), e_2) + self.assertEqual(R1 * e_1 * R1.rev(), e_2) # .2 Compute R2 R2 = ((e_3 ^ e_1) * (pi / 4)).exp() # .3 Compute R2 R1 R2R1 = R2 * R1 - self.assertEquals(R2R1 * (e_1 ^ e_2) * R2R1.rev(), -e_2 ^ e_3) + self.assertEqual(R2R1 * (e_1 ^ e_2) * R2R1.rev(), -e_2 ^ e_3) # .4 Compute the axis and angle of R2 R1 a_1 = Symbol('a_1', real=True) @@ -39,17 +39,17 @@ def test7_9_1(self): unknowns = [a_1, a_2, a_3, theta] results = solve(system, unknowns, dict=True) - self.assertEquals(a.subs(results[0]), (-e_1 - e_2 + e_3) / sqrt(3)) - self.assertEquals(Mod(theta.subs(results[0]), 2 * pi), Mod(Rational(2, 3) * pi, 2 * pi)) - self.assertEquals(a.subs(results[1]), -(-e_1 - e_2 + e_3) / sqrt(3)) - self.assertEquals(Mod(theta.subs(results[1]), 2 * pi), Mod(-Rational(2, 3) * pi, 2 * pi)) + self.assertEqual(a.subs(results[0]), (-e_1 - e_2 + e_3) / sqrt(3)) + self.assertEqual(Mod(theta.subs(results[0]), 2 * pi), Mod(Rational(2, 3) * pi, 2 * pi)) + self.assertEqual(a.subs(results[1]), -(-e_1 - e_2 + e_3) / sqrt(3)) + self.assertEqual(Mod(theta.subs(results[1]), 2 * pi), Mod(-Rational(2, 3) * pi, 2 * pi)) # Check .4 against .3 a = a.subs(results[0]) theta = theta.subs(results[0]) A = a.dual() R = cos(theta / 2) + A * sin(theta / 2) - self.assertEquals(R * (e_1 ^ e_2) * R.rev(), -e_2 ^ e_3) + self.assertEqual(R * (e_1 ^ e_2) * R.rev(), -e_2 ^ e_3) # .5 Compute the product of rotors GA, e_1, e_2, e_3, e_4 = Ga.build("e*1|2|3|4", g='1 0 0 0, 0 1 0 0, 0 0 1 0, 0 0 0 1') @@ -58,7 +58,7 @@ def test7_9_1(self): R2 = ((e_2 ^ e_3) * (-pi / 4)).exp() R = R2 * R1 - self.assertEquals(R * (e_1 ^ e_2) * R.rev(), -e_3 ^ e_4) + self.assertEqual(R * (e_1 ^ e_2) * R.rev(), -e_3 ^ e_4) # .6 Reflect in a plane A = (e_1 + e_2) ^ e_3 @@ -68,7 +68,7 @@ def test7_9_1(self): def hat(k, M): return ((-1) ** k) * M - self.assertEquals(b * hat(2, A) * b.inv(), (-e_1 + e_2) ^ e_3) + self.assertEqual(b * hat(2, A) * b.inv(), (-e_1 + e_2) ^ e_3) # .7 Reflect the dual plane reflector e1 in the plane e1 ^ e3 GA, e_1, e_2, e_3 = Ga.build("e*1|2|3", g='1 0 0, 0 1 0, 0 0 1') @@ -77,7 +77,7 @@ def hat(k, M): B = e_1 ^ e_3 b = B.dual() - self.assertEquals(-b * a * b.inv(), e_1) + self.assertEqual(-b * a * b.inv(), e_1) # Reset Ga.dual_mode() diff --git a/tests/test_chapter8.py b/tests/test_chapter8.py index 3f6e056d..25255bcd 100644 --- a/tests/test_chapter8.py +++ b/tests/test_chapter8.py @@ -16,24 +16,24 @@ def test8_2_1(self): B = GA.mv('B', 'mv') C = GA.mv('C', 'mv') - self.assertEquals(com(com(A, B), C) - com(A, com(B, C)), com(B, com(C, A))) - self.assertEquals(com(com(A, B), C) + com(com(C, A), B) + com(com(B, C), A), S.Zero) + self.assertEqual(com(com(A, B), C) - com(A, com(B, C)), com(B, com(C, A))) + self.assertEqual(com(com(A, B), C) + com(com(C, A), B) + com(com(B, C), A), S.Zero) B = GA.mv('B', 2, 'blade') X = GA.mv('A', 'mv') - self.assertEquals(B.pure_grade(), 2) - self.assertEquals(com(X, B).pure_grade(), -2) # Not pure + self.assertEqual(B.pure_grade(), 2) + self.assertEqual(com(X, B).pure_grade(), -2) # Not pure E = com(X, B).rev() - self.assertEquals(E, (B.rev() * X.rev() - X.rev() * B.rev()) / 2) - self.assertEquals(E, (X.rev() * B - B * X.rev()) / 2) - self.assertEquals(E, com(X.rev(), B)) + self.assertEqual(E, (B.rev() * X.rev() - X.rev() * B.rev()) / 2) + self.assertEqual(E, (X.rev() * B - B * X.rev()) / 2) + self.assertEqual(E, com(X.rev(), B)) alpha = GA.mv('alpha', 'scalar') - self.assertEquals(alpha * X, alpha ^ X) + self.assertEqual(alpha * X, alpha ^ X) a = GA.mv('a', 'vector') - self.assertEquals(a * X, (a < X) + (a ^ X)) + self.assertEqual(a * X, (a < X) + (a ^ X)) A = GA.mv('A', 2, 'grade') - self.assertEquals(A * X, (A < X) + com(A, X) + (A ^ X)) + self.assertEqual(A * X, (A < X) + com(A, X) + (A ^ X)) diff --git a/tests/test_generator.py b/tests/test_generator.py index f6871402..89e678ed 100644 --- a/tests/test_generator.py +++ b/tests/test_generator.py @@ -25,7 +25,7 @@ def test_add(self): X = GA.mv('x', 'mv') Y = GA.mv('y', 'mv') - self.assertEquals(X + Y, expand(GA, flatten(flat_GA, X) + flatten(flat_GA, Y))) + self.assertEqual(X + Y, expand(GA, flatten(flat_GA, X) + flatten(flat_GA, Y))) def test_sub(self): GA = self.GA @@ -33,7 +33,7 @@ def test_sub(self): X = GA.mv('x', 'mv') Y = GA.mv('y', 'mv') - self.assertEquals(X - Y, expand(GA, flatten(flat_GA, X) - flatten(flat_GA, Y))) + self.assertEqual(X - Y, expand(GA, flatten(flat_GA, X) - flatten(flat_GA, Y))) def test_mul(self): GA = self.GA @@ -41,7 +41,7 @@ def test_mul(self): X = GA.mv('x', 'mv') Y = GA.mv('y', 'mv') - self.assertEquals(X * Y, expand(GA, flatten(flat_GA, X) * flatten(flat_GA, Y))) + self.assertEqual(X * Y, expand(GA, flatten(flat_GA, X) * flatten(flat_GA, Y))) def test_and(self): GA = self.GA @@ -49,7 +49,7 @@ def test_and(self): X = GA.mv('x', 'mv') Y = GA.mv('y', 'mv') - self.assertEquals(Jinv(J(X) ^ J(Y)), expand(GA, flatten(flat_GA, X) & flatten(flat_GA, Y))) + self.assertEqual(Jinv(J(X) ^ J(Y)), expand(GA, flatten(flat_GA, X) & flatten(flat_GA, Y))) def test_xor(self): GA = self.GA @@ -57,7 +57,7 @@ def test_xor(self): X = GA.mv('x', 'mv') Y = GA.mv('y', 'mv') - self.assertEquals(X ^ Y, expand(GA, flatten(flat_GA, X) ^ flatten(flat_GA, Y))) + self.assertEqual(X ^ Y, expand(GA, flatten(flat_GA, X) ^ flatten(flat_GA, Y))) def test_lshift(self): GA = self.GA @@ -65,7 +65,7 @@ def test_lshift(self): X = GA.mv('x', 'mv') Y = GA.mv('y', 'mv') - self.assertEquals(X << Y, expand(GA, flatten(flat_GA, X) << flatten(flat_GA, Y))) + self.assertEqual(X << Y, expand(GA, flatten(flat_GA, X) << flatten(flat_GA, Y))) def test_rshift(self): GA = self.GA @@ -73,7 +73,7 @@ def test_rshift(self): X = GA.mv('x', 'mv') Y = GA.mv('y', 'mv') - self.assertEquals(X >> Y, expand(GA, flatten(flat_GA, X) >> flatten(flat_GA, Y))) + self.assertEqual(X >> Y, expand(GA, flatten(flat_GA, X) >> flatten(flat_GA, Y))) def test_meet_and_join(self): GA = self.GA @@ -81,12 +81,12 @@ def test_meet_and_join(self): X = GA.mv('x', 'mv') Y = GA.mv('y', 'mv') - self.assertEquals(Jinv(J(X) ^ J(Y)), expand(GA, flatten(flat_GA, X).meet(flatten(flat_GA, Y)))) - self.assertEquals(X ^ Y, expand(GA, flatten(flat_GA, X).join(flatten(flat_GA, Y)))) + self.assertEqual(Jinv(J(X) ^ J(Y)), expand(GA, flatten(flat_GA, X).meet(flatten(flat_GA, Y)))) + self.assertEqual(X ^ Y, expand(GA, flatten(flat_GA, X).join(flatten(flat_GA, Y)))) def test_rev(self): GA = self.GA flat_GA = self.flat_GA X = GA.mv('x', 'mv') - self.assertEquals(X.rev(), expand(GA, flatten(flat_GA, X).rev())) + self.assertEqual(X.rev(), expand(GA, flatten(flat_GA, X).rev())) diff --git a/tests/test_pga.py b/tests/test_pga.py index 0cabb54c..49bfc29d 100644 --- a/tests/test_pga.py +++ b/tests/test_pga.py @@ -111,276 +111,276 @@ def test_multiplication_table(self): Reproduce multiplication table. """ # 1 - self.assertEquals(S.One * self.e_0, self.e_0) - self.assertEquals(S.One * self.e_1, self.e_1) - self.assertEquals(S.One * self.e_2, self.e_2) - self.assertEquals(S.One * self.e_3, self.e_3) - self.assertEquals(S.One * self.e_01, self.e_01) - self.assertEquals(S.One * self.e_02, self.e_02) - self.assertEquals(S.One * self.e_03, self.e_03) - self.assertEquals(S.One * self.e_12, self.e_12) - self.assertEquals(S.One * self.e_31, self.e_31) - self.assertEquals(S.One * self.e_23, self.e_23) - self.assertEquals(S.One * self.e_021, self.e_021) - self.assertEquals(S.One * self.e_013, self.e_013) - self.assertEquals(S.One * self.e_032, self.e_032) - self.assertEquals(S.One * self.e_123, self.e_123) - self.assertEquals(S.One * self.e_0123, self.e_0123) + self.assertEqual(S.One * self.e_0, self.e_0) + self.assertEqual(S.One * self.e_1, self.e_1) + self.assertEqual(S.One * self.e_2, self.e_2) + self.assertEqual(S.One * self.e_3, self.e_3) + self.assertEqual(S.One * self.e_01, self.e_01) + self.assertEqual(S.One * self.e_02, self.e_02) + self.assertEqual(S.One * self.e_03, self.e_03) + self.assertEqual(S.One * self.e_12, self.e_12) + self.assertEqual(S.One * self.e_31, self.e_31) + self.assertEqual(S.One * self.e_23, self.e_23) + self.assertEqual(S.One * self.e_021, self.e_021) + self.assertEqual(S.One * self.e_013, self.e_013) + self.assertEqual(S.One * self.e_032, self.e_032) + self.assertEqual(S.One * self.e_123, self.e_123) + self.assertEqual(S.One * self.e_0123, self.e_0123) # e0 - self.assertEquals(self.e_0 * self.e_0, S.Zero) - self.assertEquals(self.e_0 * self.e_1, self.e_01) - self.assertEquals(self.e_0 * self.e_2, self.e_02) - self.assertEquals(self.e_0 * self.e_3, self.e_03) - self.assertEquals(self.e_0 * self.e_01, S.Zero) - self.assertEquals(self.e_0 * self.e_02, S.Zero) - self.assertEquals(self.e_0 * self.e_03, S.Zero) - self.assertEquals(self.e_0 * self.e_12, -self.e_021) - self.assertEquals(self.e_0 * self.e_31, -self.e_013) - self.assertEquals(self.e_0 * self.e_23, -self.e_032) - self.assertEquals(self.e_0 * self.e_021, S.Zero) - self.assertEquals(self.e_0 * self.e_013, S.Zero) - self.assertEquals(self.e_0 * self.e_032, S.Zero) - self.assertEquals(self.e_0 * self.e_123, self.e_0123) - self.assertEquals(self.e_0 * self.e_0123, S.Zero) + self.assertEqual(self.e_0 * self.e_0, S.Zero) + self.assertEqual(self.e_0 * self.e_1, self.e_01) + self.assertEqual(self.e_0 * self.e_2, self.e_02) + self.assertEqual(self.e_0 * self.e_3, self.e_03) + self.assertEqual(self.e_0 * self.e_01, S.Zero) + self.assertEqual(self.e_0 * self.e_02, S.Zero) + self.assertEqual(self.e_0 * self.e_03, S.Zero) + self.assertEqual(self.e_0 * self.e_12, -self.e_021) + self.assertEqual(self.e_0 * self.e_31, -self.e_013) + self.assertEqual(self.e_0 * self.e_23, -self.e_032) + self.assertEqual(self.e_0 * self.e_021, S.Zero) + self.assertEqual(self.e_0 * self.e_013, S.Zero) + self.assertEqual(self.e_0 * self.e_032, S.Zero) + self.assertEqual(self.e_0 * self.e_123, self.e_0123) + self.assertEqual(self.e_0 * self.e_0123, S.Zero) # e1 - self.assertEquals(self.e_1 * self.e_0, -self.e_01) - self.assertEquals(self.e_1 * self.e_1, S.One) - self.assertEquals(self.e_1 * self.e_2, self.e_12) - self.assertEquals(self.e_1 * self.e_3, -self.e_31) - self.assertEquals(self.e_1 * self.e_01, -self.e_0) - self.assertEquals(self.e_1 * self.e_02, self.e_021) - self.assertEquals(self.e_1 * self.e_03, -self.e_013) - self.assertEquals(self.e_1 * self.e_12, self.e_2) - self.assertEquals(self.e_1 * self.e_31, -self.e_3) - self.assertEquals(self.e_1 * self.e_23, self.e_123) - self.assertEquals(self.e_1 * self.e_021, self.e_02) - self.assertEquals(self.e_1 * self.e_013, -self.e_03) - self.assertEquals(self.e_1 * self.e_032, self.e_0123) - self.assertEquals(self.e_1 * self.e_123, self.e_23) - self.assertEquals(self.e_1 * self.e_0123, self.e_032) + self.assertEqual(self.e_1 * self.e_0, -self.e_01) + self.assertEqual(self.e_1 * self.e_1, S.One) + self.assertEqual(self.e_1 * self.e_2, self.e_12) + self.assertEqual(self.e_1 * self.e_3, -self.e_31) + self.assertEqual(self.e_1 * self.e_01, -self.e_0) + self.assertEqual(self.e_1 * self.e_02, self.e_021) + self.assertEqual(self.e_1 * self.e_03, -self.e_013) + self.assertEqual(self.e_1 * self.e_12, self.e_2) + self.assertEqual(self.e_1 * self.e_31, -self.e_3) + self.assertEqual(self.e_1 * self.e_23, self.e_123) + self.assertEqual(self.e_1 * self.e_021, self.e_02) + self.assertEqual(self.e_1 * self.e_013, -self.e_03) + self.assertEqual(self.e_1 * self.e_032, self.e_0123) + self.assertEqual(self.e_1 * self.e_123, self.e_23) + self.assertEqual(self.e_1 * self.e_0123, self.e_032) # e2 - self.assertEquals(self.e_2 * self.e_0, -self.e_02) - self.assertEquals(self.e_2 * self.e_1, -self.e_12) - self.assertEquals(self.e_2 * self.e_2, S.One) - self.assertEquals(self.e_2 * self.e_3, self.e_23) - self.assertEquals(self.e_2 * self.e_01, -self.e_021) - self.assertEquals(self.e_2 * self.e_02, -self.e_0) - self.assertEquals(self.e_2 * self.e_03, self.e_032) - self.assertEquals(self.e_2 * self.e_12, -self.e_1) - self.assertEquals(self.e_2 * self.e_31, self.e_123) - self.assertEquals(self.e_2 * self.e_23, self.e_3) - self.assertEquals(self.e_2 * self.e_021, -self.e_01) - self.assertEquals(self.e_2 * self.e_013, self.e_0123) - self.assertEquals(self.e_2 * self.e_032, self.e_03) - self.assertEquals(self.e_2 * self.e_123, self.e_31) - self.assertEquals(self.e_2 * self.e_0123, self.e_013) + self.assertEqual(self.e_2 * self.e_0, -self.e_02) + self.assertEqual(self.e_2 * self.e_1, -self.e_12) + self.assertEqual(self.e_2 * self.e_2, S.One) + self.assertEqual(self.e_2 * self.e_3, self.e_23) + self.assertEqual(self.e_2 * self.e_01, -self.e_021) + self.assertEqual(self.e_2 * self.e_02, -self.e_0) + self.assertEqual(self.e_2 * self.e_03, self.e_032) + self.assertEqual(self.e_2 * self.e_12, -self.e_1) + self.assertEqual(self.e_2 * self.e_31, self.e_123) + self.assertEqual(self.e_2 * self.e_23, self.e_3) + self.assertEqual(self.e_2 * self.e_021, -self.e_01) + self.assertEqual(self.e_2 * self.e_013, self.e_0123) + self.assertEqual(self.e_2 * self.e_032, self.e_03) + self.assertEqual(self.e_2 * self.e_123, self.e_31) + self.assertEqual(self.e_2 * self.e_0123, self.e_013) # e3 - self.assertEquals(self.e_3 * self.e_0, -self.e_03) - self.assertEquals(self.e_3 * self.e_1, self.e_31) - self.assertEquals(self.e_3 * self.e_2, -self.e_23) - self.assertEquals(self.e_3 * self.e_3, S.One) - self.assertEquals(self.e_3 * self.e_01, self.e_013) - self.assertEquals(self.e_3 * self.e_02, -self.e_032) - self.assertEquals(self.e_3 * self.e_03, -self.e_0) - self.assertEquals(self.e_3 * self.e_12, self.e_123) - self.assertEquals(self.e_3 * self.e_31, self.e_1) - self.assertEquals(self.e_3 * self.e_23, -self.e_2) - self.assertEquals(self.e_3 * self.e_021, self.e_0123) - self.assertEquals(self.e_3 * self.e_013, self.e_01) - self.assertEquals(self.e_3 * self.e_032, -self.e_02) - self.assertEquals(self.e_3 * self.e_123, self.e_12) - self.assertEquals(self.e_3 * self.e_0123, self.e_021) + self.assertEqual(self.e_3 * self.e_0, -self.e_03) + self.assertEqual(self.e_3 * self.e_1, self.e_31) + self.assertEqual(self.e_3 * self.e_2, -self.e_23) + self.assertEqual(self.e_3 * self.e_3, S.One) + self.assertEqual(self.e_3 * self.e_01, self.e_013) + self.assertEqual(self.e_3 * self.e_02, -self.e_032) + self.assertEqual(self.e_3 * self.e_03, -self.e_0) + self.assertEqual(self.e_3 * self.e_12, self.e_123) + self.assertEqual(self.e_3 * self.e_31, self.e_1) + self.assertEqual(self.e_3 * self.e_23, -self.e_2) + self.assertEqual(self.e_3 * self.e_021, self.e_0123) + self.assertEqual(self.e_3 * self.e_013, self.e_01) + self.assertEqual(self.e_3 * self.e_032, -self.e_02) + self.assertEqual(self.e_3 * self.e_123, self.e_12) + self.assertEqual(self.e_3 * self.e_0123, self.e_021) # e01 - self.assertEquals(self.e_01 * self.e_0, S.Zero) - self.assertEquals(self.e_01 * self.e_1, self.e_0) - self.assertEquals(self.e_01 * self.e_2, -self.e_021) - self.assertEquals(self.e_01 * self.e_3, self.e_013) - self.assertEquals(self.e_01 * self.e_01, S.Zero) - self.assertEquals(self.e_01 * self.e_02, S.Zero) - self.assertEquals(self.e_01 * self.e_03, S.Zero) - self.assertEquals(self.e_01 * self.e_12, self.e_02) - self.assertEquals(self.e_01 * self.e_31, -self.e_03) - self.assertEquals(self.e_01 * self.e_23, self.e_0123) - self.assertEquals(self.e_01 * self.e_021, S.Zero) - self.assertEquals(self.e_01 * self.e_013, S.Zero) - self.assertEquals(self.e_01 * self.e_032, S.Zero) - self.assertEquals(self.e_01 * self.e_123, -self.e_032) - self.assertEquals(self.e_01 * self.e_0123, S.Zero) + self.assertEqual(self.e_01 * self.e_0, S.Zero) + self.assertEqual(self.e_01 * self.e_1, self.e_0) + self.assertEqual(self.e_01 * self.e_2, -self.e_021) + self.assertEqual(self.e_01 * self.e_3, self.e_013) + self.assertEqual(self.e_01 * self.e_01, S.Zero) + self.assertEqual(self.e_01 * self.e_02, S.Zero) + self.assertEqual(self.e_01 * self.e_03, S.Zero) + self.assertEqual(self.e_01 * self.e_12, self.e_02) + self.assertEqual(self.e_01 * self.e_31, -self.e_03) + self.assertEqual(self.e_01 * self.e_23, self.e_0123) + self.assertEqual(self.e_01 * self.e_021, S.Zero) + self.assertEqual(self.e_01 * self.e_013, S.Zero) + self.assertEqual(self.e_01 * self.e_032, S.Zero) + self.assertEqual(self.e_01 * self.e_123, -self.e_032) + self.assertEqual(self.e_01 * self.e_0123, S.Zero) # e02 - self.assertEquals(self.e_02 * self.e_0, S.Zero) - self.assertEquals(self.e_02 * self.e_1, self.e_021) - self.assertEquals(self.e_02 * self.e_2, self.e_0) - self.assertEquals(self.e_02 * self.e_3, -self.e_032) - self.assertEquals(self.e_02 * self.e_01, S.Zero) - self.assertEquals(self.e_02 * self.e_02, S.Zero) - self.assertEquals(self.e_02 * self.e_03, S.Zero) - self.assertEquals(self.e_02 * self.e_12, -self.e_01) - self.assertEquals(self.e_02 * self.e_31, self.e_0123) - self.assertEquals(self.e_02 * self.e_23, self.e_03) - self.assertEquals(self.e_02 * self.e_021, S.Zero) - self.assertEquals(self.e_02 * self.e_013, S.Zero) - self.assertEquals(self.e_02 * self.e_032, S.Zero) - self.assertEquals(self.e_02 * self.e_123, -self.e_013) - self.assertEquals(self.e_02 * self.e_0123, S.Zero) + self.assertEqual(self.e_02 * self.e_0, S.Zero) + self.assertEqual(self.e_02 * self.e_1, self.e_021) + self.assertEqual(self.e_02 * self.e_2, self.e_0) + self.assertEqual(self.e_02 * self.e_3, -self.e_032) + self.assertEqual(self.e_02 * self.e_01, S.Zero) + self.assertEqual(self.e_02 * self.e_02, S.Zero) + self.assertEqual(self.e_02 * self.e_03, S.Zero) + self.assertEqual(self.e_02 * self.e_12, -self.e_01) + self.assertEqual(self.e_02 * self.e_31, self.e_0123) + self.assertEqual(self.e_02 * self.e_23, self.e_03) + self.assertEqual(self.e_02 * self.e_021, S.Zero) + self.assertEqual(self.e_02 * self.e_013, S.Zero) + self.assertEqual(self.e_02 * self.e_032, S.Zero) + self.assertEqual(self.e_02 * self.e_123, -self.e_013) + self.assertEqual(self.e_02 * self.e_0123, S.Zero) # e03 - self.assertEquals(self.e_03 * self.e_0, S.Zero) - self.assertEquals(self.e_03 * self.e_1, -self.e_013) - self.assertEquals(self.e_03 * self.e_2, self.e_032) - self.assertEquals(self.e_03 * self.e_3, self.e_0) - self.assertEquals(self.e_03 * self.e_01, S.Zero) - self.assertEquals(self.e_03 * self.e_02, S.Zero) - self.assertEquals(self.e_03 * self.e_03, S.Zero) - self.assertEquals(self.e_03 * self.e_12, self.e_0123) - self.assertEquals(self.e_03 * self.e_31, self.e_01) - self.assertEquals(self.e_03 * self.e_23, -self.e_02) - self.assertEquals(self.e_03 * self.e_021, S.Zero) - self.assertEquals(self.e_03 * self.e_013, S.Zero) - self.assertEquals(self.e_03 * self.e_032, S.Zero) - self.assertEquals(self.e_03 * self.e_123, -self.e_021) - self.assertEquals(self.e_03 * self.e_0123, S.Zero) + self.assertEqual(self.e_03 * self.e_0, S.Zero) + self.assertEqual(self.e_03 * self.e_1, -self.e_013) + self.assertEqual(self.e_03 * self.e_2, self.e_032) + self.assertEqual(self.e_03 * self.e_3, self.e_0) + self.assertEqual(self.e_03 * self.e_01, S.Zero) + self.assertEqual(self.e_03 * self.e_02, S.Zero) + self.assertEqual(self.e_03 * self.e_03, S.Zero) + self.assertEqual(self.e_03 * self.e_12, self.e_0123) + self.assertEqual(self.e_03 * self.e_31, self.e_01) + self.assertEqual(self.e_03 * self.e_23, -self.e_02) + self.assertEqual(self.e_03 * self.e_021, S.Zero) + self.assertEqual(self.e_03 * self.e_013, S.Zero) + self.assertEqual(self.e_03 * self.e_032, S.Zero) + self.assertEqual(self.e_03 * self.e_123, -self.e_021) + self.assertEqual(self.e_03 * self.e_0123, S.Zero) # e12 - self.assertEquals(self.e_12 * self.e_0, -self.e_021) - self.assertEquals(self.e_12 * self.e_1, -self.e_2) - self.assertEquals(self.e_12 * self.e_2, self.e_1) - self.assertEquals(self.e_12 * self.e_3, self.e_123) - self.assertEquals(self.e_12 * self.e_01, -self.e_02) - self.assertEquals(self.e_12 * self.e_02, self.e_01) - self.assertEquals(self.e_12 * self.e_03, self.e_0123) - self.assertEquals(self.e_12 * self.e_12, -S.One) - self.assertEquals(self.e_12 * self.e_31, self.e_23) - self.assertEquals(self.e_12 * self.e_23, -self.e_31) - self.assertEquals(self.e_12 * self.e_021, self.e_0) - self.assertEquals(self.e_12 * self.e_013, self.e_032) - self.assertEquals(self.e_12 * self.e_032, -self.e_013) - self.assertEquals(self.e_12 * self.e_123, -self.e_3) - self.assertEquals(self.e_12 * self.e_0123, -self.e_03) + self.assertEqual(self.e_12 * self.e_0, -self.e_021) + self.assertEqual(self.e_12 * self.e_1, -self.e_2) + self.assertEqual(self.e_12 * self.e_2, self.e_1) + self.assertEqual(self.e_12 * self.e_3, self.e_123) + self.assertEqual(self.e_12 * self.e_01, -self.e_02) + self.assertEqual(self.e_12 * self.e_02, self.e_01) + self.assertEqual(self.e_12 * self.e_03, self.e_0123) + self.assertEqual(self.e_12 * self.e_12, -S.One) + self.assertEqual(self.e_12 * self.e_31, self.e_23) + self.assertEqual(self.e_12 * self.e_23, -self.e_31) + self.assertEqual(self.e_12 * self.e_021, self.e_0) + self.assertEqual(self.e_12 * self.e_013, self.e_032) + self.assertEqual(self.e_12 * self.e_032, -self.e_013) + self.assertEqual(self.e_12 * self.e_123, -self.e_3) + self.assertEqual(self.e_12 * self.e_0123, -self.e_03) # e31 - self.assertEquals(self.e_31 * self.e_0, -self.e_013) - self.assertEquals(self.e_31 * self.e_1, self.e_3) - self.assertEquals(self.e_31 * self.e_2, self.e_123) - self.assertEquals(self.e_31 * self.e_3, -self.e_1) - self.assertEquals(self.e_31 * self.e_01, self.e_03) - self.assertEquals(self.e_31 * self.e_02, self.e_0123) - self.assertEquals(self.e_31 * self.e_03, -self.e_01) - self.assertEquals(self.e_31 * self.e_12, -self.e_23) - self.assertEquals(self.e_31 * self.e_31, -S.One) - self.assertEquals(self.e_31 * self.e_23, self.e_12) - self.assertEquals(self.e_31 * self.e_021, -self.e_032) - self.assertEquals(self.e_31 * self.e_013, self.e_0) - self.assertEquals(self.e_31 * self.e_032, self.e_021) - self.assertEquals(self.e_31 * self.e_123, -self.e_2) - self.assertEquals(self.e_31 * self.e_0123, -self.e_02) + self.assertEqual(self.e_31 * self.e_0, -self.e_013) + self.assertEqual(self.e_31 * self.e_1, self.e_3) + self.assertEqual(self.e_31 * self.e_2, self.e_123) + self.assertEqual(self.e_31 * self.e_3, -self.e_1) + self.assertEqual(self.e_31 * self.e_01, self.e_03) + self.assertEqual(self.e_31 * self.e_02, self.e_0123) + self.assertEqual(self.e_31 * self.e_03, -self.e_01) + self.assertEqual(self.e_31 * self.e_12, -self.e_23) + self.assertEqual(self.e_31 * self.e_31, -S.One) + self.assertEqual(self.e_31 * self.e_23, self.e_12) + self.assertEqual(self.e_31 * self.e_021, -self.e_032) + self.assertEqual(self.e_31 * self.e_013, self.e_0) + self.assertEqual(self.e_31 * self.e_032, self.e_021) + self.assertEqual(self.e_31 * self.e_123, -self.e_2) + self.assertEqual(self.e_31 * self.e_0123, -self.e_02) # e23 - self.assertEquals(self.e_23 * self.e_0, -self.e_032) - self.assertEquals(self.e_23 * self.e_1, self.e_123) - self.assertEquals(self.e_23 * self.e_2, -self.e_3) - self.assertEquals(self.e_23 * self.e_3, self.e_2) - self.assertEquals(self.e_23 * self.e_01, self.e_0123) - self.assertEquals(self.e_23 * self.e_02, -self.e_03) - self.assertEquals(self.e_23 * self.e_03, self.e_02) - self.assertEquals(self.e_23 * self.e_12, self.e_31) - self.assertEquals(self.e_23 * self.e_31, -self.e_12) - self.assertEquals(self.e_23 * self.e_23, -S.One) - self.assertEquals(self.e_23 * self.e_021, self.e_013) - self.assertEquals(self.e_23 * self.e_013, -self.e_021) - self.assertEquals(self.e_23 * self.e_032, self.e_0) - self.assertEquals(self.e_23 * self.e_123, -self.e_1) - self.assertEquals(self.e_23 * self.e_0123, -self.e_01) + self.assertEqual(self.e_23 * self.e_0, -self.e_032) + self.assertEqual(self.e_23 * self.e_1, self.e_123) + self.assertEqual(self.e_23 * self.e_2, -self.e_3) + self.assertEqual(self.e_23 * self.e_3, self.e_2) + self.assertEqual(self.e_23 * self.e_01, self.e_0123) + self.assertEqual(self.e_23 * self.e_02, -self.e_03) + self.assertEqual(self.e_23 * self.e_03, self.e_02) + self.assertEqual(self.e_23 * self.e_12, self.e_31) + self.assertEqual(self.e_23 * self.e_31, -self.e_12) + self.assertEqual(self.e_23 * self.e_23, -S.One) + self.assertEqual(self.e_23 * self.e_021, self.e_013) + self.assertEqual(self.e_23 * self.e_013, -self.e_021) + self.assertEqual(self.e_23 * self.e_032, self.e_0) + self.assertEqual(self.e_23 * self.e_123, -self.e_1) + self.assertEqual(self.e_23 * self.e_0123, -self.e_01) # e021 - self.assertEquals(self.e_021 * self.e_0, S.Zero) - self.assertEquals(self.e_021 * self.e_1, self.e_02) - self.assertEquals(self.e_021 * self.e_2, -self.e_01) - self.assertEquals(self.e_021 * self.e_3, -self.e_0123) - self.assertEquals(self.e_021 * self.e_01, S.Zero) - self.assertEquals(self.e_021 * self.e_02, S.Zero) - self.assertEquals(self.e_021 * self.e_03, S.Zero) - self.assertEquals(self.e_021 * self.e_12, self.e_0) - self.assertEquals(self.e_021 * self.e_31, self.e_032) - self.assertEquals(self.e_021 * self.e_23, -self.e_013) - self.assertEquals(self.e_021 * self.e_021, S.Zero) - self.assertEquals(self.e_021 * self.e_013, S.Zero) - self.assertEquals(self.e_021 * self.e_032, S.Zero) - self.assertEquals(self.e_021 * self.e_123, self.e_03) - self.assertEquals(self.e_021 * self.e_0123, S.Zero) + self.assertEqual(self.e_021 * self.e_0, S.Zero) + self.assertEqual(self.e_021 * self.e_1, self.e_02) + self.assertEqual(self.e_021 * self.e_2, -self.e_01) + self.assertEqual(self.e_021 * self.e_3, -self.e_0123) + self.assertEqual(self.e_021 * self.e_01, S.Zero) + self.assertEqual(self.e_021 * self.e_02, S.Zero) + self.assertEqual(self.e_021 * self.e_03, S.Zero) + self.assertEqual(self.e_021 * self.e_12, self.e_0) + self.assertEqual(self.e_021 * self.e_31, self.e_032) + self.assertEqual(self.e_021 * self.e_23, -self.e_013) + self.assertEqual(self.e_021 * self.e_021, S.Zero) + self.assertEqual(self.e_021 * self.e_013, S.Zero) + self.assertEqual(self.e_021 * self.e_032, S.Zero) + self.assertEqual(self.e_021 * self.e_123, self.e_03) + self.assertEqual(self.e_021 * self.e_0123, S.Zero) # e013 - self.assertEquals(self.e_013 * self.e_0, S.Zero) - self.assertEquals(self.e_013 * self.e_1, -self.e_03) - self.assertEquals(self.e_013 * self.e_2, -self.e_0123) - self.assertEquals(self.e_013 * self.e_3, self.e_01) - self.assertEquals(self.e_013 * self.e_01, S.Zero) - self.assertEquals(self.e_013 * self.e_02, S.Zero) - self.assertEquals(self.e_013 * self.e_03, S.Zero) - self.assertEquals(self.e_013 * self.e_12, -self.e_032) - self.assertEquals(self.e_013 * self.e_31, self.e_0) - self.assertEquals(self.e_013 * self.e_23, self.e_021) - self.assertEquals(self.e_013 * self.e_021, S.Zero) - self.assertEquals(self.e_013 * self.e_013, S.Zero) - self.assertEquals(self.e_013 * self.e_032, S.Zero) - self.assertEquals(self.e_013 * self.e_123, self.e_02) - self.assertEquals(self.e_013 * self.e_0123, S.Zero) + self.assertEqual(self.e_013 * self.e_0, S.Zero) + self.assertEqual(self.e_013 * self.e_1, -self.e_03) + self.assertEqual(self.e_013 * self.e_2, -self.e_0123) + self.assertEqual(self.e_013 * self.e_3, self.e_01) + self.assertEqual(self.e_013 * self.e_01, S.Zero) + self.assertEqual(self.e_013 * self.e_02, S.Zero) + self.assertEqual(self.e_013 * self.e_03, S.Zero) + self.assertEqual(self.e_013 * self.e_12, -self.e_032) + self.assertEqual(self.e_013 * self.e_31, self.e_0) + self.assertEqual(self.e_013 * self.e_23, self.e_021) + self.assertEqual(self.e_013 * self.e_021, S.Zero) + self.assertEqual(self.e_013 * self.e_013, S.Zero) + self.assertEqual(self.e_013 * self.e_032, S.Zero) + self.assertEqual(self.e_013 * self.e_123, self.e_02) + self.assertEqual(self.e_013 * self.e_0123, S.Zero) # e032 - self.assertEquals(self.e_032 * self.e_0, S.Zero) - self.assertEquals(self.e_032 * self.e_1, -self.e_0123) - self.assertEquals(self.e_032 * self.e_2, self.e_03) - self.assertEquals(self.e_032 * self.e_3, -self.e_02) - self.assertEquals(self.e_032 * self.e_01, S.Zero) - self.assertEquals(self.e_032 * self.e_02, S.Zero) - self.assertEquals(self.e_032 * self.e_03, S.Zero) - self.assertEquals(self.e_032 * self.e_12, self.e_013) - self.assertEquals(self.e_032 * self.e_31, -self.e_021) - self.assertEquals(self.e_032 * self.e_23, self.e_0) - self.assertEquals(self.e_032 * self.e_021, S.Zero) - self.assertEquals(self.e_032 * self.e_013, S.Zero) - self.assertEquals(self.e_032 * self.e_032, S.Zero) - self.assertEquals(self.e_032 * self.e_123, self.e_01) - self.assertEquals(self.e_032 * self.e_0123, S.Zero) + self.assertEqual(self.e_032 * self.e_0, S.Zero) + self.assertEqual(self.e_032 * self.e_1, -self.e_0123) + self.assertEqual(self.e_032 * self.e_2, self.e_03) + self.assertEqual(self.e_032 * self.e_3, -self.e_02) + self.assertEqual(self.e_032 * self.e_01, S.Zero) + self.assertEqual(self.e_032 * self.e_02, S.Zero) + self.assertEqual(self.e_032 * self.e_03, S.Zero) + self.assertEqual(self.e_032 * self.e_12, self.e_013) + self.assertEqual(self.e_032 * self.e_31, -self.e_021) + self.assertEqual(self.e_032 * self.e_23, self.e_0) + self.assertEqual(self.e_032 * self.e_021, S.Zero) + self.assertEqual(self.e_032 * self.e_013, S.Zero) + self.assertEqual(self.e_032 * self.e_032, S.Zero) + self.assertEqual(self.e_032 * self.e_123, self.e_01) + self.assertEqual(self.e_032 * self.e_0123, S.Zero) # e123 - self.assertEquals(self.e_123 * self.e_0, -self.e_0123) - self.assertEquals(self.e_123 * self.e_1, self.e_23) - self.assertEquals(self.e_123 * self.e_2, self.e_31) - self.assertEquals(self.e_123 * self.e_3, self.e_12) - self.assertEquals(self.e_123 * self.e_01, self.e_032) - self.assertEquals(self.e_123 * self.e_02, self.e_013) - self.assertEquals(self.e_123 * self.e_03, self.e_021) - self.assertEquals(self.e_123 * self.e_12, -self.e_3) - self.assertEquals(self.e_123 * self.e_31, -self.e_2) - self.assertEquals(self.e_123 * self.e_23, -self.e_1) - self.assertEquals(self.e_123 * self.e_021, -self.e_03) - self.assertEquals(self.e_123 * self.e_013, -self.e_02) - self.assertEquals(self.e_123 * self.e_032, -self.e_01) - self.assertEquals(self.e_123 * self.e_123, -S.One) - self.assertEquals(self.e_123 * self.e_0123, self.e_0) + self.assertEqual(self.e_123 * self.e_0, -self.e_0123) + self.assertEqual(self.e_123 * self.e_1, self.e_23) + self.assertEqual(self.e_123 * self.e_2, self.e_31) + self.assertEqual(self.e_123 * self.e_3, self.e_12) + self.assertEqual(self.e_123 * self.e_01, self.e_032) + self.assertEqual(self.e_123 * self.e_02, self.e_013) + self.assertEqual(self.e_123 * self.e_03, self.e_021) + self.assertEqual(self.e_123 * self.e_12, -self.e_3) + self.assertEqual(self.e_123 * self.e_31, -self.e_2) + self.assertEqual(self.e_123 * self.e_23, -self.e_1) + self.assertEqual(self.e_123 * self.e_021, -self.e_03) + self.assertEqual(self.e_123 * self.e_013, -self.e_02) + self.assertEqual(self.e_123 * self.e_032, -self.e_01) + self.assertEqual(self.e_123 * self.e_123, -S.One) + self.assertEqual(self.e_123 * self.e_0123, self.e_0) # e0123 - self.assertEquals(self.e_0123 * self.e_0, S.Zero) - self.assertEquals(self.e_0123 * self.e_1, -self.e_032) - self.assertEquals(self.e_0123 * self.e_2, -self.e_013) - self.assertEquals(self.e_0123 * self.e_3, -self.e_021) - self.assertEquals(self.e_0123 * self.e_01, S.Zero) - self.assertEquals(self.e_0123 * self.e_02, S.Zero) - self.assertEquals(self.e_0123 * self.e_03, S.Zero) - self.assertEquals(self.e_0123 * self.e_12, -self.e_03) - self.assertEquals(self.e_0123 * self.e_31, -self.e_02) - self.assertEquals(self.e_0123 * self.e_23, -self.e_01) - self.assertEquals(self.e_0123 * self.e_021, S.Zero) - self.assertEquals(self.e_0123 * self.e_013, S.Zero) - self.assertEquals(self.e_0123 * self.e_032, S.Zero) - self.assertEquals(self.e_0123 * self.e_123, -self.e_0) - self.assertEquals(self.e_0123 * self.e_0123, S.Zero) + self.assertEqual(self.e_0123 * self.e_0, S.Zero) + self.assertEqual(self.e_0123 * self.e_1, -self.e_032) + self.assertEqual(self.e_0123 * self.e_2, -self.e_013) + self.assertEqual(self.e_0123 * self.e_3, -self.e_021) + self.assertEqual(self.e_0123 * self.e_01, S.Zero) + self.assertEqual(self.e_0123 * self.e_02, S.Zero) + self.assertEqual(self.e_0123 * self.e_03, S.Zero) + self.assertEqual(self.e_0123 * self.e_12, -self.e_03) + self.assertEqual(self.e_0123 * self.e_31, -self.e_02) + self.assertEqual(self.e_0123 * self.e_23, -self.e_01) + self.assertEqual(self.e_0123 * self.e_021, S.Zero) + self.assertEqual(self.e_0123 * self.e_013, S.Zero) + self.assertEqual(self.e_0123 * self.e_032, S.Zero) + self.assertEqual(self.e_0123 * self.e_123, -self.e_0) + self.assertEqual(self.e_0123 * self.e_0123, S.Zero) def test_J(self): """ @@ -389,9 +389,9 @@ def test_J(self): PGA = self.PGA for k in range(PGA.n + 1): X = PGA.mv('x', k, 'grade') - self.assertEquals(X, Jinv(J(X))) + self.assertEqual(X, Jinv(J(X))) X = PGA.mv('x', 'mv') - self.assertEquals(X, Jinv(J(X))) + self.assertEqual(X, Jinv(J(X))) def test_geometry_incidence_planes_meet_into_points(self): """ @@ -404,12 +404,12 @@ def test_geometry_incidence_planes_meet_into_points(self): p2 = self.plane(0, 1, 0, -y) p3 = self.plane(0, 0, 1, -z) R = self.point(x, y, z) - self.assertProjEquals(R, p1 ^ p2 ^ p3) - self.assertProjEquals(R, p1 ^ p3 ^ p2) - self.assertProjEquals(R, p2 ^ p1 ^ p3) - self.assertProjEquals(R, p2 ^ p3 ^ p1) - self.assertProjEquals(R, p3 ^ p1 ^ p2) - self.assertProjEquals(R, p3 ^ p2 ^ p1) + self.assertProjEqual(R, p1 ^ p2 ^ p3) + self.assertProjEqual(R, p1 ^ p3 ^ p2) + self.assertProjEqual(R, p2 ^ p1 ^ p3) + self.assertProjEqual(R, p2 ^ p3 ^ p1) + self.assertProjEqual(R, p3 ^ p1 ^ p2) + self.assertProjEqual(R, p3 ^ p2 ^ p1) def test_geometry_incidence_planes_meet_into_points_2(self): """ @@ -445,7 +445,7 @@ def test_geometry_incidence_points_join_into_planes(self): pp = Jinv(J(P1) ^ J(P2) ^ J(P3)) pm = Jinv(J(P1) ^ J(P3) ^ J(P2)) - self.assertEquals(pp, -pm) + self.assertEqual(pp, -pm) a = Symbol('a') b = Symbol('b') @@ -453,15 +453,15 @@ def test_geometry_incidence_points_join_into_planes(self): coefs = pp.blade_coefs([self.e_0, self.e_1, self.e_2, self.e_3]) p = coefs[0] + a * coefs[1] + b * coefs[2] + c * coefs[3] - self.assertEquals(p.subs({a: x1, b: y1, c: z1}), S.Zero) - self.assertEquals(p.subs({a: x2, b: y2, c: z2}), S.Zero) - self.assertEquals(p.subs({a: x3, b: y3, c: z3}), S.Zero) + self.assertEqual(p.subs({a: x1, b: y1, c: z1}), S.Zero) + self.assertEqual(p.subs({a: x2, b: y2, c: z2}), S.Zero) + self.assertEqual(p.subs({a: x3, b: y3, c: z3}), S.Zero) coefs = pm.blade_coefs([self.e_0, self.e_1, self.e_2, self.e_3]) p = coefs[0] + a * coefs[1] + b * coefs[2] + c * coefs[3] - self.assertEquals(p.subs({a: x1, b: y1, c: z1}), S.Zero) - self.assertEquals(p.subs({a: x2, b: y2, c: z2}), S.Zero) - self.assertEquals(p.subs({a: x3, b: y3, c: z3}), S.Zero) + self.assertEqual(p.subs({a: x1, b: y1, c: z1}), S.Zero) + self.assertEqual(p.subs({a: x2, b: y2, c: z2}), S.Zero) + self.assertEqual(p.subs({a: x3, b: y3, c: z3}), S.Zero) def test_geometry_incidence_join_and_plane_side(self): """ @@ -493,7 +493,7 @@ def test_geometry_incidence_points_join_into_lines(self): lp = Jinv(J(P1) ^ J(P2)) lm = Jinv(J(P2) ^ J(P1)) - self.assertEquals(lp, -lm) + self.assertEqual(lp, -lm) # TODO: add more tests... @@ -510,10 +510,10 @@ def test_metric_distance_of_points(self): d = sqrt(abs((x1 - x0) ** 2 + (y1 - y0) ** 2 + (z1 - z0) ** 2)) lp = Jinv(J(P0) ^ J(P1)) - self.assertEquals(self.norm(lp), d) + self.assertEqual(self.norm(lp), d) lm = Jinv(J(P1) ^ J(P0)) - self.assertEquals(self.norm(lm), d) + self.assertEqual(self.norm(lm), d) def test_metric_angle_of_intersecting_planes(self): """ @@ -524,7 +524,7 @@ def test_metric_angle_of_intersecting_planes(self): p1 = self.plane(1, 0, 0, -x) p2 = self.plane(0, 1, 0, -y) - self.assertEquals(asin(self.norm(p1 ^ p2)), pi / 2) + self.assertEqual(asin(self.norm(p1 ^ p2)), pi / 2) def test_metric_distance_between_parallel_planes(self): """ @@ -536,7 +536,7 @@ def test_metric_distance_between_parallel_planes(self): p1 = self.plane(nx / n_norm, ny / n_norm, nz / n_norm, -x) p2 = self.plane(nx / n_norm, ny / n_norm, nz / n_norm, -y) - self.assertEquals(self.ideal_norm(p1 ^ p2), sqrt((x - y) ** 2)) + self.assertEqual(self.ideal_norm(p1 ^ p2), sqrt((x - y) ** 2)) def test_metric_oriented_distance_between_point_and_plane(self): """ @@ -548,11 +548,11 @@ def test_metric_oriented_distance_between_point_and_plane(self): d0 = Symbol('d0') p0 = self.plane(1, 0, 0, -d0) - self.assertEquals(Jinv(J(P0) ^ J(p0)), d0 - x0) - self.assertEquals(Jinv(J(p0) ^ J(P0)), x0 - d0) + self.assertEqual(Jinv(J(P0) ^ J(p0)), d0 - x0) + self.assertEqual(Jinv(J(p0) ^ J(P0)), x0 - d0) # TODO: We could assume d0 - x0 is not null for simplifying further - self.assertEquals(self.ideal_norm(P0 ^ p0), sqrt((d0 - x0) ** 2)) + self.assertEqual(self.ideal_norm(P0 ^ p0), sqrt((d0 - x0) ** 2)) def test_metric_oriented_distance_between_point_and_line(self): """ @@ -571,10 +571,10 @@ def test_metric_oriented_distance_between_point_and_line(self): d = self.norm(Jinv(J(P0) ^ J(l0))) - self.assertEquals(d.subs({x0: 2, y0: 0, z0: 0, x1: 0, y1: 2, z1: 0, x2: 1, y2: 2, z2: 0}), 2) - self.assertEquals(d.subs({x0: 3, y0: 0, z0: 0, x1: 0, y1: 2, z1: 0, x2: 1, y2: 2, z2: 0}), 2) - self.assertEquals(d.subs({x0: 2, y0: 0, z0: 0, x1: 0, y1: 1, z1: 0, x2: 1, y2: 1, z2: 0}), 1) - self.assertEquals(d.subs({x0: 2, y0: 0, z0: 0, x1: 0, y1: 2, z1: 0, x2: 2, y2: 0, z2: 0}), 0) + self.assertEqual(d.subs({x0: 2, y0: 0, z0: 0, x1: 0, y1: 2, z1: 0, x2: 1, y2: 2, z2: 0}), 2) + self.assertEqual(d.subs({x0: 3, y0: 0, z0: 0, x1: 0, y1: 2, z1: 0, x2: 1, y2: 2, z2: 0}), 2) + self.assertEqual(d.subs({x0: 2, y0: 0, z0: 0, x1: 0, y1: 1, z1: 0, x2: 1, y2: 1, z2: 0}), 1) + self.assertEqual(d.subs({x0: 2, y0: 0, z0: 0, x1: 0, y1: 2, z1: 0, x2: 2, y2: 0, z2: 0}), 0) def test_metric_common_normal_line(self): """ @@ -605,8 +605,8 @@ def test_metric_common_normal_line(self): self.assertTrue(dot0.is_scalar()) self.assertTrue(dot1.is_scalar()) - self.assertEquals(acos(dot0.scalar()), S.Half * pi) - self.assertEquals(acos(dot1.scalar()), S.Half * pi) + self.assertEqual(acos(dot0.scalar()), S.Half * pi) + self.assertEqual(acos(dot1.scalar()), S.Half * pi) def test_metric_angle_between_lines(self): """ @@ -626,7 +626,7 @@ def test_metric_angle_between_lines(self): dot = l0 | l1 self.assertTrue(dot.is_scalar()) - self.assertEquals(acos(dot.scalar()), pi / 2) + self.assertEqual(acos(dot.scalar()), pi / 2) @staticmethod def rotor_cs(alpha, l): @@ -665,117 +665,117 @@ def test_motors_rotator(self): # Around X+ RXp = self.rotor_cs(pi / 2, lXp) - self.assertEquals(RXp, self.rotor_exp(pi / 2, lXp)) + self.assertEqual(RXp, self.rotor_exp(pi / 2, lXp)) # Points - self.assertProjEquals(RXp * Yp * RXp.rev(), Zp) - self.assertProjEquals(RXp * Zp * RXp.rev(), Ym) - self.assertProjEquals(RXp * Ym * RXp.rev(), Zm) - self.assertProjEquals(RXp * Zm * RXp.rev(), Yp) + self.assertProjEqual(RXp * Yp * RXp.rev(), Zp) + self.assertProjEqual(RXp * Zp * RXp.rev(), Ym) + self.assertProjEqual(RXp * Ym * RXp.rev(), Zm) + self.assertProjEqual(RXp * Zm * RXp.rev(), Yp) # Lines - self.assertProjEquals(RXp * lYp * RXp.rev(), lZp) - self.assertProjEquals(RXp * lZp * RXp.rev(), lYm) - self.assertProjEquals(RXp * lYm * RXp.rev(), lZm) - self.assertProjEquals(RXp * lZm * RXp.rev(), lYp) + self.assertProjEqual(RXp * lYp * RXp.rev(), lZp) + self.assertProjEqual(RXp * lZp * RXp.rev(), lYm) + self.assertProjEqual(RXp * lYm * RXp.rev(), lZm) + self.assertProjEqual(RXp * lZm * RXp.rev(), lYp) # Planes - self.assertProjEquals(RXp * pYp * RXp.rev(), pZp) - self.assertProjEquals(RXp * pZp * RXp.rev(), pYm) - self.assertProjEquals(RXp * pYm * RXp.rev(), pZm) - self.assertProjEquals(RXp * pZm * RXp.rev(), pYp) + self.assertProjEqual(RXp * pYp * RXp.rev(), pZp) + self.assertProjEqual(RXp * pZp * RXp.rev(), pYm) + self.assertProjEqual(RXp * pYm * RXp.rev(), pZm) + self.assertProjEqual(RXp * pZm * RXp.rev(), pYp) # Around X- RXm = self.rotor_cs(pi / 2, lXm) - self.assertEquals(RXm, self.rotor_exp(pi / 2, lXm)) + self.assertEqual(RXm, self.rotor_exp(pi / 2, lXm)) # Points - self.assertProjEquals(RXm * Yp * RXm.rev(), Zm) - self.assertProjEquals(RXm * Zm * RXm.rev(), Ym) - self.assertProjEquals(RXm * Ym * RXm.rev(), Zp) - self.assertProjEquals(RXm * Zp * RXm.rev(), Yp) + self.assertProjEqual(RXm * Yp * RXm.rev(), Zm) + self.assertProjEqual(RXm * Zm * RXm.rev(), Ym) + self.assertProjEqual(RXm * Ym * RXm.rev(), Zp) + self.assertProjEqual(RXm * Zp * RXm.rev(), Yp) # Lines - self.assertProjEquals(RXm * lYp * RXm.rev(), lZm) - self.assertProjEquals(RXm * lZm * RXm.rev(), lYm) - self.assertProjEquals(RXm * lYm * RXm.rev(), lZp) - self.assertProjEquals(RXm * lZp * RXm.rev(), lYp) + self.assertProjEqual(RXm * lYp * RXm.rev(), lZm) + self.assertProjEqual(RXm * lZm * RXm.rev(), lYm) + self.assertProjEqual(RXm * lYm * RXm.rev(), lZp) + self.assertProjEqual(RXm * lZp * RXm.rev(), lYp) # Planes - self.assertProjEquals(RXm * pYp * RXm.rev(), pZm) - self.assertProjEquals(RXm * pZm * RXm.rev(), pYm) - self.assertProjEquals(RXm * pYm * RXm.rev(), pZp) - self.assertProjEquals(RXm * pZp * RXm.rev(), pYp) + self.assertProjEqual(RXm * pYp * RXm.rev(), pZm) + self.assertProjEqual(RXm * pZm * RXm.rev(), pYm) + self.assertProjEqual(RXm * pYm * RXm.rev(), pZp) + self.assertProjEqual(RXm * pZp * RXm.rev(), pYp) # Around Y+ RYp = self.rotor_cs(pi / 2, lYp) - self.assertEquals(RYp, self.rotor_exp(pi / 2, lYp)) + self.assertEqual(RYp, self.rotor_exp(pi / 2, lYp)) # Points - self.assertProjEquals(RYp * Xp * RYp.rev(), Zm) - self.assertProjEquals(RYp * Zm * RYp.rev(), Xm) - self.assertProjEquals(RYp * Xm * RYp.rev(), Zp) - self.assertProjEquals(RYp * Zp * RYp.rev(), Xp) + self.assertProjEqual(RYp * Xp * RYp.rev(), Zm) + self.assertProjEqual(RYp * Zm * RYp.rev(), Xm) + self.assertProjEqual(RYp * Xm * RYp.rev(), Zp) + self.assertProjEqual(RYp * Zp * RYp.rev(), Xp) # Lines - self.assertProjEquals(RYp * lXp * RYp.rev(), lZm) - self.assertProjEquals(RYp * lZm * RYp.rev(), lXm) - self.assertProjEquals(RYp * lXm * RYp.rev(), lZp) - self.assertProjEquals(RYp * lZp * RYp.rev(), lXp) + self.assertProjEqual(RYp * lXp * RYp.rev(), lZm) + self.assertProjEqual(RYp * lZm * RYp.rev(), lXm) + self.assertProjEqual(RYp * lXm * RYp.rev(), lZp) + self.assertProjEqual(RYp * lZp * RYp.rev(), lXp) # Planes - self.assertProjEquals(RYp * pXp * RYp.rev(), pZm) - self.assertProjEquals(RYp * pZm * RYp.rev(), pXm) - self.assertProjEquals(RYp * pXm * RYp.rev(), pZp) - self.assertProjEquals(RYp * pZp * RYp.rev(), pXp) + self.assertProjEqual(RYp * pXp * RYp.rev(), pZm) + self.assertProjEqual(RYp * pZm * RYp.rev(), pXm) + self.assertProjEqual(RYp * pXm * RYp.rev(), pZp) + self.assertProjEqual(RYp * pZp * RYp.rev(), pXp) # Around Y- RYm = self.rotor_cs(pi / 2, lYm) - self.assertEquals(RYm, self.rotor_exp(pi / 2, lYm)) + self.assertEqual(RYm, self.rotor_exp(pi / 2, lYm)) # Points - self.assertProjEquals(RYm * Xp * RYm.rev(), Zp) - self.assertProjEquals(RYm * Zp * RYm.rev(), Xm) - self.assertProjEquals(RYm * Xm * RYm.rev(), Zm) - self.assertProjEquals(RYm * Zm * RYm.rev(), Xp) + self.assertProjEqual(RYm * Xp * RYm.rev(), Zp) + self.assertProjEqual(RYm * Zp * RYm.rev(), Xm) + self.assertProjEqual(RYm * Xm * RYm.rev(), Zm) + self.assertProjEqual(RYm * Zm * RYm.rev(), Xp) # Lines - self.assertProjEquals(RYm * lXp * RYm.rev(), lZp) - self.assertProjEquals(RYm * lZp * RYm.rev(), lXm) - self.assertProjEquals(RYm * lXm * RYm.rev(), lZm) - self.assertProjEquals(RYm * lZm * RYm.rev(), lXp) + self.assertProjEqual(RYm * lXp * RYm.rev(), lZp) + self.assertProjEqual(RYm * lZp * RYm.rev(), lXm) + self.assertProjEqual(RYm * lXm * RYm.rev(), lZm) + self.assertProjEqual(RYm * lZm * RYm.rev(), lXp) # Planes - self.assertProjEquals(RYm * pXp * RYm.rev(), pZp) - self.assertProjEquals(RYm * pZp * RYm.rev(), pXm) - self.assertProjEquals(RYm * pXm * RYm.rev(), pZm) - self.assertProjEquals(RYm * pZm * RYm.rev(), pXp) + self.assertProjEqual(RYm * pXp * RYm.rev(), pZp) + self.assertProjEqual(RYm * pZp * RYm.rev(), pXm) + self.assertProjEqual(RYm * pXm * RYm.rev(), pZm) + self.assertProjEqual(RYm * pZm * RYm.rev(), pXp) # Around Z+ RZp = self.rotor_cs(pi / 2, lZp) - self.assertEquals(RZp, self.rotor_exp(pi / 2, lZp)) + self.assertEqual(RZp, self.rotor_exp(pi / 2, lZp)) # Points - self.assertProjEquals(RZp * Xp * RZp.rev(), Yp) - self.assertProjEquals(RZp * Yp * RZp.rev(), Xm) - self.assertProjEquals(RZp * Xm * RZp.rev(), Ym) - self.assertProjEquals(RZp * Ym * RZp.rev(), Xp) + self.assertProjEqual(RZp * Xp * RZp.rev(), Yp) + self.assertProjEqual(RZp * Yp * RZp.rev(), Xm) + self.assertProjEqual(RZp * Xm * RZp.rev(), Ym) + self.assertProjEqual(RZp * Ym * RZp.rev(), Xp) # Lines - self.assertProjEquals(RZp * lXp * RZp.rev(), lYp) - self.assertProjEquals(RZp * lYp * RZp.rev(), lXm) - self.assertProjEquals(RZp * lXm * RZp.rev(), lYm) - self.assertProjEquals(RZp * lYm * RZp.rev(), lXp) + self.assertProjEqual(RZp * lXp * RZp.rev(), lYp) + self.assertProjEqual(RZp * lYp * RZp.rev(), lXm) + self.assertProjEqual(RZp * lXm * RZp.rev(), lYm) + self.assertProjEqual(RZp * lYm * RZp.rev(), lXp) # Planes - self.assertProjEquals(RZp * pXp * RZp.rev(), pYp) - self.assertProjEquals(RZp * pYp * RZp.rev(), pXm) - self.assertProjEquals(RZp * pXm * RZp.rev(), pYm) - self.assertProjEquals(RZp * pYm * RZp.rev(), pXp) + self.assertProjEqual(RZp * pXp * RZp.rev(), pYp) + self.assertProjEqual(RZp * pYp * RZp.rev(), pXm) + self.assertProjEqual(RZp * pXm * RZp.rev(), pYm) + self.assertProjEqual(RZp * pYm * RZp.rev(), pXp) # Around Z- RZm = self.rotor_cs(pi / 2, lZm) - self.assertEquals(RZm, self.rotor_exp(pi / 2, lZm)) + self.assertEqual(RZm, self.rotor_exp(pi / 2, lZm)) # Points - self.assertProjEquals(RZm * Xp * RZm.rev(), Ym) - self.assertProjEquals(RZm * Ym * RZm.rev(), Xm) - self.assertProjEquals(RZm * Xm * RZm.rev(), Yp) - self.assertProjEquals(RZm * Yp * RZm.rev(), Xp) + self.assertProjEqual(RZm * Xp * RZm.rev(), Ym) + self.assertProjEqual(RZm * Ym * RZm.rev(), Xm) + self.assertProjEqual(RZm * Xm * RZm.rev(), Yp) + self.assertProjEqual(RZm * Yp * RZm.rev(), Xp) # Lines - self.assertProjEquals(RZm * lXp * RZm.rev(), lYm) - self.assertProjEquals(RZm * lYm * RZm.rev(), lXm) - self.assertProjEquals(RZm * lXm * RZm.rev(), lYp) - self.assertProjEquals(RZm * lYp * RZm.rev(), lXp) + self.assertProjEqual(RZm * lXp * RZm.rev(), lYm) + self.assertProjEqual(RZm * lYm * RZm.rev(), lXm) + self.assertProjEqual(RZm * lXm * RZm.rev(), lYp) + self.assertProjEqual(RZm * lYp * RZm.rev(), lXp) # Planes - self.assertProjEquals(RZm * pXp * RZm.rev(), pYm) - self.assertProjEquals(RZm * pYm * RZm.rev(), pXm) - self.assertProjEquals(RZm * pXm * RZm.rev(), pYp) - self.assertProjEquals(RZm * pYp * RZm.rev(), pXp) + self.assertProjEqual(RZm * pXp * RZm.rev(), pYm) + self.assertProjEqual(RZm * pYm * RZm.rev(), pXm) + self.assertProjEqual(RZm * pXm * RZm.rev(), pYp) + self.assertProjEqual(RZm * pYp * RZm.rev(), pXp) def translator(self, d, l): return 1 + (d / 2) * (l * self.e_0123) @@ -793,4 +793,4 @@ def test_motors_translator(self): d = Symbol('d') T = self.translator(d, l) - self.assertProjEquals(T * Px * T.rev(), self.point(x0 + d, y0, z0)) # TODO : like ganja.js but weird... + self.assertProjEqual(T * Px * T.rev(), self.point(x0 + d, y0, z0)) # TODO : like ganja.js but weird... diff --git a/tests/test_utils.py b/tests/test_utils.py index f7d0fe96..3571b580 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -14,7 +14,7 @@ def com(A, B): class TestCase(unittest.TestCase): - def assertEquals(self, first, second): + def assertEqual(self, first, second): """ Compare two expressions are equals. """ @@ -28,7 +28,7 @@ def assertEquals(self, first, second): self.assertTrue(diff == 0, "\n%s\n==\n%s\n%s" % (first, second, diff)) - def assertProjEquals(self, X, Y): + def assertProjEqual(self, X, Y): """ Compare two points, two planes or two lines up to a scalar. """ @@ -45,7 +45,7 @@ def assertProjEquals(self, X, Y): self.assertTrue(diff == S.Zero, "\n%s\n==\n%s" % (X, Y)) - def assertNotEquals(self, first, second): + def assertNotEqual(self, first, second): """ Compare two expressions are not equals. """ From e376c59f596d71644e707a7cd64ef2bbdde6e3d8 Mon Sep 17 00:00:00 2001 From: Sylvain Meunier Date: Mon, 25 Nov 2019 22:04:23 +0100 Subject: [PATCH 74/78] Move all tests to test --- {tests => test}/test_chapter11.py | 0 {tests => test}/test_chapter13.py | 0 {tests => test}/test_chapter2.py | 0 {tests => test}/test_chapter3.py | 0 {tests => test}/test_chapter6.py | 0 {tests => test}/test_chapter7.py | 0 {tests => test}/test_chapter8.py | 0 {tests => test}/test_generator.py | 0 {tests => test}/test_pga.py | 0 {tests => test}/test_utils.py | 0 10 files changed, 0 insertions(+), 0 deletions(-) rename {tests => test}/test_chapter11.py (100%) rename {tests => test}/test_chapter13.py (100%) rename {tests => test}/test_chapter2.py (100%) rename {tests => test}/test_chapter3.py (100%) rename {tests => test}/test_chapter6.py (100%) rename {tests => test}/test_chapter7.py (100%) rename {tests => test}/test_chapter8.py (100%) rename {tests => test}/test_generator.py (100%) rename {tests => test}/test_pga.py (100%) rename {tests => test}/test_utils.py (100%) diff --git a/tests/test_chapter11.py b/test/test_chapter11.py similarity index 100% rename from tests/test_chapter11.py rename to test/test_chapter11.py diff --git a/tests/test_chapter13.py b/test/test_chapter13.py similarity index 100% rename from tests/test_chapter13.py rename to test/test_chapter13.py diff --git a/tests/test_chapter2.py b/test/test_chapter2.py similarity index 100% rename from tests/test_chapter2.py rename to test/test_chapter2.py diff --git a/tests/test_chapter3.py b/test/test_chapter3.py similarity index 100% rename from tests/test_chapter3.py rename to test/test_chapter3.py diff --git a/tests/test_chapter6.py b/test/test_chapter6.py similarity index 100% rename from tests/test_chapter6.py rename to test/test_chapter6.py diff --git a/tests/test_chapter7.py b/test/test_chapter7.py similarity index 100% rename from tests/test_chapter7.py rename to test/test_chapter7.py diff --git a/tests/test_chapter8.py b/test/test_chapter8.py similarity index 100% rename from tests/test_chapter8.py rename to test/test_chapter8.py diff --git a/tests/test_generator.py b/test/test_generator.py similarity index 100% rename from tests/test_generator.py rename to test/test_generator.py diff --git a/tests/test_pga.py b/test/test_pga.py similarity index 100% rename from tests/test_pga.py rename to test/test_pga.py diff --git a/tests/test_utils.py b/test/test_utils.py similarity index 100% rename from tests/test_utils.py rename to test/test_utils.py From c4693c8669534d858f4d64fedf59da5a5ac3fc4a Mon Sep 17 00:00:00 2001 From: Sylvain Meunier Date: Fri, 6 Dec 2019 20:17:24 +0100 Subject: [PATCH 75/78] Refactor chapter 13 --- test/test_chapter13.py | 92 +++++++++++++++++++++--------------------- 1 file changed, 47 insertions(+), 45 deletions(-) diff --git a/test/test_chapter13.py b/test/test_chapter13.py index 21587899..ba036c76 100644 --- a/test/test_chapter13.py +++ b/test/test_chapter13.py @@ -6,9 +6,9 @@ class TestChapter13(TestCase): - def test_13_1_2(self): + def setUp(self): """ - Points as null vectors. + Initialize CGA. """ g = [ [+0, +0, +0, +0, -1], @@ -18,16 +18,44 @@ def test_13_1_2(self): [-1, +0, +0, +1, +0], ] - GA, o, e_1, e_2, e_3, inf = Ga.build('o e1 e2 e3 inf', g=g) + self.ga, self.o, self.e_1, self.e_2, self.e_3, self.inf = Ga.build('o e1 e2 e3 inf', g=g) - def vector(x, y, z): - return x * e_1 + y * e_2 + z * e_3 + def vector(self, x, y, z): + """ + Make a vector. + """ + return x * self.e_1 + y * self.e_2 + z * self.e_3 - def point(v): - return o + v + S.Half * v * v * inf + def point(self, alpha, v): + """ + Make a point. + """ + return alpha * (self.o + v + S.Half * v * v * self.inf) - p = point(vector(Symbol('px', real=True), Symbol('py', real=True), Symbol('pz', real=True))) - q = point(vector(Symbol('qx', real=True), Symbol('qy', real=True), Symbol('qz', real=True))) + def dual_plane(self, n, delta): + """ + Make a dual plane. + """ + return n + delta * self.inf + + def dual_sphere(self, alpha, c, r): + """ + Make a dual sphere. + """ + return alpha * (c - S.Half * r * r * self.inf) + + def dual_im_sphere(self, alpha, c, r): + """ + Make a dual imaginary sphere. + """ + return alpha * (c + S.Half * r * r * self.inf) + + def test_13_1_2(self): + """ + Points as null vectors. + """ + p = self.point(1, self.vector(Symbol('px', real=True), Symbol('py', real=True), Symbol('pz', real=True))) + q = self.point(1, self.vector(Symbol('qx', real=True), Symbol('qy', real=True), Symbol('qz', real=True))) self.assertEqual(p | q, -S.Half * q * q + p | q - S.Half * p * p) self.assertEqual(p | q, -S.Half * (q - p) * (q - p)) @@ -36,59 +64,33 @@ def test_13_1_3(self): """ General vectors represent dual planes and spheres. """ - g = [ - [+0, +0, +0, +0, -1], - [+0, +1, +0, +0, +0], - [+0, +0, +1, +0, +0], - [+0, +0, +0, +1, +0], - [-1, +0, +0, +1, +0], - ] - - GA, o, e_1, e_2, e_3, inf = Ga.build('o e1 e2 e3 inf', g=g) - - def vector(x, y, z): - return x * e_1 + y * e_2 + z * e_3 - - # Point - def point(alpha, v): - return alpha * (o + v + S.Half * v * v * inf) - alpha = Symbol('alpha', real=True) - p = point(alpha, vector(Symbol('px', real=True), Symbol('py', real=True), Symbol('pz', real=True))) + p = self.point(alpha, self.vector(Symbol('px', real=True), Symbol('py', real=True), Symbol('pz', real=True))) self.assertEqual(p | p, S.Zero) - self.assertEqual(inf | p, -alpha) + self.assertEqual(self.inf | p, -alpha) # Dual plane - def dual_plane(n, delta): - return n + delta * inf - nx = Symbol('nx', real=True) ny = Symbol('ny', real=True) nz = Symbol('nz', real=True) - p = dual_plane(vector(nx, ny, nz), Symbol('delta', real=True)) + p = self.dual_plane(self.vector(nx, ny, nz), Symbol('delta', real=True)) self.assertEqual(p | p, nx * nx + ny * ny + nz * nz) - self.assertEqual(inf | p, S.Zero) + self.assertEqual(self.inf | p, S.Zero) # Dual sphere - def dual_sphere(alpha, c, r): - return alpha * (c - S.Half * r * r * inf) - - def dual_im_sphere(alpha, c, r): - return alpha * (c + S.Half * r * r * inf) - cx = Symbol('cx', real=True) cy = Symbol('cy', real=True) cz = Symbol('cz', real=True) r = Symbol('r', real=True) - c = point(1, vector(cx, cy, cz)) + c = self.point(1, self.vector(cx, cy, cz)) self.assertEqual(c * c, S.Zero) - self.assertEqual(-inf | c, S.One) + self.assertEqual(-self.inf | c, S.One) - s = dual_sphere(alpha, c, r) + s = self.dual_sphere(alpha, c, r) self.assertEqual(s | s, alpha * alpha * r * r) - self.assertEqual(-inf | s, alpha) + self.assertEqual(-self.inf | s, alpha) - im_s = dual_im_sphere(alpha, c, r) + im_s = self.dual_im_sphere(alpha, c, r) self.assertEqual(im_s | im_s, -alpha * alpha * r * r) - self.assertEqual(-inf | im_s, alpha) + self.assertEqual(-self.inf | im_s, alpha) From 3c4a5b88c7bb7e175b9eb43e0c7622a8dde5cd5a Mon Sep 17 00:00:00 2001 From: Sylvain Meunier Date: Fri, 6 Dec 2019 21:44:21 +0100 Subject: [PATCH 76/78] chapter 13 exercises (in progress) --- test/test_chapter13.py | 66 +++++++++++++++++++++++++++++++++++++----- 1 file changed, 59 insertions(+), 7 deletions(-) diff --git a/test/test_chapter13.py b/test/test_chapter13.py index ba036c76..ff1ddc7d 100644 --- a/test/test_chapter13.py +++ b/test/test_chapter13.py @@ -1,6 +1,6 @@ from .test_utils import TestCase -from sympy import Symbol, S +from sympy import Symbol, S, sqrt from galgebra.ga import Ga @@ -11,14 +11,43 @@ def setUp(self): Initialize CGA. """ g = [ - [+0, +0, +0, +0, -1], - [+0, +1, +0, +0, +0], - [+0, +0, +1, +0, +0], - [+0, +0, +0, +1, +0], - [-1, +0, +0, +1, +0], + [ 1, 0, 0, 0, 0], + [ 0, 1, 0, 0, 0], + [ 0, 0, 1, 0, 0], + [ 0, 0, 0, 1, 0], + [ 0, 0, 0, 0, -1], ] - self.ga, self.o, self.e_1, self.e_2, self.e_3, self.inf = Ga.build('o e1 e2 e3 inf', g=g) + ga, e, e_1, e_2, e_3, eb = Ga.build('e e1 e2 e3 eb', g=g) + o = S.Half * (e + eb) + inf = eb - e + + self.ga = ga + self.o = o + self.e_1 = e_1 + self.e_2 = e_2 + self.e_3 = e_3 + self.inf = inf + + """ + # test_13_2_2 will fails with this + + g = [ + [ 0, 0, 0, 0, -1], + [ 0, 1, 0, 0, 0], + [ 0, 0, 1, 0, 0], + [ 0, 0, 0, 1, 0], + [-1, 0, 0, 0, 0], + ] + + ga, o, e_1, e_2, e_3, inf = Ga.build('o e1 e2 e3 inf', g=g) + self.ga = ga + self.o = o + self.e_1 = e_1 + self.e_2 = e_2 + self.e_3 = e_3 + self.inf = inf + """ def vector(self, x, y, z): """ @@ -94,3 +123,26 @@ def test_13_1_3(self): im_s = self.dual_im_sphere(alpha, c, r) self.assertEqual(im_s | im_s, -alpha * alpha * r * r) self.assertEqual(-self.inf | im_s, alpha) + + def test_13_2_2(self): + """ + Proper Euclidean motions as even versors : Translations. + """ + + nx = Symbol('nx', real=True) + ny = Symbol('ny', real=True) + nz = Symbol('nz', real=True) + + n = self.vector(nx, ny, nz) / sqrt(nx * nx + ny * ny + nz * nz) + delta_1 = Symbol('delta_1', real=True) + delta_2 = Symbol('delta_2', real=True) + + Tt = self.dual_plane(n, delta_2) * self.dual_plane(n, delta_1) + + # Even versor + self.assertEqual(Tt * Tt.rev(), 1) + + # Translation + r = Tt * self.o * Tt.inv() + t = self.point(1, 2 * (delta_2 - delta_1) * n) + self.assertEqual(r, t) From 5fc94d26cb095f109ab97f2539eb1fbd9da77e49 Mon Sep 17 00:00:00 2001 From: Sylvain Meunier Date: Sat, 7 Dec 2019 18:24:35 +0100 Subject: [PATCH 77/78] Fix assertEqual for difficult expressions --- test/test_chapter13.py | 57 +++++++++++++++++++----------------------- test/test_utils.py | 28 ++++++++++++++++----- 2 files changed, 48 insertions(+), 37 deletions(-) diff --git a/test/test_chapter13.py b/test/test_chapter13.py index ff1ddc7d..ce6645a1 100644 --- a/test/test_chapter13.py +++ b/test/test_chapter13.py @@ -3,6 +3,8 @@ from sympy import Symbol, S, sqrt from galgebra.ga import Ga +USE_DIAGONAL_G = True + class TestChapter13(TestCase): @@ -10,44 +12,37 @@ def setUp(self): """ Initialize CGA. """ - g = [ - [ 1, 0, 0, 0, 0], - [ 0, 1, 0, 0, 0], - [ 0, 0, 1, 0, 0], - [ 0, 0, 0, 1, 0], - [ 0, 0, 0, 0, -1], - ] - - ga, e, e_1, e_2, e_3, eb = Ga.build('e e1 e2 e3 eb', g=g) - o = S.Half * (e + eb) - inf = eb - e - - self.ga = ga - self.o = o - self.e_1 = e_1 - self.e_2 = e_2 - self.e_3 = e_3 - self.inf = inf - - """ - # test_13_2_2 will fails with this - - g = [ - [ 0, 0, 0, 0, -1], - [ 0, 1, 0, 0, 0], - [ 0, 0, 1, 0, 0], - [ 0, 0, 0, 1, 0], - [-1, 0, 0, 0, 0], - ] + if USE_DIAGONAL_G: + # This is way faster with galgebra but o and inf can't be printed... + g = [ + [ 1, 0, 0, 0, 0], + [ 0, 1, 0, 0, 0], + [ 0, 0, 1, 0, 0], + [ 0, 0, 0, 1, 0], + [ 0, 0, 0, 0, -1], + ] + + ga, e, e_1, e_2, e_3, eb = Ga.build('e e1 e2 e3 eb', g=g) + o = S.Half * (e + eb) + inf = eb - e + + else: + g = [ + [ 0, 0, 0, 0, -1], + [ 0, 1, 0, 0, 0], + [ 0, 0, 1, 0, 0], + [ 0, 0, 0, 1, 0], + [-1, 0, 0, 0, 0], + ] + + ga, o, e_1, e_2, e_3, inf = Ga.build('o e1 e2 e3 inf', g=g) - ga, o, e_1, e_2, e_3, inf = Ga.build('o e1 e2 e3 inf', g=g) self.ga = ga self.o = o self.e_1 = e_1 self.e_2 = e_2 self.e_3 = e_3 self.inf = inf - """ def vector(self, x, y, z): """ diff --git a/test/test_utils.py b/test/test_utils.py index 3571b580..0ab99ecc 100644 --- a/test/test_utils.py +++ b/test/test_utils.py @@ -1,8 +1,9 @@ import unittest -from sympy import S, simplify +from sympy import expand, S, simplify from galgebra.ga import Ga from galgebra.mv import Mv +from galgebra.metric import Simp def com(A, B): @@ -24,19 +25,29 @@ def assertEqual(self, first, second): if isinstance(second, Mv): second = second.obj + # We need to help sympy a little... + first = Simp.apply(expand(first)) + second = Simp.apply(expand(second)) + + # Check diff = simplify(first - second) self.assertTrue(diff == 0, "\n%s\n==\n%s\n%s" % (first, second, diff)) - def assertProjEqual(self, X, Y): + def assertProjEqual(self, first, second): """ Compare two points, two planes or two lines up to a scalar. """ - assert isinstance(X, Mv) - assert isinstance(Y, Mv) + assert isinstance(first, Mv) + assert isinstance(second, Mv) + + # TODO: this should use Mv methods and not the derived test case methods... + first /= self.norm(first) + second /= self.norm(second) - X /= self.norm(X) - Y /= self.norm(Y) + # We need to help sympy a little... + X = Simp.apply(expand(first.obj)) + Y = Simp.apply(expand(second.obj)) # We can't easily retrieve the sign, so we test both diff = simplify(X.obj - Y.obj) @@ -55,6 +66,11 @@ def assertNotEqual(self, first, second): if isinstance(second, Mv): second = second.obj + # We need to help sympy a little... + first = Simp.apply(expand(first)) + second = Simp.apply(expand(second)) + + # Check diff = simplify(first - second) self.assertTrue(diff != 0, "\n%s\n!=\n%s\n%s" % (first, second, diff)) From 3fea69ff4c4ca8f8afea083b697ef9d5112824b9 Mon Sep 17 00:00:00 2001 From: Sylvain Meunier Date: Sat, 7 Dec 2019 21:19:34 +0100 Subject: [PATCH 78/78] chapter 13 exercises (in progress) --- test/test_chapter13.py | 33 +++++++++++++++++++++++++++------ 1 file changed, 27 insertions(+), 6 deletions(-) diff --git a/test/test_chapter13.py b/test/test_chapter13.py index ce6645a1..ea4b800e 100644 --- a/test/test_chapter13.py +++ b/test/test_chapter13.py @@ -119,18 +119,19 @@ def test_13_1_3(self): self.assertEqual(im_s | im_s, -alpha * alpha * r * r) self.assertEqual(-self.inf | im_s, alpha) + def symbol_vector(self, name='n', normalize=False): + nx = Symbol(name + 'x', real=True) + ny = Symbol(name + 'y', real=True) + nz = Symbol(name + 'z', real=True) + return self.vector(nx, ny, nz) / (sqrt(nx * nx + ny * ny + nz * nz) if normalize else S.One) + def test_13_2_2(self): """ Proper Euclidean motions as even versors : Translations. """ - - nx = Symbol('nx', real=True) - ny = Symbol('ny', real=True) - nz = Symbol('nz', real=True) - - n = self.vector(nx, ny, nz) / sqrt(nx * nx + ny * ny + nz * nz) delta_1 = Symbol('delta_1', real=True) delta_2 = Symbol('delta_2', real=True) + n = self.symbol_vector('n', normalize=True) Tt = self.dual_plane(n, delta_2) * self.dual_plane(n, delta_1) @@ -141,3 +142,23 @@ def test_13_2_2(self): r = Tt * self.o * Tt.inv() t = self.point(1, 2 * (delta_2 - delta_1) * n) self.assertEqual(r, t) + + # TODO: This exponential isn't available in galgebra + #Te = (-t * self.inf * S.Half).exp() + #self.assertEqual(Te * Te.rev(), 1) + #self.assertEqual(Te, Tt) + + def test_13_2_3(self): + """ + Proper Euclidean motions as even versors : Rotations in the origin. + """ + n0 = self.symbol_vector('n0') + n1 = self.symbol_vector('n1') + R = n1 * n0 # 2 reflections + Rinv = R.inv() + + p = self.symbol_vector('p') + + p0 = R * self.point(1, p) * Rinv + p1 = self.point(1, R * p * Rinv) + self.assertEqual(p0, p1)