Commutation 1.5.0__tar.gz → 1.5.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: Commutation
3
- Version: 1.5.0
3
+ Version: 1.5.1
4
4
  Summary: A small package for doing commutator algebra
5
5
  Author-email: Alaric Sanders <thathatatt@gmail.com>
6
6
  Project-URL: Homepage, https://github.com/Spuriosity1/Commutation
@@ -12,6 +12,8 @@ Requires-Python: >=3.8
12
12
  Description-Content-Type: text/markdown
13
13
  License-File: LICENSE
14
14
  Requires-Dist: ipython
15
+ Provides-Extra: test
16
+ Requires-Dist: pytest; extra == "test"
15
17
  Dynamic: license-file
16
18
 
17
19
  # Commutation Station for Operator Elimination
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "Commutation"
7
- version = "1.5.0"
7
+ version = "1.5.1"
8
8
  requires-python = ">= 3.8"
9
9
  authors = [
10
10
  {name = "Alaric Sanders", email="thathatatt@gmail.com"}
@@ -23,6 +23,9 @@ dependencies = [
23
23
  "ipython",
24
24
  ]
25
25
 
26
+ [project.optional-dependencies]
27
+ test = ["pytest"]
28
+
26
29
  [project.urls]
27
30
  Homepage = "https://github.com/Spuriosity1/Commutation"
28
31
  Issues = "https://github.com/Spuriosity1/Commutation/issues"
@@ -33,3 +36,9 @@ package-dir = {"" = "src"}
33
36
 
34
37
  [tool.setuptools.packages.find]
35
38
  where = ["src"]
39
+
40
+ [tool.pytest.ini_options]
41
+ # allows `pytest` from the repo root without installing the package first
42
+ # (tox installs it, but this keeps the local dev loop simple)
43
+ pythonpath = ["src"]
44
+ testpaths = ["tests"]
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: Commutation
3
- Version: 1.5.0
3
+ Version: 1.5.1
4
4
  Summary: A small package for doing commutator algebra
5
5
  Author-email: Alaric Sanders <thathatatt@gmail.com>
6
6
  Project-URL: Homepage, https://github.com/Spuriosity1/Commutation
@@ -12,6 +12,8 @@ Requires-Python: >=3.8
12
12
  Description-Content-Type: text/markdown
13
13
  License-File: LICENSE
14
14
  Requires-Dist: ipython
15
+ Provides-Extra: test
16
+ Requires-Dist: pytest; extra == "test"
15
17
  Dynamic: license-file
16
18
 
17
19
  # Commutation Station for Operator Elimination
@@ -9,4 +9,9 @@ src/Commutation.egg-info/top_level.txt
9
9
  src/commutation/__init__.py
10
10
  src/commutation/commutatoralgebra.py
11
11
  src/commutation/expression.py
12
- src/commutation/style.py
12
+ src/commutation/style.py
13
+ tests/test_anticommutator.py
14
+ tests/test_commutator.py
15
+ tests/test_expression.py
16
+ tests/test_operator.py
17
+ tests/test_term.py
@@ -0,0 +1,4 @@
1
+ ipython
2
+
3
+ [test]
4
+ pytest
@@ -156,8 +156,9 @@ class CommutatorAlgebra(AbstractCommutatorAlgebra):
156
156
  B = term.ops[i-1]
157
157
  front = Term(*term.ops[:i-1])
158
158
  back = Term(*term.ops[i+1:])
159
-
160
- c = self.get_commutator(A, B)
159
+
160
+ # reordering B*A -> A*B picks up [B, A] = -[A, B]
161
+ c = self.get_commutator(B, A)
161
162
  extra = front * c * back * term.multiplier
162
163
 
163
164
  term.ops[i-1], term.ops[i] = term.ops[i], term.ops[i-1]
@@ -215,8 +216,11 @@ class AntiCommutatorAlgebra(AbstractCommutatorAlgebra):
215
216
  front = Term(*term.ops[:i])
216
217
  back = Term(*term.ops[i+2:])
217
218
 
218
- c = self.get_anticommutator(
219
- term.ops[i], term.ops[i+1])
219
+ A = term.ops[i]
220
+ B = term.ops[i+1]
221
+ c = self.get_anticommutator(A, B)
222
+ if c is None:
223
+ raise AntiCommutatorUnknownException(A, B)
220
224
  extra = Expression(front) * \
221
225
  c * Expression(back)*term.multiplier
222
226
  term.multiplier *= -1
@@ -0,0 +1,54 @@
1
+ """Tests for AntiCommutatorAlgebra.
2
+
3
+ Anticommutator support is newer than the commutator path (see TODO.md), so
4
+ these focus on the basics plus two documented rough edges.
5
+ """
6
+ import pytest
7
+
8
+ from commutation import (
9
+ Operator,
10
+ Expression,
11
+ AntiCommutatorAlgebra,
12
+ AntiCommutatorUnknownException,
13
+ )
14
+
15
+
16
+ def _algebra():
17
+ """Two fermion-like modes with {f1, f2} = 0."""
18
+ f1, f2 = Operator("f1"), Operator("f2")
19
+ ac = AntiCommutatorAlgebra()
20
+ ac.set_anticommutator(f1, f2)(0)
21
+ return ac, f1, f2
22
+
23
+
24
+ def test_set_anticommutator_is_symmetric():
25
+ ac, f1, f2 = _algebra()
26
+ assert ac.relations["f1"]["f2"] == ac.relations["f2"]["f1"]
27
+
28
+
29
+ def test_diagonal_defaults_to_twice_the_operator():
30
+ ac, f1, f2 = _algebra()
31
+ assert ac.relations["f1"]["f1"] == 2 * f1
32
+
33
+
34
+ def test_move_right_flips_sign_on_swap():
35
+ # f1*f2 -> -f2*f1 + {f1, f2} = -f2*f1 (since {f1, f2} = 0)
36
+ ac, f1, f2 = _algebra()
37
+ e = Expression(f1 * f2)
38
+ ac.move_right(e, f1)
39
+ e.collect()
40
+ assert e == -1 * (f2 * f1)
41
+
42
+
43
+ def test_move_left_unknown_raises_friendly_exception():
44
+ ac, f1, f2 = _algebra()
45
+ g = Operator("g") # unregistered
46
+ with pytest.raises(AntiCommutatorUnknownException):
47
+ ac.move_left(Expression(g * f1), f1)
48
+
49
+
50
+ def test_move_right_unknown_raises_friendly_exception():
51
+ ac, f1, f2 = _algebra()
52
+ g = Operator("g") # unregistered
53
+ with pytest.raises(AntiCommutatorUnknownException):
54
+ ac.move_right(Expression(f1 * g), f1)
@@ -0,0 +1,132 @@
1
+ """Tests for CommutatorAlgebra: relation storage and operator reordering.
2
+
3
+ These cover the TODO's "some well known commutator reductions" item using the
4
+ SU(2) spin algebra [az, ap] = ap, [az, am] = -am, [ap, am] = 2 az.
5
+ """
6
+ import warnings
7
+
8
+ import pytest
9
+
10
+ from commutation import (
11
+ Operator,
12
+ Scalar,
13
+ Expression,
14
+ CommutatorAlgebra,
15
+ CommutatorUnknownException,
16
+ )
17
+
18
+
19
+ # --------------------------------------------------------------------------
20
+ # relation storage
21
+ # --------------------------------------------------------------------------
22
+ def test_set_commutator_stores_antisymmetric_partner(spin):
23
+ ca, az, ap, am = spin
24
+ assert ca.relations["ap"]["am"] == 2 * az
25
+ assert ca.relations["am"]["ap"] == -2 * az
26
+
27
+
28
+ def test_get_commutator_scalar_is_zero(spin):
29
+ ca, az, ap, am = spin
30
+ K = Scalar("K")
31
+ assert ca.get_commutator(K, az) == 0
32
+ assert ca.get_commutator(az, K) == 0
33
+
34
+
35
+ def test_get_commutator_unknown_warns_and_assumes_commute(spin):
36
+ ca, az, ap, am = spin
37
+ unknown = Operator("unknown")
38
+ with warnings.catch_warnings(record=True) as w:
39
+ warnings.simplefilter("always")
40
+ result = ca.get_commutator(unknown, az)
41
+ assert result == 0
42
+ assert any(issubclass(x.category, UserWarning) for x in w)
43
+
44
+
45
+ def test_strict_mode_raises_for_unknown():
46
+ az, ap = Operator("az"), Operator("ap")
47
+ ca = CommutatorAlgebra(strict=True)
48
+ ca.set_commutator(az, ap)(ap)
49
+ am = Operator("am") # never registered
50
+ with pytest.raises(CommutatorUnknownException):
51
+ ca.move_right(Expression(az * ap * am), az)
52
+
53
+
54
+ # --------------------------------------------------------------------------
55
+ # elementary reductions
56
+ # --------------------------------------------------------------------------
57
+ def test_move_right_single_swap(spin):
58
+ # ap*az -> az*ap + [ap, az] = az*ap - ap
59
+ ca, az, ap, am = spin
60
+ e = Expression(ap * az)
61
+ ca.move_right(e, ap)
62
+ e.collect()
63
+ assert e == az * ap - ap
64
+
65
+
66
+ def test_move_right_noop_when_already_rightmost(spin):
67
+ # az is already at the right end; nothing to move past.
68
+ ca, az, ap, am = spin
69
+ e = Expression(ap * az)
70
+ ca.move_right(e, az)
71
+ e.collect()
72
+ assert e == ap * az
73
+
74
+
75
+ def test_move_right_readme_example(spin):
76
+ ca, az, ap, am = spin
77
+ e = Expression(az * ap * az * az * am)
78
+ ca.move_right(e, az)
79
+ e.collect()
80
+ assert e == ap * am * az * az * az - 2 * ap * am * az * az + ap * am * az
81
+
82
+
83
+ def test_scalars_commute_through(spin):
84
+ ca, az, ap, am = spin
85
+ KA = Scalar("KA")
86
+ e = Expression(az * ap * am * KA * am)
87
+ ca.move_right(e, az)
88
+ e.collect()
89
+ assert e == ap * am * KA * am * az - ap * am * KA * am
90
+
91
+
92
+ def test_unknown_operator_during_move_warns(spin):
93
+ ca, az, ap, am = spin
94
+ c = Operator("c") # not in the database
95
+ e = Expression(az * ap * am * c * am)
96
+ with warnings.catch_warnings(record=True) as w:
97
+ warnings.simplefilter("always")
98
+ ca.move_right(e, az)
99
+ e.collect()
100
+ assert any(issubclass(x.category, UserWarning) for x in w)
101
+ assert e == ap * am * c * am * az - ap * am * c * am
102
+
103
+
104
+ # --------------------------------------------------------------------------
105
+ # move_left
106
+ # --------------------------------------------------------------------------
107
+ def test_move_left_single_swap(spin):
108
+ # ap*az -> az*ap + [ap, az] = az*ap - ap
109
+ ca, az, ap, am = spin
110
+ e = Expression(ap * az)
111
+ ca.move_left(e, az)
112
+ e.collect()
113
+ assert e == az * ap - ap
114
+
115
+
116
+ def test_move_left_readme_example(spin):
117
+ ca, az, ap, am = spin
118
+ e = Expression(az * ap * az * az * am)
119
+ ca.move_left(e, am)
120
+ e.collect()
121
+ expected = (
122
+ am * az * ap * az * az
123
+ - 2 * am * az * ap * az
124
+ + 2 * az * az * az * az
125
+ - am * ap * az * az
126
+ + am * az * ap
127
+ - 4 * az * az * az
128
+ + 2 * am * ap * az
129
+ + 2 * az * az
130
+ - am * ap
131
+ )
132
+ assert e == expected
@@ -0,0 +1,149 @@
1
+ """Tests for the Expression abelian-group / CAS operations."""
2
+ from fractions import Fraction
3
+
4
+ import pytest
5
+
6
+ from commutation import Operator, Term, Expression
7
+
8
+
9
+ def test_construction_drops_zeros():
10
+ a = Operator("a")
11
+ e = Expression(a, 0, Term(a))
12
+ assert len(e.terms) == 2
13
+
14
+
15
+ def test_empty_expression():
16
+ assert Expression().terms == []
17
+
18
+
19
+ def test_add_sub_neg():
20
+ a, b = Operator("a"), Operator("b")
21
+ assert (a + b) - b == a
22
+ assert -(a + b) == Expression(-Term(a), -Term(b))
23
+
24
+
25
+ def test_mul_distributes():
26
+ a, b, c = Operator("a"), Operator("b"), Operator("c")
27
+ e = (a + b) * c
28
+ assert e == a * c + b * c
29
+
30
+
31
+ def test_scalar_mul():
32
+ a, b = Operator("a"), Operator("b")
33
+ assert 2 * (a + b) == (2 * a) + (2 * b)
34
+
35
+
36
+ def test_collect_folds_like_terms():
37
+ a = Operator("a")
38
+ e = Expression(a) + Expression(a) - Expression(a)
39
+ e.collect()
40
+ assert len(e.terms) == 1
41
+ assert e.terms[0].multiplier == 1
42
+
43
+
44
+ def test_collect_drops_zero_terms():
45
+ a = Operator("a")
46
+ e = a - a
47
+ e.collect()
48
+ assert e.terms == []
49
+
50
+
51
+ def test_sort_strategies():
52
+ a, b, c = Operator("a"), Operator("b"), Operator("c")
53
+ e = Expression(c) + a + b
54
+ e.sort("first")
55
+ assert [str(t.ops[0]) for t in e.terms] == ["a", "b", "c"]
56
+
57
+
58
+ def test_operators_property():
59
+ a, b, c = Operator("a"), Operator("b"), Operator("c")
60
+ assert (a * b + c).operators == {"a", "b", "c"}
61
+
62
+
63
+ def test_order_property():
64
+ a, b, c = Operator("a"), Operator("b"), Operator("c")
65
+ assert (a * b * c + a).order == 3
66
+
67
+
68
+ def test_is_scalar_property():
69
+ from commutation import Scalar
70
+ a = Operator("a")
71
+ K = Scalar("K")
72
+ assert (K + K).is_scalar is True
73
+ assert (a + K).is_scalar is False
74
+
75
+
76
+ def test_eq_is_algebraic():
77
+ a, b = Operator("a"), Operator("b")
78
+ assert (a + b) == (b + a) # collect ignores term order for matching keys
79
+ assert (a + a) == (2 * a)
80
+ assert (a + b) != (a + a)
81
+
82
+
83
+ def test_substitute_readme_example():
84
+ a, b, c, d = (Operator(x) for x in "abcd")
85
+ x = a * b * b * b * a * c * a * a * c * a * b + 1
86
+ y = x.substitute(a * b, c + d)
87
+ expected = (
88
+ c * b * b * a * c * a * a * c * c
89
+ + c * b * b * a * c * a * a * c * d
90
+ + d * b * b * a * c * a * a * c * c
91
+ + d * b * b * a * c * a * a * c * d
92
+ + 1
93
+ )
94
+ assert y == expected
95
+
96
+
97
+ def test_substitute_preserves_multiplier():
98
+ a, b, c = Operator("a"), Operator("b"), Operator("c")
99
+ assert Expression(3 * a * b).substitute(a * b, c) == 3 * c
100
+
101
+
102
+ def test_sub_is_alias_for_substitute():
103
+ a, b, c = Operator("a"), Operator("b"), Operator("c")
104
+ e = Expression(a * b)
105
+ assert e.sub(a * b, c) == e.substitute(a * b, c)
106
+
107
+
108
+ def test_replaceall():
109
+ a, b, c = Operator("a"), Operator("b"), Operator("c")
110
+ assert Expression(a * b * a * b).replaceall((a * b, c)) == c * c
111
+
112
+
113
+ def test_factor_no_x_right():
114
+ a, b, c, d = (Operator(x) for x in "abcd")
115
+ xx = 7 * a * b * b + Fraction(4, 5) * a * c * b * b + a * d * b * b
116
+ front, back = xx.factor("right")
117
+ assert front * back == xx
118
+ assert front == Term(a)
119
+
120
+
121
+ def test_factor_no_x_left():
122
+ a, b, c, d = (Operator(x) for x in "abcd")
123
+ xx = 7 * a * b * b + Fraction(4, 5) * a * c * b * b + a * d * b * b
124
+ front, back = xx.factor() # default side = left
125
+ assert front * back == xx
126
+ assert back == Term(b, b)
127
+
128
+
129
+ def test_factor_explicit_x():
130
+ a, b, c = Operator("a"), Operator("b"), Operator("c")
131
+ front, back = (a * b + a * c).factor("right", a)
132
+ assert front == Term(a)
133
+ assert back == b + c
134
+ assert front * back == (a * b + a * c)
135
+
136
+
137
+ def test_coefficient():
138
+ a, b, c = Operator("a"), Operator("b"), Operator("c")
139
+ assert (a * b + a * c).coefficient(a, "right") == b + c
140
+
141
+
142
+ def test_as_latex_fraction():
143
+ a = Operator("a")
144
+ assert (Fraction(4, 5) * Term(a)).as_latex() == r"+\frac{4}{5} a"
145
+
146
+
147
+ def test_from_str_not_implemented():
148
+ with pytest.raises(NotImplementedError):
149
+ Expression().from_str("a + b")
@@ -0,0 +1,85 @@
1
+ """Tests for the Operator / Scalar primitives."""
2
+ import pytest
3
+
4
+ from commutation import Operator, Scalar, Term, Expression
5
+
6
+
7
+ def test_name_must_be_str():
8
+ with pytest.raises(TypeError):
9
+ Operator(123)
10
+
11
+
12
+ def test_latex_must_be_str():
13
+ with pytest.raises(TypeError):
14
+ Operator("a", 123)
15
+
16
+
17
+ def test_latex_defaults_to_name():
18
+ a = Operator("a")
19
+ assert a.as_latex() == "a"
20
+
21
+
22
+ def test_latex_string_used_when_given():
23
+ a = Operator("a", "S^z_a")
24
+ assert a.as_latex() == "S^z_a"
25
+
26
+
27
+ def test_str_and_repr_are_name():
28
+ a = Operator("a")
29
+ assert str(a) == "a"
30
+ assert repr(a) == "a"
31
+
32
+
33
+ def test_mul_by_int_gives_term():
34
+ a = Operator("a")
35
+ t = a * 3
36
+ assert isinstance(t, Term)
37
+ assert t.multiplier == 3
38
+ assert t.ops == [a]
39
+
40
+
41
+ def test_rmul_by_int_gives_term():
42
+ a = Operator("a")
43
+ t = 3 * a
44
+ assert isinstance(t, Term)
45
+ assert t.multiplier == 3
46
+
47
+
48
+ def test_mul_by_operator_gives_term():
49
+ a, b = Operator("a"), Operator("b")
50
+ t = a * b
51
+ assert isinstance(t, Term)
52
+ assert t.ops == [a, b]
53
+
54
+
55
+ def test_add_gives_expression():
56
+ a, b = Operator("a"), Operator("b")
57
+ e = a + b
58
+ assert isinstance(e, Expression)
59
+ assert len(e.terms) == 2
60
+
61
+
62
+ def test_neg_gives_term_with_negative_multiplier():
63
+ a = Operator("a")
64
+ t = -a
65
+ assert isinstance(t, Term)
66
+ assert t.multiplier == -1
67
+
68
+
69
+ def test_eq_is_algebraic():
70
+ a, b = Operator("a"), Operator("b")
71
+ assert a == a
72
+ assert a != b
73
+ assert a == Term(a)
74
+ assert (a - a) == 0
75
+
76
+
77
+ def test_scalar_sets_flag():
78
+ assert Scalar("K").is_scalar is True
79
+ assert Operator("a").is_scalar is False
80
+
81
+
82
+ def test_subtraction_of_operators():
83
+ a, b = Operator("a"), Operator("b")
84
+ e = a - b
85
+ assert e == Expression(Term(a), -Term(b))
@@ -0,0 +1,114 @@
1
+ """Tests for the Term monoid."""
2
+ from fractions import Fraction
3
+
4
+ import pytest
5
+
6
+ from commutation import Operator, Scalar, Term
7
+
8
+
9
+ def test_construction_products_operators_and_scalars():
10
+ a, b = Operator("a"), Operator("b")
11
+ t = Term(2, a, 3, b)
12
+ assert t.multiplier == 6
13
+ assert t.ops == [a, b]
14
+
15
+
16
+ def test_identity_term_is_empty():
17
+ t = Term()
18
+ assert t.multiplier == 1
19
+ assert t.ops == []
20
+ assert len(t) == 0
21
+
22
+
23
+ def test_nested_term_flattens():
24
+ a, b = Operator("a"), Operator("b")
25
+ t = Term(Term(2, a), Term(3, b))
26
+ assert t.multiplier == 6
27
+ assert t.ops == [a, b]
28
+
29
+
30
+ def test_bad_type_raises():
31
+ with pytest.raises(TypeError):
32
+ Term(1.5) # float is not allowed, only int / Fraction / Operator
33
+
34
+
35
+ def test_is_scalar():
36
+ a = Operator("a")
37
+ assert Term(Scalar("K"), Scalar("J")).is_scalar is True
38
+ assert Term(a).is_scalar is False
39
+
40
+
41
+ def test_order_sign_len():
42
+ a, b = Operator("a"), Operator("b")
43
+ t = -2 * Term(a, b)
44
+ assert t.order == 2
45
+ assert len(t) == 2
46
+ assert t.sign == -1
47
+ assert (2 * Term(a)).sign == 1
48
+
49
+
50
+ def test_mul_rmul_truediv():
51
+ a, b = Operator("a"), Operator("b")
52
+ assert (Term(a) * b).ops == [a, b]
53
+ assert (b * Term(a)).ops == [b, a]
54
+ assert (Term(a) * 4).multiplier == 4
55
+ assert (Term(4, a) / 2).multiplier == Fraction(2, 1)
56
+
57
+
58
+ def test_neg_copies():
59
+ a = Operator("a")
60
+ t = Term(a)
61
+ n = -t
62
+ assert n.multiplier == -1
63
+ assert t.multiplier == 1 # original untouched
64
+
65
+
66
+ def test_factor_scalars():
67
+ a = Operator("a")
68
+ K = Scalar("K")
69
+ scal, ops = Term(3, K, a).factor_scalars()
70
+ assert scal == Term(3, K)
71
+ assert ops == Term(a)
72
+
73
+
74
+ def test_move_scalars_left_and_right():
75
+ a, b = Operator("a"), Operator("b")
76
+ K = Scalar("K")
77
+
78
+ left = Term(a, K, b)
79
+ left.move_scalars("left")
80
+ assert [str(o) for o in left.ops] == ["K", "a", "b"]
81
+
82
+ right = Term(a, K, b)
83
+ right.move_scalars("right")
84
+ assert [str(o) for o in right.ops] == ["a", "b", "K"]
85
+
86
+
87
+ def test_move_scalars_bad_side():
88
+ a = Operator("a")
89
+ with pytest.raises(IndexError):
90
+ Term(a).move_scalars("sideways")
91
+
92
+
93
+ def test_findall_non_overlapping():
94
+ a, b = Operator("a"), Operator("b")
95
+ assert Term(a, a, a).findall(Term(a, a)) == [0]
96
+ assert Term(a, a, a, a).findall(Term(a, a)) == [0, 2]
97
+ assert Term(a, b, a, b).findall(Term(a, b)) == [0, 2]
98
+ assert Term(a, b).findall(Term(b, a)) == []
99
+
100
+
101
+ def test_eq():
102
+ a, b = Operator("a"), Operator("b")
103
+ assert Term(2, a, b) == Term(2, a, b)
104
+ assert Term(2, a, b) != Term(3, a, b)
105
+ assert Term(a, b) != Term(b, a)
106
+ assert Term(a) != "a"
107
+
108
+
109
+ def test_copy_is_independent():
110
+ a = Operator("a")
111
+ t = Term(a)
112
+ c = t.copy()
113
+ c.multiplier = 5
114
+ assert t.multiplier == 1
@@ -1 +0,0 @@
1
- ipython
File without changes
File without changes
File without changes