test_differential_ops.py
1 import operator 2 3 from sympy import symbols, S 4 import pytest 5 6 from galgebra.ga import Ga 7 from galgebra.mv import Dop, Mv 8 from galgebra.dop import Sdop, Pdop 9 10 11 class TestDop(object): 12 13 def test_associativity_and_distributivity(self): 14 coords = x, y, z = symbols('x y z', real=True) 15 ga, ex, ey, ez = Ga.build('e*x|y|z', g=[1, 1, 1], coords=coords) 16 v = ga.mv('v', 'vector', f=True) 17 laplacian = ga.grad * ga.grad 18 rlaplacian = ga.rgrad * ga.rgrad 19 20 # check addition distributes 21 assert (laplacian + ga.grad) * v == laplacian * v + ga.grad * v != 0 22 assert (laplacian + 1234567) * v == laplacian * v + 1234567 * v != 0 23 assert (1234 * ex + ga.grad) * v == 1234 * ex * v + ga.grad * v != 0 24 # check subtraction distributes 25 assert (laplacian - ga.grad) * v == laplacian * v - ga.grad * v != 0 26 assert (laplacian - 1234567) * v == laplacian * v - 1234567 * v != 0 27 assert (1234 * ex - ga.grad) * v == 1234 * ex * v - ga.grad * v != 0 28 # check unary subtraction distributes 29 assert (-ga.grad) * v == -(ga.grad * v) != 0 30 # check division is associative 31 assert v * (ga.rgrad / 2) == (v * ga.rgrad) / 2 != 0 32 # check multiplication is associative 33 assert (ex * ga.grad) * v == ex * (ga.grad * v) != 0 34 assert (20 * ga.grad) * v == 20 * (ga.grad * v) != 0 35 assert v * (ga.rgrad * ex) == (v * ga.rgrad) * ex != 0 36 assert v * (ga.rgrad * 20) == (v * ga.rgrad) * 20 != 0 37 assert (laplacian * ga.grad) * v == laplacian * (ga.grad * v) != 0 38 # check wedge is associative 39 assert (ex ^ ga.grad) ^ v == ex ^ (ga.grad ^ v) != 0 40 assert (20 ^ ga.grad) ^ v == 20 ^ (ga.grad ^ v) != 0 41 assert v ^ (ga.rgrad ^ ex) == (v ^ ga.rgrad) ^ ex != 0 42 assert v ^ (ga.rgrad ^ 20) == (v ^ ga.rgrad) ^ 20 != 0 43 44 def test_empty_dop(self): 45 """ Test that dop with zero terms is equivalent to multiplying by zero """ 46 coords = x, y, z = symbols('x y z', real=True) 47 ga, ex, ey, ez = Ga.build('e*x|y|z', g=[1, 1, 1], coords=coords) 48 v = ga.mv('v', 'vector', f=True) 49 50 make_zero = ga.dop([]) 51 assert make_zero * v == 0 52 assert make_zero * make_zero * v == 0 53 assert (make_zero + make_zero) * v == 0 54 assert (-make_zero) * v == 0 55 56 def test_misc(self): 57 """ Other miscellaneous tests """ 58 coords = x, y, z = symbols('x y z', real=True) 59 ga, ex, ey, ez = Ga.build('e*x|y|z', g=[1, 1, 1], coords=coords) 60 v = ga.mv('v', 'vector', f=True) 61 laplacian = ga.grad * ga.grad 62 rlaplacian = ga.rgrad * ga.rgrad 63 64 # laplacian is a scalar operator, so applying it from either side 65 # is the same 66 assert laplacian * v == v * rlaplacian 67 68 assert laplacian.is_scalar() 69 assert not ga.grad.is_scalar() 70 71 # test comparison 72 assert ga.grad == ga.grad 73 assert not (ga.grad != ga.grad) 74 assert ga.grad != laplacian 75 assert not (ga.grad == laplacian) 76 assert ga.grad != object() 77 assert not (ga.grad == object()) 78 79 # inconsistent cmpflg, not clear which side the operator goes on 80 with pytest.raises(ValueError): 81 ga.grad + ga.rgrad 82 with pytest.raises(ValueError): 83 ga.grad * ga.rgrad 84 85 def test_mixed_algebras(self): 86 coords = x, y, z = symbols('x y z', real=True) 87 ga1, ex1, ey1, ez1 = Ga.build('e1*x|y|z', g=[1, 1, 1], coords=coords) 88 ga2, ex2, ey2, ez2 = Ga.build('e2*x|y|z', g=[1, 1, 1], coords=coords) 89 assert ga1 != ga2 90 91 v1 = ga1.mv('v', 'vector', f=True) 92 v2 = ga2.mv('v', 'vector', f=True) 93 94 with pytest.raises(ValueError): 95 ga1.grad * v2 96 with pytest.raises(ValueError): 97 v1 * ga2.rgrad 98 with pytest.raises(ValueError): 99 ga1.grad * ga2.grad 100 101 def test_components(self): 102 coords = x, y, z = symbols('x y z', real=True) 103 ga, ex, ey, ez = Ga.build('e*x|y|z', g=[1, 1, 1], coords=coords) 104 105 components = ga.grad.components() 106 assert components == ( 107 ex * (ex | ga.grad), 108 ey * (ey | ga.grad), 109 ez * (ez | ga.grad), 110 ) 111 112 def test_constructor_errors(self): 113 ga, ex, ey, ez = Ga.build('e*x|y|z', g=[1, 1, 1]) 114 115 # list lengths must match 116 with pytest.raises(ValueError, match='same length'): 117 Dop([ex], [], ga=ga) 118 119 # the two conventions can't be mixed 120 mixed_args = [ 121 (ex, Pdop({})), 122 (Sdop([]), ex), 123 ] 124 with pytest.raises(TypeError, match='pairs'): 125 Dop(mixed_args, ga=ga) 126 127 # ga must be non-none 128 with pytest.raises(ValueError, match='must not be None'): 129 Dop([], ga=None) 130 131 # too few arguments 132 with pytest.raises(TypeError, match='0 were given'): 133 Dop(ga=ga) 134 # too many arguments 135 with pytest.raises(TypeError, match='3 were given'): 136 Dop(1, 2, 3, ga=ga) 137 138 139 class TestSdop(object): 140 141 def test_deprecation(self): 142 coords = x, y, z = symbols('x y z', real=True) 143 ga, ex, ey, ez = Ga.build('e*x|y|z', g=[1, 1, 1], coords=coords) 144 145 with pytest.warns(DeprecationWarning): 146 ga.sPds 147 148 def test_shorthand(self): 149 coords = x, y, z = symbols('x y z', real=True) 150 ga, ex, ey, ez = Ga.build('e*x|y|z', g=[1, 1, 1], coords=coords) 151 152 assert Sdop(x) == Sdop([(S(1), Pdop({x: 1}))]) 153 154 def test_empty_sdop(self): 155 """ Test that sdop with zero terms is equivalent to multiplying by zero """ 156 coords = x, y, z = symbols('x y z', real=True) 157 ga, ex, ey, ez = Ga.build('e*x|y|z', g=[1, 1, 1], coords=coords) 158 v = ga.mv('v', 'vector', f=True) 159 160 make_zero = Sdop([]) 161 assert make_zero * v == 0 162 assert make_zero * make_zero * v == 0 163 assert (make_zero + make_zero) * v == 0 164 assert (-make_zero) * v == 0 165 166 def test_associativity_and_distributivity(self): 167 coords = x, y, z = symbols('x y z', real=True) 168 ga, ex, ey, ez = Ga.build('e*x|y|z', g=[1, 1, 1], coords=coords) 169 v = ga.mv('v', 'vector', f=True) 170 laplacian = Sdop((ga.grad * ga.grad).terms) 171 172 # check addition distributes 173 assert (laplacian + 20) * v == laplacian * v + 20 * v != 0 174 assert (20 + laplacian) * v == laplacian * v + 20 * v != 0 175 # check subtraction distributes 176 assert (laplacian - 20) * v == laplacian * v - 20 * v != 0 177 assert (20 - laplacian) * v == 20 * v - laplacian * v != 0 178 # check unary subtraction distributes 179 assert (-laplacian) * v == -(laplacian * v) != 0 180 # check multiplication is associative 181 assert (20 * laplacian) * v == 20 * (laplacian * v) != 0 182 assert (laplacian * laplacian) * v == laplacian * (laplacian * v) != 0 183 184 185 def test_misc(self): 186 """ Other miscellaneous tests """ 187 coords = x, y, z = symbols('x y z', real=True) 188 ga, ex, ey, ez = Ga.build('e*x|y|z', g=[1, 1, 1], coords=coords) 189 laplacian = ga.sdop((ga.grad * ga.grad).terms) 190 lap2 = laplacian*laplacian 191 192 # test comparison 193 assert lap2 == lap2 194 assert not (lap2 != lap2) 195 assert lap2 != laplacian 196 assert not (lap2 == laplacian) 197 assert lap2 != object() 198 assert not (lap2 == object()) 199 200 def chain_with_pdop(self): 201 x, y = symbols('x y', real=True) 202 s = Sdop([(x, Pdop(x)), (y, Pdop(y))]) 203 # right-multiplication by Pdop chains only the pdiffs 204 sp = s * Pdop(x) 205 assert sp == Sdop([(x, Pdop(x) * Pdop(x)), (y, Pdop(y) * Pdop(x))]) 206 # left-multiplcation by Pdop invokes the product rule 207 ps = Pdop(x) * s 208 assert ps == Sdop([(x, Pdop(x) * Pdop(x)), (1, Pdop(x)), (y, Pdop(y) * Pdop(x))]) 209 210 # implicit multiplication 211 assert ps == Pdop(x)(s) 212 assert sp == s(Pdop(x)) 213 214 # no-op pdop 215 assert s == Pdop({})(s) 216 assert s == s(Pdop({})) 217 218 def chain_with_mv(self): 219 coords = x, y, z = symbols('x y z', real=True) 220 ga, ex, ey, ez = Ga.build('e*x|y|z', g=[1, 1, 1], coords=coords) 221 s = Sdop([(x, Pdop(x)), (y, Pdop(y))]) 222 223 assert type(ex * s) is Sdop 224 assert type(s * ex) is Mv 225 226 # type should be preserved even when the result is 0 227 assert type(ex * Sdop([])) is Sdop 228 assert type(Sdop([]) * ex) is Mv 229 230 # As discussed with brombo, these operations are not well defined - if 231 # you need them, you should be using `Dop` not `Sdop`. 232 for op in [operator.xor, operator.or_, operator.lt, operator.gt]: 233 with pytest.raises(TypeError): 234 op(ex, s) 235 with pytest.raises(TypeError): 236 op(s, ex) 237 238 def test_constructor_errors(self): 239 coords = x, y, z = symbols('x y z', real=True) 240 ga, ex, ey, ez = Ga.build('e*x|y|z', g=[1, 1, 1], coords=coords) 241 242 # list lengths must match 243 with pytest.raises(ValueError, match='same length'): 244 Sdop([ex], []) 245 246 # not a symbol or list 247 with pytest.raises(TypeError, match='symbol or sequence is required'): 248 Sdop(1) 249 250 # not a pair of lists 251 with pytest.raises(TypeError, match='must be lists'): 252 Sdop([], 1) 253 254 # too few arguments 255 with pytest.raises(TypeError, match='0 were given'): 256 Sdop() 257 # too many arguments 258 with pytest.raises(TypeError, match='3 were given'): 259 Sdop(1, 2, 3) 260 261 262 class TestPdop(object): 263 264 def test_deprecation(self): 265 coords = x, y, z = symbols('x y z', real=True) 266 ga, ex, ey, ez = Ga.build('e*x|y|z', g=[1, 1, 1], coords=coords) 267 268 # passing `None` is a deprecate way to spell `{}` 269 with pytest.warns(DeprecationWarning): 270 p = Pdop(None) 271 assert p == Pdop({}) 272 273 with pytest.warns(DeprecationWarning): 274 ga.Pdop_identity 275 with pytest.warns(DeprecationWarning): 276 ga.Pdiffs 277 278 def test_misc(self): 279 """ Other miscellaneous tests """ 280 coords = x, y, z = symbols('x y z', real=True) 281 ga, ex, ey, ez = Ga.build('e*x|y|z', g=[1, 1, 1], coords=coords) 282 283 pxa = ga.pdop({x: 1}) 284 pxb = ga.pdop({x: 1}) 285 p1 = ga.pdop({}) 286 287 # test comparison 288 assert pxa == pxb 289 assert not (pxa != pxb) 290 assert p1 != pxa 291 assert not (p1 == pxa) 292 assert pxa != object() 293 assert not (pxa == object()) 294 295 def test_multiply(self): 296 coords = x, y, z = symbols('x y z', real=True) 297 ga, ex, ey, ez = Ga.build('e*x|y|z', g=[1, 1, 1], coords=coords) 298 299 p = Pdop(x) 300 assert x * p == Sdop([(x, p)]) 301 assert ex * p == Sdop([(ex, p)]) 302 303 assert p * x == p(x) == S(1) 304 assert p * ex == p(ex) == S(0) 305 assert type(p(ex)) is Mv 306 307 # These are not defined for consistency with Sdop 308 for op in [operator.xor, operator.or_, operator.lt, operator.gt]: 309 with pytest.raises(TypeError): 310 op(ex, p) 311 with pytest.raises(TypeError): 312 op(p, ex) 313 314 def test_constructor_errors(self): 315 # not a symbol or dict 316 with pytest.raises(TypeError, match='dictionary or symbol is required'): 317 Pdop(1)