• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

tBuLi / kingdon / 20096369315

09 Dec 2025 11:24AM UTC coverage: 88.356% (+0.2%) from 88.11%
20096369315

push

github

tBuLi
Added test for grad of weighted geometric product

40 of 45 new or added lines in 3 files covered. (88.89%)

14 existing lines in 2 files now uncovered.

1677 of 1898 relevant lines covered (88.36%)

0.88 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

87.37
/kingdon/algebra.py
1
import operator
1✔
2
import re
1✔
3
from itertools import combinations, product, chain
1✔
4
from functools import partial, reduce
1✔
5
from collections import Counter
1✔
6
from dataclasses import dataclass, field, fields
1✔
7
from collections.abc import Mapping, Callable
1✔
8
from typing import List, Tuple
1✔
9
import warnings
1✔
10

11
try:
1✔
12
    from functools import cached_property
1✔
13
except ImportError:
×
14
    from functools import lru_cache
×
15

16
    def cached_property(func):
×
17
        return property(lru_cache()(func))
×
18

19
import numpy as np
1✔
20
import sympy
1✔
21

22
from kingdon.codegen import (
1✔
23
    codegen_gp, codegen_sw, codegen_cp, codegen_ip, codegen_op, codegen_div,
24
    codegen_rp, codegen_acp, codegen_proj, codegen_sp, codegen_lc, codegen_inv,
25
    codegen_rc, codegen_normsq, codegen_add, codegen_neg, codegen_reverse,
26
    codegen_involute, codegen_conjugate, codegen_sub, codegen_sqrt,
27
    codegen_outerexp, codegen_outersin, codegen_outercos, codegen_outertan,
28
    codegen_polarity, codegen_unpolarity, codegen_hodge, codegen_unhodge,
29
)
30
from kingdon.operator_dict import OperatorDict, UnaryOperatorDict, Registry, do_operation, resolve_and_expand
1✔
31
from kingdon.polynomial import mathstr
1✔
32
from kingdon.matrixreps import matrix_rep
1✔
33
from kingdon.multivector import MultiVector
1✔
34
from kingdon.graph import GraphWidget
1✔
35

36
operation_field = partial(field, default_factory=dict, init=False, repr=False, compare=False)
1✔
37

38

39
@dataclass
1✔
40
class Algebra:
1✔
41
    """
42
    A Geometric (Clifford) algebra with :code:`p` positive dimensions,
43
    :code:`q` negative dimensions, and :code:`r` null dimensions.
44

45
    The default settings of :code:`cse = simplify = True` usually strike a good balance between
46
    initiation times and subsequent code execution times.
47

48
    :param p:  number of positive dimensions.
49
    :param q:  number of negative dimensions.
50
    :param r:  number of null dimensions.
51
    :param signature: Optional signature of the algebra, e.g. [0, 1, 1] for 2DPGA.
52
        Mutually exclusive with `p`, `q`, `r`.
53
    :param start_index: Optionally set the start index of the dimensions. For PGA this defualts to `0`, otherwise `1`.
54
    :param basis: Custom basis order, e.g. `["e", "e1", "e2", "e0", "e20", "e01", "e12", "e012"]` for 2DPGA.
55
    :param cse: If :code:`True` (default), attempt Common Subexpression Elimination (CSE)
56
        on symbolically optimized expressions.
57
    :param graded: If :code:`True` (default is :code:`False`), perform binary and unary operations on a graded basis.
58
        This will still be more sparse than computing with a full multivector, but not as sparse as possible.
59
        It does however, vastly reduce the number of possible expressions that have to be symbolically optimized.
60
    :param simplify: If :code:`True` (default), we attempt to simplify as much as possible. Setting this to
61
        :code:`False` will reduce the number of calls to simplify. However, it seems that :code:`True` is still faster,
62
        probably because it keeps sympy expressions from growing too large, which makes both symbolic computations and
63
        printing into a python function slower.
64
    :param wrapper: A function that is always applied to the generated functions as a decorator. For example,
65
        using :code:`numba.njit` as a wrapper will ensure that all kingdon code is jitted using numba.
66
    :param codegen_symbolcls: The symbol class used during codegen. By default, this is our own fast
67
        :class:`~kingdon.polynomial.RationalPolynomial` class.
68
    :param simp_func: This function is applied as a filter function to every multivector coefficient.
69
    :param pretty_blade: character to use for basis blades when pretty printing to string. Default is 𝐞.
70
    :param large: if true this is considered a large algebra. This means various cashing options are removed to save
71
        memory, and codegen is replaced by direct computation since codegen is very resource intensive for big
72
        expressions. By default, algebras of :math:`d > 6` are considered large, but the user can override this setting
73
        because also in large algebras it is still true that the generated code will perform order(s) of magnitude
74
        better than direct computation.
75
    """
76
    p: int = 0
1✔
77
    q: int = 0
1✔
78
    r: int = 0
1✔
79
    d: int = field(init=False, repr=False, compare=False)  # Total number of dimensions
1✔
80
    signature: np.ndarray = field(default=None, compare=False)
1✔
81
    start_index: int = field(default=None, repr=False, compare=False)
1✔
82
    basis: List[str] = field(repr=False, default_factory=list)
1✔
83

84
    # Clever dictionaries that cache previously symbolically optimized lambda functions between elements.
85
    gp: OperatorDict = operation_field(metadata={'codegen': codegen_gp, 'codegen_symbolcls': mathstr})  # geometric product
1✔
86
    sw: OperatorDict = operation_field(metadata={'codegen': codegen_sw})  # conjugation
1✔
87
    cp: OperatorDict = operation_field(metadata={'codegen': codegen_cp, 'codegen_symbolcls': mathstr})  # commutator product
1✔
88
    acp: OperatorDict = operation_field(metadata={'codegen': codegen_acp, 'codegen_symbolcls': mathstr})  # anti-commutator product
1✔
89
    ip: OperatorDict = operation_field(metadata={'codegen': codegen_ip, 'codegen_symbolcls': mathstr})  # inner product
1✔
90
    sp: OperatorDict = operation_field(metadata={'codegen': codegen_sp, 'codegen_symbolcls': mathstr})  # Scalar product
1✔
91
    lc: OperatorDict = operation_field(metadata={'codegen': codegen_lc, 'codegen_symbolcls': mathstr})  # left-contraction
1✔
92
    rc: OperatorDict = operation_field(metadata={'codegen': codegen_rc, 'codegen_symbolcls': mathstr})  # right-contraction
1✔
93
    op: OperatorDict = operation_field(metadata={'codegen': codegen_op, 'codegen_symbolcls': mathstr})  # exterior product
1✔
94
    rp: OperatorDict = operation_field(metadata={'codegen': codegen_rp, 'codegen_symbolcls': mathstr})  # regressive product
1✔
95
    proj: OperatorDict = operation_field(metadata={'codegen': codegen_proj})  # projection
1✔
96
    add: OperatorDict = operation_field(metadata={'codegen': codegen_add, 'codegen_symbolcls': mathstr})  # add
1✔
97
    sub: OperatorDict = operation_field(metadata={'codegen': codegen_sub, 'codegen_symbolcls': mathstr})  # sub
1✔
98
    div: OperatorDict = operation_field(metadata={'codegen': codegen_div})  # division
1✔
99
    # Unary operators
100
    inv: UnaryOperatorDict = operation_field(metadata={'codegen': codegen_inv})  # inverse
1✔
101
    neg: UnaryOperatorDict = operation_field(metadata={'codegen': codegen_neg, 'codegen_symbolcls': mathstr})  # negate
1✔
102
    reverse: UnaryOperatorDict = operation_field(metadata={'codegen': codegen_reverse, 'codegen_symbolcls': mathstr})  # reverse
1✔
103
    involute: UnaryOperatorDict = operation_field(metadata={'codegen': codegen_involute, 'codegen_symbolcls': mathstr})  # grade involution
1✔
104
    conjugate: UnaryOperatorDict = operation_field(metadata={'codegen': codegen_conjugate, 'codegen_symbolcls': mathstr})  # clifford conjugate
1✔
105
    sqrt: UnaryOperatorDict = operation_field(metadata={'codegen': codegen_sqrt})  # Square root
1✔
106
    polarity: UnaryOperatorDict = operation_field(metadata={'codegen': codegen_polarity})
1✔
107
    unpolarity: UnaryOperatorDict = operation_field(metadata={'codegen': codegen_unpolarity})
1✔
108
    hodge: UnaryOperatorDict = operation_field(metadata={'codegen': codegen_hodge, 'codegen_symbolcls': mathstr})
1✔
109
    unhodge: UnaryOperatorDict = operation_field(metadata={'codegen': codegen_unhodge, 'codegen_symbolcls': mathstr})
1✔
110
    normsq: UnaryOperatorDict = operation_field(metadata={'codegen': codegen_normsq})  # norm squared
1✔
111
    outerexp: UnaryOperatorDict = operation_field(metadata={'codegen': codegen_outerexp})
1✔
112
    outersin: UnaryOperatorDict = operation_field(metadata={'codegen': codegen_outersin})
1✔
113
    outercos: UnaryOperatorDict = operation_field(metadata={'codegen': codegen_outercos})
1✔
114
    outertan: UnaryOperatorDict = operation_field(metadata={'codegen': codegen_outertan})
1✔
115
    registry: dict = field(default_factory=dict, repr=False, compare=False)  # Dict of all operator dicts. Should be extended using Algebra.register
1✔
116
    numspace: dict = field(default_factory=dict, repr=False, compare=False)  # Namespace for numerical functions
1✔
117

118
    # Mappings from binary to canonical reps. e.g. 0b01 = 1 <-> 'e1', 0b11 = 3 <-> 'e12'.
119
    canon2bin: dict = field(init=False, repr=False, compare=False)
1✔
120
    bin2canon: dict = field(init=False, repr=False, compare=False)
1✔
121

122
    # Options for the algebra
123
    cse: bool = field(default=True, repr=False)  # Common Subexpression Elimination (CSE)
1✔
124
    graded: bool = field(default=False, repr=False)  # If true, precompute products per grade.
1✔
125
    pretty_blade: str = field(default='𝐞', repr=False, compare=False)
1✔
126
    pretty_digits: dict = field(default_factory=dict, init=False, repr=False, compare=False)  # TODO: this can be defined outside Algebra
1✔
127
    large: bool = field(default=None, repr=False, compare=False)
1✔
128

129
    # Codegen & call customization.
130
    # Wrapper function applied to the codegen generated functions.
131
    wrapper: Callable = field(default=None, repr=False, compare=False)
1✔
132
    # The symbol class used in codegen. By default, use our own fast RationalPolynomial class.
133
    codegen_symbolcls: object = field(default=None, repr=False, compare=False)
1✔
134
    # This simplify func is applied to every component after a symbolic expression is called, to simplify and filter by.
135
    simp_func: Callable = field(default=lambda v: v if not isinstance(v, sympy.Expr) else sympy.simplify(sympy.expand(v)), repr=False, compare=False)
1✔
136

137
    signs: dict = field(init=False, repr=False, compare=False)
1✔
138
    blades: "BladeDict" = field(init=False, repr=False, compare=False)
1✔
139
    pss: object = field(init=False, repr=False, compare=False)
1✔
140

141
    def __post_init__(self):
1✔
142
        if self.signature is not None:
1✔
143
            counts = Counter(self.signature)
1✔
144
            self.p, self.q, self.r = counts[1], counts[-1], counts[0]
1✔
145
            if self.p + self.q + self.r != len(self.signature):
1✔
146
                raise TypeError('Unsupported signature.')
×
147
            self.signature = np.array(self.signature)
1✔
148
        else:
149
            if self.r == 1:  # PGA, so put r first.
1✔
150
                self.signature = np.array([0] * self.r + [1] * self.p + [-1] * self.q)
1✔
151
            else:
152
                self.signature = np.array([1] * self.p + [-1] * self.q + [0] * self.r)
1✔
153

154
        if self.start_index is None:
1✔
155
            self.start_index = 0 if self.r == 1 else 1
1✔
156

157
        self.d = self.p + self.q + self.r
1✔
158

159
        if self.d + self.start_index <= 10:
1✔
160
            self.pretty_digits = {'0': '₀', '1': '₁', '2': '₂', '3': '₃', '4': '₄', '5': '₅', '6': '₆', '7': '₇', '8': '₈', '9': '₉',}
1✔
161
        else:
162
            # Use superscript above 10 because that is almost the entire alphabet.
163
            self.pretty_digits = {
1✔
164
                '0': '⁰', '1': '¹', '2': '²', '3': '³', '4': '⁴',
165
                '5': '⁵', '6': '⁶', '7': '⁷', '8': '⁸', '9': '⁹',
166
                'A': 'ᴬ', 'B': 'ᴮ', 'C': 'ᶜ', 'D': 'ᴰ', 'E': 'ᴱ',
167
                'F': 'ᶠ', 'G': 'ᴳ', 'H': 'ᴴ', 'I': 'ᴵ', 'J': 'ᴶ',
168
                'K': 'ᴷ', 'L': 'ᴸ', 'M': 'ᴹ', 'N': 'ᴺ', 'O': 'ᴼ',
169
                'P': 'ᴾ', 'R': 'ᴿ', 'Q': 'Q', 'S': 'ˢ', 'T': 'ᵀ', 'U': 'ᵁ',
170
                'V': 'ⱽ', 'W': 'ᵂ', 'X': 'ˣ', 'Y': 'ʸ', 'Z': 'ᶻ'
171
            }
172

173
        # Setup mapping from binary to canonical string rep and vise versa
174
        if self.basis:
1✔
175
            assert len(self.basis) == len(self)
1✔
176
            assert self.basis == sorted(self.basis, key=len)  # The basis has to be ordered by grade.
1✔
177
            assert all(eJ[0] == 'e' for eJ in self.basis)
1✔
178
            vecs = [eJ[1:] for eJ in self.basis if len(eJ) == 2]
1✔
179
            self.start_index = int(min(vecs))
1✔
180
            vec2bin = {vec: 2 ** j for j, vec in enumerate(vecs)}
1✔
181
            self.canon2bin = {eJ: reduce(operator.xor, (vec2bin[v] for v in eJ[1:]), 0)
1✔
182
                              for eJ in self.basis}
183
            self.bin2canon = {J: eJ for eJ, J in sorted(self.canon2bin.items(), key=lambda x: x[1])}
1✔
184
        else:
185
            digits = list(self.pretty_digits)
1✔
186
            self.bin2canon = {
1✔
187
                eJ: 'e' + ''.join(digits[ei + self.start_index] for ei in range(0, self.d) if eJ & 2**ei)
188
                for eJ in range(2 ** self.d)
189
            }
190
            self.canon2bin = dict(sorted({c: b for b, c in self.bin2canon.items()}.items(), key=lambda x: (len(x[0]), x[0])))
1✔
191

192
        self.signs = self._prepare_signs()
1✔
193

194
        if self.large is None:
1✔
195
            self.large = self.d > 6
1✔
196
        # Blades are not precomputed for large algebras.
197
        self.blades = BladeDict(algebra=self, lazy=self.large)
1✔
198

199
        self.pss = self.blades[self.bin2canon[2 ** self.d - 1]]
1✔
200

201
        if self.large:
1✔
202
            self.registry = {f.name: self.wrapper(resolve_and_expand(partial(do_operation, codegen=codegen, algebra=self)))
1✔
203
                                     if self.wrapper else resolve_and_expand(partial(do_operation, codegen=codegen, algebra=self))
204
                             for f in fields(self) if (codegen := f.metadata.get('codegen'))}
205
        else:
206
            # Prepare OperatorDict's
207
            self.registry = {f.name: f.type(name=f.name, algebra=self, **f.metadata)
1✔
208
                             for f in fields(self) if 'codegen' in f.metadata}
209
        for name, op in self.registry.items():
1✔
210
            setattr(self, name, op)
1✔
211

212
    @classmethod
1✔
213
    def fromname(cls, name: str, **kwargs):
1✔
214
        """
215
        Initialize a well known algebra by its name. Options are 2DPGA, 3DPGA, and STAP.
216
        This uses sensible ordering of the basis vectors in the basis blades to avoid minus superfluous signs.
217
        """
218
        if name == '2DPGA':
1✔
219
            basis = ["e", "e1", "e2", "e0", "e20", "e01", "e12", "e012"]
×
220
            return cls(2, 0, 1, basis=basis, **kwargs)
×
221
        elif name == '3DPGA':
1✔
222
            basis = ["e", "e1", "e2", "e3", "e0",
1✔
223
                     "e01", "e02", "e03", "e12", "e31", "e23",
224
                     "e032", "e013", "e021", "e123", "e0123"]
225
            return cls(3, 0, 1, basis=basis, **kwargs)
1✔
226
        elif name == 'STAP':
×
227
            basis = ["e", "e0", "e1", "e2", "e3", "e4",
×
228
                     "e01", "e02", "e03", "e40", "e12", "e31", "e23", "e41", "e42", "e43",
229
                     "e234", "e314", "e124", "e123", "e014", "e024", "e034", "e032", "e013", "e021",
230
                     "e0324", "e0134", "e0214", "e0123", "e1234", "e01234"]
231
            return cls(3, 1, 1, basis=basis, **kwargs)
×
232
        else:
233
            raise ValueError("No algebra by this name is known.")
×
234

235
    def __len__(self):
1✔
236
        return 2 ** self.d
1✔
237

238
    def indices_for_grade(self, grade: int):
1✔
239
        """
240
        Function that returns a generator for all the indices for a given grade. E.g. in 2D VGA, this returns
241

242
        .. code-block ::
243

244
            >>> alg = Algebra(2)
245
            >>> tuple(alg.indices_for_grade(1))
246
            (1, 2)
247
        """
248
        return (sum(2**bin for bin in bins) for bins in combinations(range(self.d), r=grade))
1✔
249

250
    def indices_for_grades(self, grades: Tuple[int]):
1✔
251
        """
252
        Function that returns a generator for all the indices from a sequence of grades.
253
        E.g. in 2D VGA, this returns
254

255
        .. code-block ::
256

257
            >>> alg = Algebra(2)
258
            >>> tuple(alg.indices_for_grades((1, 2)))
259
            (1, 2, 3)
260
        """
261
        return (chain.from_iterable(self.indices_for_grade(grade) for grade in sorted(grades)))
1✔
262

263
    @cached_property
1✔
264
    def matrix_basis(self):
1✔
265
        return matrix_rep(self.p, self.q, self.r, signature=self.signature)
1✔
266

267
    @cached_property
1✔
268
    def frame(self) -> list:
1✔
269
        r"""
270
        The set of orthogonal basis vectors, :math:`\{ e_i \}`. Note that for a frame linear independence suffices,
271
        but we already have orthogonal basis vectors so why not use those?
272
        """
273
        return [self.blades[self.bin2canon[2**j]] for j in range(0, self.d)]
1✔
274

275
    @cached_property
1✔
276
    def reciprocal_frame(self) -> list:
1✔
277
        r"""
278
        The reciprocal frame is a set of vectors :math:`\{ e^i \}` that satisfies
279
        :math:`e^i \cdot e_j = \delta^i_j` with the frame vectors :math:`\{ e_i \}`.
280
        """
281
        return [v.inv() for v in self.frame]
1✔
282

283
    def _prepare_signs(self):
1✔
284
        r"""
285
        Prepares a dict whose keys are a pair of basis-blades (in binary rep) and the
286
        result is the sign (1, -1, 0) of the corresponding multiplication.
287

288
        E.g. in :math:`\mathbb{R}_2`, sings[(0b11, 0b11)] = -1.
289
        """
290
        signs = {}
1✔
291

292
        def _compute_sign(bin_pair, canon_pair=None):
1✔
293
            I, J = bin_pair
1✔
294
            if not canon_pair:
1✔
295
                canon_pair = self.bin2canon[I], self.bin2canon[J]
1✔
296
            eI, eJ = canon_pair
1✔
297
            # Compute the number of swaps of orthogonal vectors needed to order the basis vectors.
298
            swaps, prod, eliminated = _swap_blades(eI[1:], eJ[1:], self.bin2canon[I ^ J][1:])
1✔
299

300
            # Remove even powers of basis-vectors.
301
            sign = -1 if swaps % 2 else 1
1✔
302
            for key in eliminated:
1✔
303
                sign *= self.signature[int(key, base=len(self.pretty_digits)) - self.start_index]
1✔
304
            return sign
1✔
305

306
        if self.d > 6:
1✔
307
            return DefaultKeyDict(_compute_sign)
1✔
308

309
        for (eI, I), (eJ, J) in product(self.canon2bin.items(), repeat=2):
1✔
310
            signs[I, J] = _compute_sign((I, J), (eI, eJ))
1✔
311

312
        return signs
1✔
313

314
    @cached_property
1✔
315
    def cayley(self):
1✔
316
        """ Cayley table of the algebra. """
317
        cayley = {}
1✔
318
        for (eI, I), (eJ, J) in product(self.canon2bin.items(), repeat=2):
1✔
319
            if sign := self.signs[I, J]:
1✔
320
                sign = '-' if sign == -1 else ''
1✔
321
                cayley[eI, eJ] = f'{sign}{self.bin2canon[I ^ J]}'
1✔
322
            else:
323
                cayley[eI, eJ] = f'0'
1✔
324
        return cayley
1✔
325

326
    def register(self, expr=None, /, *, name=None, symbolic=False):
1✔
327
        """
328
        Compile a function with the algebra to optimize its execution times. Deprecated in favor of :meth:`~kingdon.algebra.Algebra.compile`.
329
        """
330
        warnings.warn("Use @alg.compile instead of @alg.register", FutureWarning)
×
331
        return self.compile(expr, name=name, symbolic=symbolic)
×
332

333
    def compile(self, expr=None, /, *, name=None, symbolic=False, codegen_symbolcls=None):
1✔
334
        """
335
        Compile a function with the algebra to optimize its execution times. 
336
        The function must be a valid GA expression, not an arbitrary python function.
337

338
        Example:
339

340
        .. code-block ::
341

342
            @alg.compile
343
            def myexpr(a, b):
344
                return a @ b
345

346
            @alg.register(symbolic=True)
347
            def myexpr(a, b):
348
                return a @ b
349

350
        With default settings, the decorator will ensure that every GA unary or binary
351
        operator is replaced by the corresponding numerical function, and produces
352
        numerically much more performant code. The speed up is particularly notible when
353
        e.g. `self.wrapper=numba.njit`, because then the cost for all the python glue surrounding
354
        the actual computation has to be paid only once.
355

356
        When `symbolic=True` the expression is symbolically optimized before being turned
357
        into a numerical function. Beware that symbolic optimization of longer expressions
358
        can (currently) take exorbitant amounts of time, and often isn't worth it if the end
359
        goal is numerical computation.
360

361
        :param expr: Python function of a valid kingdon GA expression.
362
        :param name: (optional) name by which the function will be known to the algebra.
363
            By default, this is the `expr.__name__`.
364
        :param symbolic: (optional) If true, the expression is symbolically optimized.
365
            By default this is False, given the cost of optimizing large expressions.
366
        :param codegen_symbolcls: (optional) The class to use for symbolic multivectors.
367
            By default the codegen_symbolcls from Algebra is used.
368
        """
369
        def wrap(expr, name=None, symbolic=False):
1✔
370
            if name is None:
1✔
371
                name = expr.__name__
1✔
372

373
            if not symbolic:
1✔
374
                self.registry[expr] = Registry(name, codegen=expr, algebra=self)
1✔
375
            else:
376
                self.registry[expr] = OperatorDict(name, codegen=expr, algebra=self, codegen_symbolcls=codegen_symbolcls or OperatorDict.codegen_symbolcls)
1✔
377
            return self.registry[expr]
1✔
378

379
        # See if we are being called as @compile or @compile()
380
        if expr is None:
1✔
381
            # Called as @compile()
382
            return partial(wrap, name=name, symbolic=symbolic)
1✔
383

384
        # Called as @compile
385
        return wrap(expr, name=name, symbolic=symbolic)
1✔
386

387
    def multivector(self, *args, **kwargs) -> MultiVector:
1✔
388
        """ Create a new :class:`~kingdon.multivector.MultiVector`. """
389
        return MultiVector(self, *args, **kwargs)
1✔
390

391
    def evenmv(self, *args, **kwargs) -> MultiVector:
1✔
392
        """ Create a new :class:`~kingdon.multivector.MultiVector` in the even subalgebra. """
393
        grades = tuple(filter(lambda x: x % 2 == 0, range(self.d + 1)))
1✔
394
        return MultiVector(self, *args, grades=grades, **kwargs)
1✔
395

396
    def oddmv(self, *args, **kwargs) -> MultiVector:
1✔
397
        """
398
        Create a new :class:`~kingdon.multivector.MultiVector` of odd grades.
399
        (There is technically no such thing as an odd subalgebra, but
400
        otherwise this is similar to :class:`~kingdon.algebra.Algebra.evenmv`.)
401
        """
402
        grades = tuple(filter(lambda x: x % 2 == 1, range(self.d + 1)))
1✔
403
        return MultiVector(self, *args, grades=grades, **kwargs)
1✔
404

405
    def purevector(self, *args, grade, **kwargs) -> MultiVector:
1✔
406
        """
407
        Create a new :class:`~kingdon.multivector.MultiVector` of a specific grade.
408

409
        :param grade: Grade of the mutivector to create.
410
        """
411
        return MultiVector(self, *args, grades=(grade,), **kwargs)
1✔
412

413
    def scalar(self, *args, **kwargs) -> MultiVector:
1✔
414
        return self.purevector(*args, grade=0, **kwargs)
1✔
415

416
    def vector(self, *args, **kwargs) -> MultiVector:
1✔
417
        return self.purevector(*args, grade=1, **kwargs)
1✔
418

419
    def bivector(self, *args, **kwargs) -> MultiVector:
1✔
420
        return self.purevector(*args, grade=2, **kwargs)
1✔
421

422
    def trivector(self, *args, **kwargs) -> MultiVector:
1✔
UNCOV
423
        return self.purevector(*args, grade=3, **kwargs)
×
424

425
    def quadvector(self, *args, **kwargs) -> MultiVector:
1✔
UNCOV
426
        return self.purevector(*args, grade=4, **kwargs)
×
427

428
    def pseudoscalar(self, *args, **kwargs) -> MultiVector:
1✔
UNCOV
429
        return self.purevector(*args, grade=self.d - 0, **kwargs)
×
430

431
    def pseudovector(self, *args, **kwargs) -> MultiVector:
1✔
UNCOV
432
        return self.purevector(*args, grade=self.d - 1, **kwargs)
×
433

434
    def pseudobivector(self, *args, **kwargs) -> MultiVector:
1✔
UNCOV
435
        return self.purevector(*args, grade=self.d - 2, **kwargs)
×
436

437
    def pseudotrivector(self, *args, **kwargs) -> MultiVector:
1✔
UNCOV
438
        return self.purevector(*args, grade=self.d - 3, **kwargs)
×
439

440
    def pseudoquadvector(self, *args, **kwargs) -> MultiVector:
1✔
UNCOV
441
        return self.purevector(*args, grade=self.d - 4, **kwargs)
×
442

443
    def graph(self, *subjects, graph_widget=GraphWidget, **options):
1✔
444
        """
445
        The graph function outputs :code:`ganja.js` renders and is meant
446
        for use in jupyter notebooks. The syntax of the graph function will feel
447
        familiar to users of :code:`ganja.js`: all position arguments are considered
448
        as subjects to graph, while all keyword arguments are interpreted as options
449
        to :code:`ganja.js`'s :code:`Algebra.graph` method.
450

451
        Example usage:
452

453
        .. code-block ::
454

455
            alg.graph(
456
                0xD0FFE1, [A,B,C],
457
                0x224488, A, "A", B, "B", C, "C",
458
                lineWidth=3, grid=1, labels=1
459
            )
460

461
        Will create
462

463
        .. image :: ../docs/_static/graph_triangle.png
464
            :scale: 50%
465
            :align: center
466

467
        If a function is given to :code:`Algebra.graph` then it is called without arguments.
468
        This can be used to make animations in a manner identical to :code:`ganja.js`.
469

470
        Example usage:
471

472
        .. code-block ::
473

474
            def graph_func():
475
                return [
476
                    0xD0FFE1, [A,B,C],
477
                    0x224488, A, "A", B, "B", C, "C"
478
                ]
479

480
            alg.graph(
481
                graph_func,
482
                lineWidth=3, grid=1, labels=1
483
            )
484

485
        :param `*subjects`: Subjects to be graphed.
486
            Can be strings, hexadecimal colors, (lists of) MultiVector, (lists of) callables.
487
        :param camera: [optional] a motor that places the camera at the desired viewpoint.
488
        :param up: [optional] the 'up' (C) function that takes a Euclidean point and casts it into a larger
489
            embedding space. This will invoke ganja's OPNS renderer, which can be used to render any algebra.
490
            Examples include 2D CSGA, 3D CCGA, 3D Mother Algebra, etc. See the teahouse for examples.
491
        :param `**options`: Other options passed to :code:`ganja.js`'s :code:`Algebra.graph`.
492

493
        """
494
        return graph_widget(
1✔
495
            algebra=self,
496
            raw_subjects=subjects,
497
            options=options,
498
        )
499

500
    def _blade2canon(self, basis_blade: str):
1✔
501
        """ Retrieve the canonical blade for a given blade, and the number of sing swaps required. """
502
        if basis_blade in self.canon2bin:
1✔
503
            return basis_blade, 0
1✔
504
        # if a generator isn't found, return a generator outside of the current space.
505
        bin = reduce(operator.or_, (self.canon2bin.get(f'e{i}', 2 ** self.d) for i in basis_blade[1:]))
1✔
506
        canon_blade = self.bin2canon.get(bin, False)
1✔
507
        if canon_blade:
1✔
508
            swaps, *_ = _swap_blades(basis_blade, '', target=canon_blade)
1✔
509
            return canon_blade, swaps
1✔
510
        return f'e{2 ** self.d}', 0
1✔
511

512
    def _swap_blades_bin(self, A: int, B: int):
1✔
513
        """
514
        Swap basis blades binary style. Not currently used because (suprinsingly) this does not
515
        seem to be faster than the string manipulation version.
516
        """
517
        ab = A & B
×
518
        res = A ^ B
×
UNCOV
519
        if ab & ((1 << self.r) - 1):
×
520
            return [0, 0]
×
521

522
        t = A >> 1
×
523
        t ^= t >> 1
×
524
        t ^= t >> 2
×
UNCOV
525
        t ^= t >> 4
×
526
        t ^= t >> 8
×
527

528
        t &= B
×
529
        t ^= ab >> (self.p + self.r)
×
530
        t ^= t >> 16
×
531
        t ^= t >> 8
×
UNCOV
532
        t ^= t >> 4
×
UNCOV
533
        return [res, 1 - 2 * (27030 >> (t & 15) & 1)]
×
534

535

536
def _swap_blades(blade1: str, blade2: str, target: str = '') -> (int, str, str):
1✔
537
    """
538
    Compute the number of swaps of orthogonal vectors needed to pair the basis vectors. E.g. in
539
    ['1', '2', '3', '1', '2'] we need 3 swaps to get to ['1', '1', '2', '2', '3']. Pairs are also removed,
540
    in order to find the resulting blade; in the above example the result is ['3'].
541

542
    The output of the function is the number of swaps, the resulting blade indices, and the eliminated indices. E.g.
543

544
    .. code-block ::
545

546
            >>> _swap_blades('123', '12')
547
            3, '3', '12'
548
    """
549
    blade1 = list(blade1)
1✔
550
    swaps = 0
1✔
551
    eliminated = []
1✔
552
    for char in blade2:
1✔
553
        if char not in blade1:  # Move char from blade2 to blade1
1✔
554
            blade1.append(char)
1✔
555
            continue
1✔
556

557
        idx = blade1.index(char)
1✔
558
        swaps += len(blade1) - idx - 1
1✔
559
        blade1.remove(char)
1✔
560
        eliminated.append(char)
1✔
561

562
    if target:
1✔
563
        # Find the number of additional swaps needed to match the target.
564
        for i, char in enumerate(target):
1✔
565
            idx = blade1.index(char)
1✔
566
            blade1.insert(i, blade1.pop(idx))
1✔
567
            swaps += idx - i
1✔
568

569
    return swaps, ''.join(blade1), ''.join(eliminated)
1✔
570

571

572
class DefaultKeyDict(dict):
1✔
573
    """
574
    A lightweight dict subclass that behaves like a defaultdict
575
    but calls the factory function with the key as argument.
576
    """
577
    def __init__(self, factory):
1✔
578
        self.factory = factory
1✔
579

580
    def __missing__(self, key):
1✔
581
        res = self[key] = self.factory(key)
1✔
582
        return res
1✔
583

584

585
@dataclass
1✔
586
class BladeDict(Mapping):
1✔
587
    """
588
    Dictionary of basis blades. Use getitem or getattr to retrieve a basis blade from this dict, e.g.::
589

590
        alg = Algebra(3, 0, 1)
591
        blade_dict = BladeDict(alg, lazy=True)
592
        blade_dict['e03']
593
        blade_dict.e03
594

595
    When `lazy=True`, the basis blade is only initiated when requested.
596
    This is done for performance in higher dimensional algebras.
597
    """
598
    algebra: Algebra
1✔
599
    lazy: bool = field(default=False)
1✔
600
    blades: dict = field(default_factory=dict, init=False, repr=False, compare=False)
1✔
601

602
    def __post_init__(self):
1✔
603
        if not self.lazy:
1✔
604
            # If not lazy, retrieve all blades once to force initiation.
605
            for blade in self.algebra.canon2bin: self[blade]
1✔
606

607
    def __getitem__(self, basis_blade):
1✔
608
        """ Blade must be in canonical form, e.g. 'e12'. """
609
        if not re.match(r'^e[0-9a-fA-Z]*$', basis_blade):
1✔
UNCOV
610
            raise AttributeError(f'{basis_blade} is not a valid basis blade.')
×
611
        basis_blade, swaps = self.algebra._blade2canon(basis_blade)
1✔
612
        if basis_blade not in self.blades:
1✔
613
            bin_blade = self.algebra.canon2bin[basis_blade]
1✔
614
            if self.algebra.graded:
1✔
615
                g = format(bin_blade, 'b').count('1')
1✔
616
                indices = self.algebra.indices_for_grade(g)
1✔
617
                self.blades[basis_blade] = self.algebra.multivector(values=[int(bin_blade == i) for i in indices], grades=(g,))
1✔
618
            else:
619
                self.blades[basis_blade] = MultiVector.fromkeysvalues(self.algebra, keys=(bin_blade,), values=[1])
1✔
620
        return self.blades[basis_blade] if swaps % 2 == 0 else - self.blades[basis_blade]
1✔
621

622
    def __getattr__(self, blade):
1✔
623
        return self[blade]
1✔
624

625
    def __len__(self):
1✔
626
        return len(self.blades)
1✔
627

628
    def __iter__(self):
1✔
629
        return iter(self.blades)
1✔
630

631
    def grade(self, *grades) -> dict:
1✔
632
        """
633
        Return blades of grade `grades`.
634

635
        :param grades: tuple or ints, grades to select.
636
        """
637
        if len(grades) == 1 and isinstance(grades[0], tuple):
1✔
UNCOV
638
            grades = grades[0]
×
639

640
        return {(blade := self.algebra.bin2canon[k]): self[blade]
1✔
641
                for k in self.algebra.indices_for_grades(grades)}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc