fockit 0.0.2__py3-none-any.whl

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.
fockit/converters.py ADDED
@@ -0,0 +1,52 @@
1
+ """Converters from OpSum to external operator formats."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import TYPE_CHECKING
6
+
7
+ from fockit._types import as_complex
8
+ from fockit.algebra import FermionRules, Ladder
9
+ from fockit.operators import OpSum
10
+
11
+ if TYPE_CHECKING:
12
+ from triqs.operators import Operator as TriqsOperator
13
+
14
+
15
+ def to_triqs(op: OpSum) -> TriqsOperator:
16
+ """Convert a fermionic OpSum to a TRIQS Operator.
17
+
18
+ Parameters
19
+ ----------
20
+ op : OpSum
21
+ Operator expression with ``FermionRules``.
22
+
23
+ Returns
24
+ -------
25
+ triqs.operators.Operator
26
+
27
+ """
28
+ for mono in op.terms:
29
+ for single_op in mono.ops:
30
+ if not isinstance(single_op.rules, FermionRules):
31
+ name = type(single_op.rules).__name__
32
+ msg = f"to_triqs requires FermionRules, got {name}"
33
+ raise TypeError(msg)
34
+
35
+ from triqs.operators import Operator, c, c_dag
36
+
37
+ result = Operator()
38
+ for mono, coeff in op.terms.items():
39
+ c_val = as_complex(coeff)
40
+ term = c_val.real if c_val.imag == 0 else c_val
41
+ for ladder_op in mono.ops:
42
+ indices = (
43
+ ladder_op.mode
44
+ if isinstance(ladder_op.mode, tuple)
45
+ else (ladder_op.mode,)
46
+ )
47
+ if ladder_op.kind is Ladder.CREATE:
48
+ term = term * c_dag(*indices)
49
+ else:
50
+ term = term * c(*indices)
51
+ result = result + term
52
+ return result
fockit/display.py ADDED
@@ -0,0 +1,150 @@
1
+ """Display and formatting for operator expressions."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Callable, Hashable
6
+
7
+ from fockit._types import Coefficient
8
+ from fockit.algebra import (
9
+ BosonRules,
10
+ FermionRules,
11
+ Ladder,
12
+ Pauli,
13
+ PauliRules,
14
+ mode_sort_key,
15
+ )
16
+ from fockit.operators import Monomial, Op, OpSum, format_mode
17
+
18
+ # operators of other algebras show their kind as the symbol
19
+ _TEXT_SYMBOLS: dict[tuple[type, Hashable], str] = {
20
+ (FermionRules, Ladder.CREATE): "c†",
21
+ (FermionRules, Ladder.ANNIHILATE): "c",
22
+ (BosonRules, Ladder.CREATE): "b†",
23
+ (BosonRules, Ladder.ANNIHILATE): "b",
24
+ (PauliRules, Pauli.X): "σ_x",
25
+ (PauliRules, Pauli.Y): "σ_y",
26
+ (PauliRules, Pauli.Z): "σ_z",
27
+ }
28
+ _LATEX_SYMBOLS: dict[tuple[type, Hashable], str] = {
29
+ (FermionRules, Ladder.CREATE): r"c^{\dagger}",
30
+ (FermionRules, Ladder.ANNIHILATE): "c",
31
+ (BosonRules, Ladder.CREATE): r"b^{\dagger}",
32
+ (BosonRules, Ladder.ANNIHILATE): "b",
33
+ (PauliRules, Pauli.X): r"\sigma^{x}",
34
+ (PauliRules, Pauli.Y): r"\sigma^{y}",
35
+ (PauliRules, Pauli.Z): r"\sigma^{z}",
36
+ }
37
+ _LATEX_LABELS = {"up": r"\uparrow", "dn": r"\downarrow"}
38
+
39
+
40
+ def to_text(op: OpSum) -> str:
41
+ """Format an OpSum as a human-readable string.
42
+
43
+ Parameters
44
+ ----------
45
+ op : OpSum
46
+
47
+ Returns
48
+ -------
49
+ str
50
+
51
+ """
52
+ return _join_terms(op, _op_text)
53
+
54
+
55
+ def to_latex(op: OpSum) -> str:
56
+ r"""Convert an operator expression to LaTeX.
57
+
58
+ Parameters
59
+ ----------
60
+ op : OpSum
61
+
62
+ Returns
63
+ -------
64
+ str
65
+
66
+ """
67
+ return _join_terms(op, _op_latex)
68
+
69
+
70
+ def _join_terms(op: OpSum, format_op: Callable[[Op], str]) -> str:
71
+ """Write the terms in display order, joined with + and -."""
72
+ if not op.terms:
73
+ return "0"
74
+
75
+ sorted_terms = sorted(
76
+ op.terms.items(), key=lambda item: _monomial_sort_key(item[0])
77
+ )
78
+
79
+ parts: list[str] = []
80
+ for mono, raw_coeff in sorted_terms:
81
+ coeff = _real_if_possible(raw_coeff)
82
+ if not mono.ops:
83
+ parts.append(str(coeff))
84
+ continue
85
+
86
+ ops_str = " ".join(format_op(single_op) for single_op in mono.ops)
87
+
88
+ coeff_str = _format_coeff(coeff)
89
+ if coeff_str == "":
90
+ parts.append(ops_str)
91
+ elif coeff_str == "-":
92
+ parts.append(f"-{ops_str}")
93
+ else:
94
+ parts.append(f"{coeff_str} {ops_str}")
95
+
96
+ result = parts[0]
97
+ for part in parts[1:]:
98
+ if part.startswith("-"):
99
+ result += f" - {part[1:]}"
100
+ else:
101
+ result += f" + {part}"
102
+ return result
103
+
104
+
105
+ def _op_text(op: Op) -> str:
106
+ """Format as e.g. ``c†(0, up)``."""
107
+ symbol = _TEXT_SYMBOLS.get((type(op.rules), op.kind), op.kind)
108
+ return f"{symbol}({format_mode(op.mode)})"
109
+
110
+
111
+ def _op_latex(op: Op) -> str:
112
+ r"""Format as e.g. ``c^{\dagger}_{0, \uparrow}``."""
113
+ symbol = _LATEX_SYMBOLS.get((type(op.rules), op.kind), op.kind)
114
+ labels = op.mode if isinstance(op.mode, tuple) else (op.mode,)
115
+ subscript = ", ".join(_LATEX_LABELS.get(label, str(label)) for label in labels)
116
+ return f"{symbol}_{{{subscript}}}"
117
+
118
+
119
+ def _monomial_sort_key(mono: Monomial) -> tuple[object, ...]:
120
+ """Sort key for deterministic term ordering in display."""
121
+ keys = []
122
+ for op in mono.ops:
123
+ keys.append((_kind_sort_key(op.kind), mode_sort_key(op.mode)))
124
+ return (len(mono.ops), tuple(keys))
125
+
126
+
127
+ def _kind_sort_key(kind: Hashable) -> tuple[object, ...]:
128
+ """Sort key for operator kind within display ordering."""
129
+ if isinstance(kind, str):
130
+ return (2, kind)
131
+ return (3, repr(kind))
132
+
133
+
134
+ def _real_if_possible(coeff: Coefficient) -> Coefficient:
135
+ """Show a complex number without its zero part, e.g. ``0.5`` or ``-0.5j``."""
136
+ if isinstance(coeff, complex):
137
+ if coeff.imag == 0:
138
+ return coeff.real
139
+ if coeff.real == 0:
140
+ return complex(0.0, coeff.imag)
141
+ return coeff
142
+
143
+
144
+ def _format_coeff(coeff: Coefficient) -> str:
145
+ """Format a coefficient for display."""
146
+ if coeff == 1:
147
+ return ""
148
+ if coeff == -1:
149
+ return "-"
150
+ return str(coeff)
fockit/inspection.py ADDED
@@ -0,0 +1,60 @@
1
+ """Inspection utilities for operator expressions."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Hashable
6
+
7
+ from fockit.operators import OpSum
8
+
9
+
10
+ def modes(op: OpSum) -> set[Hashable]:
11
+ """Return the set of all mode labels appearing in the operator.
12
+
13
+ Parameters
14
+ ----------
15
+ op : OpSum
16
+
17
+ Returns
18
+ -------
19
+ set[Hashable]
20
+
21
+ """
22
+ result: set[Hashable] = set()
23
+ for mono in op.terms:
24
+ for ladder_op in mono.ops:
25
+ result.add(ladder_op.mode)
26
+ return result
27
+
28
+
29
+ def degree(op: OpSum) -> int:
30
+ """Maximum monomial length (number of ladder operators).
31
+
32
+ Returns 0 for the zero operator.
33
+
34
+ Parameters
35
+ ----------
36
+ op : OpSum
37
+
38
+ Returns
39
+ -------
40
+ int
41
+
42
+ """
43
+ if not op.terms:
44
+ return 0
45
+ return max(len(mono) for mono in op.terms)
46
+
47
+
48
+ def num_terms(op: OpSum) -> int:
49
+ """Return the number of nonzero terms.
50
+
51
+ Parameters
52
+ ----------
53
+ op : OpSum
54
+
55
+ Returns
56
+ -------
57
+ int
58
+
59
+ """
60
+ return len(op.terms)
fockit/normal_order.py ADDED
@@ -0,0 +1,142 @@
1
+ """Normal ordering algorithm.
2
+
3
+ Every algebra goes through the same path, using only its ``swap`` and
4
+ ``is_normal_ordered_pair`` rules. Pauli strings could be multiplied faster site
5
+ by site, or stored as bitmasks, but that would treat one algebra differently
6
+ from the rest.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import functools
12
+
13
+ from fockit._types import Coefficient, as_coefficient
14
+ from fockit.algebra import SwapResult
15
+ from fockit.operators import Monomial, Op, OpSum
16
+
17
+
18
+ def normal_order(op: OpSum) -> OpSum:
19
+ """Normal-order an operator expression.
20
+
21
+ Operators of different algebras commute and are grouped by algebra.
22
+
23
+ Parameters
24
+ ----------
25
+ op : OpSum
26
+ The operator to normal-order.
27
+
28
+ Returns
29
+ -------
30
+ OpSum
31
+ The normal-ordered operator.
32
+
33
+ """
34
+ terms: dict[Monomial, Coefficient] = {}
35
+ for mono, coeff in op.terms.items():
36
+ for ordered, c in _order(mono.ops, Monomial(())).items():
37
+ _accumulate(terms, ordered, as_coefficient(coeff * c))
38
+ return OpSum(terms)
39
+
40
+
41
+ def is_normal_ordered(op: OpSum) -> bool:
42
+ """Check if an operator expression is in normal order.
43
+
44
+ Parameters
45
+ ----------
46
+ op : OpSum
47
+
48
+ Returns
49
+ -------
50
+ bool
51
+
52
+ """
53
+ for mono in op.terms:
54
+ ops = mono.ops
55
+ for i in range(len(ops) - 1):
56
+ if not _is_ordered(ops[i], ops[i + 1]):
57
+ return False
58
+ return True
59
+
60
+
61
+ def _is_ordered(left: Op, right: Op) -> bool:
62
+ if type(left.rules) is type(right.rules):
63
+ return left.rules.is_normal_ordered_pair(left, right)
64
+ return type(left.rules).__name__ < type(right.rules).__name__
65
+
66
+
67
+ def _swap(left: Op, right: Op) -> SwapResult:
68
+ if type(left.rules) is type(right.rules):
69
+ return left.rules.swap(left, right)
70
+ return SwapResult(terms=((1, (right, left)),))
71
+
72
+
73
+ def _order(prefix: tuple[Op, ...], tail: Monomial) -> dict[Monomial, Coefficient]:
74
+ """Normal-order ``prefix * tail``, where ``tail`` is already normal ordered."""
75
+ terms: dict[Monomial, Coefficient] = {tail: 1}
76
+ for op in reversed(prefix):
77
+ new: dict[Monomial, Coefficient] = {}
78
+ for t, c1 in terms.items():
79
+ for m, c2 in _insert(op, t):
80
+ _accumulate(new, m, as_coefficient(c1 * c2))
81
+ terms = new
82
+ return terms
83
+
84
+
85
+ @functools.lru_cache(maxsize=2**16)
86
+ def _insert(op: Op, tail: Monomial) -> tuple[tuple[Monomial, Coefficient], ...]:
87
+ """Normal-order ``op * tail``, where ``tail`` is already normal ordered.
88
+
89
+ ``op`` moves right past plain exchanges, picking up only their sign. The
90
+ first swap that does anything else is expanded, and the operators ``op``
91
+ passed are put back in front.
92
+ """
93
+ ops = tail.ops
94
+ sign: Coefficient = 1
95
+ i = 0
96
+ while i < len(ops) and not _is_ordered(op, ops[i]):
97
+ terms = _swap(op, ops[i]).terms
98
+ if len(terms) != 1 or terms[0][1] != (ops[i], op):
99
+ break
100
+ sign = as_coefficient(sign * terms[0][0])
101
+ i += 1
102
+
103
+ passed = ops[:i]
104
+ rest: dict[Monomial, Coefficient] = {}
105
+ if i == len(ops) or _is_ordered(op, ops[i]):
106
+ if not passed or _is_ordered(passed[-1], op):
107
+ return ((Monomial((*passed, op, *ops[i:])), sign),)
108
+ rest[Monomial((op, *ops[i:]))] = sign
109
+ else:
110
+ for coeff, repl in terms:
111
+ scale = as_coefficient(sign * coeff)
112
+ for m, c in _order(repl, Monomial(ops[i + 1 :])).items():
113
+ _accumulate(rest, m, as_coefficient(scale * c))
114
+
115
+ acc: dict[Monomial, Coefficient] = {}
116
+ for m, c in rest.items():
117
+ if not passed:
118
+ _accumulate(acc, m, c)
119
+ elif not m.ops or _is_ordered(passed[-1], m.ops[0]):
120
+ _accumulate(acc, Monomial(passed + m.ops), c)
121
+ else:
122
+ for ordered, c2 in _order(passed, m).items():
123
+ _accumulate(acc, ordered, as_coefficient(c * c2))
124
+ return tuple(acc.items())
125
+
126
+
127
+ def _accumulate(
128
+ acc: dict[Monomial, Coefficient], mono: Monomial, coeff: Coefficient
129
+ ) -> None:
130
+ """Add coeff * mono into the accumulator dict.
131
+
132
+ Parameters
133
+ ----------
134
+ acc : dict[Monomial, Coefficient]
135
+ mono : Monomial
136
+ coeff : Coefficient
137
+
138
+ """
139
+ if mono in acc:
140
+ acc[mono] = as_coefficient(acc[mono] + coeff)
141
+ else:
142
+ acc[mono] = coeff