• 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

89.9
/kingdon/multivector.py
1
import operator
1✔
2
from collections.abc import Mapping
1✔
3
from copy import deepcopy
1✔
4
from dataclasses import dataclass, field
1✔
5
from functools import reduce, cached_property
1✔
6
from typing import Generator, ClassVar
1✔
7
from itertools import product
1✔
8
import re
1✔
9
import math
1✔
10
import sys
1✔
11

12
from sympy import Expr, Symbol, sympify, sinc, cos
1✔
13

14
from kingdon.codegen import _lambdify_mv
1✔
15
from kingdon.polynomial import RationalPolynomial
1✔
16

17
if sys.version_info >= (3, 10):
1✔
18
    _bit_count = int.bit_count
1✔
19
else:
20
    def _bit_count(n):
×
21
        count = 0
×
22
        while n:
×
23
            n &= n - 1
×
24
            count += 1
×
25
        return count
×
26

27

28
class MultiVectorType(type):
1✔
29
    """
30
    MultiVector type allows typehinting for MultiVectors of a given shape.
31
    For example, :code:`MultiVector[3]` is interpreted as a MultiVectors of shape (N, 3) by :code:`Algebra.compile`,
32
    where N is the number of blades in the multivector.
33
    """
34
    def __getitem__(cls, item): return cls, item
1✔
35

36
@dataclass(init=False)
1✔
37
class MultiVector(metaclass=MultiVectorType):
1✔
38
    algebra: "Algebra"
1✔
39
    _values: list = field(default_factory=list)
1✔
40
    _keys: tuple = field(default_factory=tuple)
1✔
41

42
    # Make MultiVector "primary" operand in operations involving ndarray.
43
    # (forces reflected (swapped) operands operations, like __radd__)
44
    __array_priority__: ClassVar[int] = 1
1✔
45

46
    def __copy__(self):
1✔
47
        return self.fromkeysvalues(self.algebra, self._keys, self._values)
1✔
48

49
    def __deepcopy__(self, memo):
1✔
50
        return self.fromkeysvalues(self.algebra, self._keys, deepcopy(self._values))
1✔
51

52
    def __new__(cls, algebra: "Algebra", values=None, keys=None, *, name=None, grades=None, symbolcls=Symbol, **items):
1✔
53
        """
54
        :param algebra: Instance of :class:`~kingdon.algebra.Algebra`.
55
        :param keys: Keys corresponding to the basis blades in either binary rep or as strings, e.g. :code:'"e12"'.
56
        :param values: Values of the multivector. If keys are provided, then keys and values should
57
            satisfy :code:`len(keys) == len(values)`. If no keys nor grades are provided, :code:`len(values)`
58
            should equal :code:`len(algebra)`, i.e. a full multivector. If grades is provided,
59
            then :code:`len(values)` should be identical to the number of values in a multivector
60
            of that grade.
61
        :param name: Base string to be used as the name for symbolic values.
62
        :param grades: Optional, :class:`tuple` of grades in this multivector.
63
            If present, :code:`keys` is checked against these grades.
64
        :param symbolcls: Optional, class to be used for symbol creation. This is a :class:`sympy.Symbol` by default,
65
            but could be e.g. :class:`symfit.Variable` or :class:`symfit.Parameter` when the goal is to use this
66
            multivector in a fitting problem.
67
        :param items: keyword arguments can be used to initiate multivectors as well, e.g.
68
            :code:`MultiVector(alg, e12=1)`. Mutually exclusive with `values` and `keys`.
69
        """
70
        if items and keys is None and values is None:
1✔
71
            for key in list(items.keys()):
1✔
72
                if key not in algebra.canon2bin:
1✔
73
                    target, swaps = algebra._blade2canon(key)
1✔
74
                    if swaps % 2:
1✔
75
                        items[target] = - items.pop(key)
1✔
76

77
            keys, values = zip(*((blade, items[blade]) for blade in algebra.canon2bin if blade in items))
1✔
78
            values = list(values)
1✔
79

80
        # Sanitize input
81
        if keys is not None and not all(isinstance(k, int) for k in keys):
1✔
82
            keys = tuple(k if k in algebra.bin2canon else algebra.canon2bin[k] for k in keys)
1✔
83
        if grades is None and name and keys is not None:
1✔
84
            grades = tuple(sorted({_bit_count(k) for k in keys}))
1✔
85
        values = values if values is not None else list()
1✔
86
        keys = keys if keys is not None else tuple()
1✔
87

88
        if grades is not None:
1✔
89
            if not all(0 <= grade <= algebra.d for grade in grades):
1✔
90
                raise ValueError(f'Each grade in `grades` needs to be a value between 0 and {algebra.d}.')
1✔
91
        else:
92
            if keys:
1✔
93
                grades = tuple(sorted({format(k, 'b').count('1') for k in keys}))
1✔
94
            else:
95
                grades = tuple(range(algebra.d + 1))
1✔
96

97
        if algebra.graded and keys and len(keys) != sum(math.comb(algebra.d, grade) for grade in grades):
1✔
98
            raise ValueError(f"In graded mode, the number of keys should be equal to "
1✔
99
                             f"those expected for a multivector of {grades=}.")
100

101
        # Construct a new MV on the basis of the kind of input we received.
102
        if isinstance(values, Mapping):
1✔
103
            keys, values = zip(*values.items()) if values else (tuple(), list())
1✔
104
            values = list(values)
1✔
105
        elif len(values) == sum(math.comb(algebra.d, grade) for grade in grades) and not keys:
1✔
106
            keys = tuple(algebra.indices_for_grades(grades))
1✔
107
        elif name and not values:
1✔
108
            # values was not given, but we do have a name. So we are in symbolic mode.
109
            keys = tuple(algebra.indices_for_grades(grades)) if not keys else keys
1✔
110
            values = list(symbolcls(f'{name}{algebra.bin2canon[k][1:]}') for k in keys)
1✔
111
        elif len(keys) != len(values):
1✔
112
            raise TypeError(f'Length of `keys` and `values` have to match.')
1✔
113

114
        if not all(isinstance(k, int) for k in keys):
1✔
115
            keys = tuple(key if key in algebra.bin2canon else algebra.canon2bin[key]
1✔
116
                         for key in keys)
117

118
        if any(isinstance(v, str) for v in values):
1✔
119
            values = list(val if not isinstance(val, str) else sympify(val)
1✔
120
                          for val in values)
121

122
        if not all(_bit_count(k) in grades for k in keys):
1✔
123
            raise ValueError(f"All keys should be of grades {grades}.")
1✔
124

125
        return cls.fromkeysvalues(algebra, keys, values)
1✔
126

127
    @classmethod
1✔
128
    def fromkeysvalues(cls, algebra, keys, values):
1✔
129
        """
130
        Initiate a multivector from a sequence of keys and a sequence of values.
131
        """
132
        obj = object.__new__(cls)
1✔
133
        obj.algebra = algebra
1✔
134
        obj._values = values
1✔
135
        obj._keys = keys
1✔
136
        return obj
1✔
137

138
    @classmethod
1✔
139
    def frommatrix(cls, algebra, matrix):
1✔
140
        """
141
        Initiate a multivector from a matrix. This matrix is assumed to be
142
        generated by :class:`~kingdon.multivector.MultiVector.asmatrix`, and
143
        thus we only read the first column of the input matrix.
144
        """
145
        obj = cls(algebra=algebra, values=matrix[..., 0])
1✔
146
        return obj
1✔
147

148
    def keys(self):
1✔
149
        return self._keys
1✔
150

151
    def values(self):
1✔
152
        return self._values
1✔
153

154
    def items(self):
1✔
155
        return zip(self._keys, self._values)
1✔
156

157
    def __len__(self):
1✔
158
        return len(self._values)
1✔
159

160
    @cached_property
1✔
161
    def type_number(self) -> int:
1✔
162
        return int(''.join('1' if i in self.keys() else '0' for i in reversed(self.algebra.canon2bin.values())), 2)
1✔
163

164

165
    def itermv(self, axis=None) -> Generator["MultiVector", None, None]:
1✔
166
        """
167
        Returns an iterator over the multivectors within this multivector, if it is a multidimensional multivector.
168
        For example, if you have a pointcloud of N points, itermv will iterate over these points one at a time.
169

170
        :param axis: Axis over which to iterate. Default is to iterate over all possible mv.
171
        """
172
        shape = self.shape[1:]
1✔
173
        if not shape:
1✔
174
            return self
×
175
        elif axis is None:
1✔
176
            return (
1✔
177
                self[indices]
178
                for indices in product(*(range(n) for n in shape))
179
            )
180
        else:
181
            raise NotImplementedError
×
182

183
    @property
1✔
184
    def shape(self):
1✔
185
        """ Return the shape of the .values() attribute of this multivector. """
186
        if hasattr(self._values, 'shape'):
1✔
187
            return self._values.shape
1✔
188
        elif hasattr(self._values[0], 'shape'):
1✔
189
            return len(self), *self._values[0].shape
×
190
        elif isinstance(self._values[0], (tuple, list)):
1✔
191
            if not all(len(v) == len(self._values[0]) for v in self._values):
1✔
NEW
192
                raise ValueError("All values of a Multivectors must have the same length in order for the mulivector to have a consistent shape.")
×
193
            return len(self), len(self._values[0])
1✔
194
        else:
195
            return len(self),
1✔
196

197
    @cached_property
1✔
198
    def grades(self):
1✔
199
        """ Tuple of the grades present in `self`. """
200
        return tuple(sorted({bin(ind).count('1') for ind in self.keys()}))
1✔
201

202
    def grade(self, *grades):
1✔
203
        """
204
        Returns a new  :class:`~kingdon.multivector.MultiVector` instance with
205
        only the selected `grades` from `self`.
206

207
        :param grades: tuple or ints, grades to select.
208
        """
209
        if len(grades) == 1 and isinstance(grades[0], tuple):
1✔
210
            grades = grades[0]
1✔
211

212
        items = {k: v for k, v in self.items() if _bit_count(k) in grades}
1✔
213
        return self.fromkeysvalues(self.algebra, tuple(items.keys()), list(items.values()))
1✔
214

215
    @cached_property
1✔
216
    def issymbolic(self):
1✔
217
        """ True if this mv contains Symbols, False otherwise. """
218
        symbol_classes = (Expr, RationalPolynomial)
1✔
219
        if self.algebra.codegen_symbolcls:
1✔
220
            # Allowed symbol classes. codegen_symbolcls might refer to a constructor (method): get the class instead.
221
            symbolcls = self.algebra.codegen_symbolcls
×
222
            symbol_classes = (*symbol_classes, symbolcls.__self__ if hasattr(symbolcls, '__self__') else symbolcls)
×
223
        return any(isinstance(v, symbol_classes) for v in self.values())
1✔
224

225
    def neg(self):
1✔
226
        return self.algebra.neg(self)
1✔
227

228
    __neg__ = neg
1✔
229

230
    def __invert__(self):
1✔
231
        """ Reversion """
232
        return self.algebra.reverse(self)
1✔
233

234
    def reverse(self):
1✔
235
        """ Reversion """
236
        return self.algebra.reverse(self)
1✔
237

238
    def involute(self):
1✔
239
        """ Main grade involution. """
240
        return self.algebra.involute(self)
1✔
241

242
    def conjugate(self):
1✔
243
        """ Clifford conjugation: involution and reversion combined. """
244
        return self.algebra.conjugate(self)
1✔
245

246
    def sqrt(self):
1✔
247
        return self.algebra.sqrt(self)
1✔
248

249
    def normsq(self):
1✔
250
        return self.algebra.normsq(self)
1✔
251

252
    def norm(self):
1✔
253
        normsq = self.normsq()
1✔
254
        return normsq.sqrt()
1✔
255

256
    def normalized(self):
1✔
257
        """ Normalized version of this multivector. """
258
        return self / self.norm()
1✔
259

260
    def inv(self):
1✔
261
        """ Inverse of this multivector. """
262
        return self.algebra.inv(self)
1✔
263

264
    def add(self, other):
1✔
265
        return self.algebra.add(self, other)
1✔
266

267
    __radd__ = __add__ = add
1✔
268

269
    def sub(self, other):
1✔
270
        return self.algebra.sub(self, other)
1✔
271

272
    __sub__ = sub
1✔
273

274
    def __rsub__(self, other):
1✔
275
        return self.algebra.sub(other, self)
1✔
276

277
    def div(self, other):
1✔
278
        return self.algebra.div(self, other)
1✔
279

280
    __truediv__ = div
1✔
281

282
    def __rtruediv__(self, other):
1✔
283
        return self.algebra.div(other, self)
1✔
284

285
    def __str__(self):
1✔
286
        if not len(self.values()):
1✔
287
            return '0'
×
288

289
        def print_value(val):
1✔
290
            s = str(val)
1✔
291
            if isinstance(val, Expr):
1✔
292
                if val.is_Symbol:
1✔
293
                    return s
×
294
                return f"({s})"
1✔
295
            if isinstance(val, float):
1✔
296
                return f'{val:.3}'
×
297
            if isinstance(val, int):
1✔
298
                return s
1✔
299
            if bool(re.search(r'[\(\[\{].*[\)\]\}]$', s)):
1✔
300
                # If the expression already has brackets, like numpy arrays
301
                return s
×
302
            return f'({s})'
1✔
303

304
        def print_key(blade):
1✔
305
            if blade == 'e':
1✔
306
                return '1'
1✔
307
            return self.algebra.pretty_blade + ''.join(self.algebra.pretty_digits[num] for num in blade[1:])
1✔
308

309
        canon_sorted_vals = {print_key(self.algebra.bin2canon[key]): val
1✔
310
                             for key, val in self.items()}
311
        str_repr = ' + '.join(
1✔
312
            [f'{print_value(val)} {blade}' if blade != '1' else f'{print_value(val)}'
313
             for blade, val in canon_sorted_vals.items() if (val.any() if hasattr(val, 'any') else val)]
314
        )
315
        return str_repr
1✔
316

317
    def _repr_pretty_(self, p, cycle):
1✔
318
        if cycle:
×
319
            p.text(f'{self.__class__.__name__}(...)')
×
320
        else:
321
            p.text(str(self))
×
322

323
    def __format__(self, format_spec):
1✔
324
        if format_spec == 'keys_binary':
×
325
            return bin(self.type_number)[2:].zfill(len(self.algebra))
×
326
        return str(self)
×
327

328
    def __getitem__(self, item):
1✔
329
        values = self.values()
1✔
330
        if isinstance(values, (tuple, list)):
1✔
331
            return_values = values.__class__(value[item] for value in values)
1✔
332
        else:
333
            if not isinstance(item, tuple):
1✔
334
                item = (item,)
1✔
335
            return_values = values[(slice(None), *item)]
1✔
336
        return self.__class__.fromkeysvalues(self.algebra, keys=self.keys(), values=return_values)
1✔
337

338
    def __setitem__(self, indices, values):
1✔
339
        if isinstance(values, MultiVector):
1✔
340
            if self.keys() != values.keys():
1✔
341
                raise ValueError('setitem with a multivector is only possible for equivalent MVs.')
×
342
            values = values.values()
1✔
343

344
        if not isinstance(indices, tuple):
1✔
345
            indices = (indices,)
1✔
346

347
        if isinstance(self.values(), (tuple, list)):
1✔
348
            for self_values, other_value in zip(self.values(), values):
1✔
349
                self_values[indices] = other_value
1✔
350
        else:
351
            self.values()[(slice(None), *indices)] = values
×
352

353
    def __getattr__(self, basis_blade):
1✔
354
        # TODO: if this first check is not true, raise hell instead?
355
        if not re.match(r'^e[0-9a-fA-Z]*$', basis_blade):
1✔
356
            raise AttributeError(f'{self.__class__.__name__} object has no attribute or basis blade {basis_blade}')
1✔
357
        basis_blade, swaps = self.algebra._blade2canon(basis_blade)
1✔
358
        if basis_blade not in self.algebra.canon2bin:
1✔
359
            return 0
1✔
360
        try:
1✔
361
            idx = self.keys().index(self.algebra.canon2bin[basis_blade])
1✔
362
        except ValueError:
1✔
363
            return 0
1✔
364
        return self._values[idx] if swaps % 2 == 0 else - self._values[idx]
1✔
365

366
    def __setattr__(self, basis_blade, value):
1✔
367
        if not re.match(r'^e[0-9a-fA-Z]*$', basis_blade):
1✔
368
            return super().__setattr__(basis_blade, value)
1✔
369
        if (key := self.algebra.canon2bin[basis_blade]) in self.keys():
1✔
370
            self._values[key] = value
1✔
371
        else:
372
            raise TypeError("The keys of a MultiVector are immutable, please create a new MultiVector.")
1✔
373

374
    def __delattr__(self, basis_blade, value):
1✔
375
        if not re.match(r'^e[0-9a-fA-Z]*$', basis_blade):
×
376
            return super().__setattr__(basis_blade, value)
×
377
        raise TypeError("The keys of a MultiVector are immutable, please create a new MultiVector.")
×
378

379
    def __contains__(self, item):
1✔
380
        item = item if isinstance(item, int) else self.algebra.canon2bin[item]
1✔
381
        return item in self._keys
1✔
382

383
    def __bool__(self):
1✔
384
        return bool(self.values())
1✔
385

386
    @cached_property
1✔
387
    def free_symbols(self) -> set:
1✔
388
        return reduce(operator.or_, (v.free_symbols for v in self.values() if hasattr(v, "free_symbols")), set())
1✔
389

390
    def map(self, func) -> "MultiVector":
1✔
391
        """
392
        Returns a new multivector where `func` has been applied to all the values.
393
        If `func` has one argument, it is called on each entry of self.values().
394
        If `func` has two arguments, the function is called with the key, value pairs as per
395
        self.items() instead.
396
        """
397
        if hasattr(func, '__code__') and func.__code__.co_argcount == 2:
1✔
398
            vals = [func(k, v) for k, v in self.items()]
1✔
399
        else:
400
            vals = [func(v) for v in self.values()]
1✔
401
        return self.fromkeysvalues(self.algebra, keys=self.keys(), values=vals)
1✔
402

403
    def filter(self, func=None) -> "MultiVector":
1✔
404
        """
405
        Returns a new multivector containing only those elements for which `func` was true-ish.
406
        If no function was provided, use the simp_func of the Algebra.
407
        If `func` has one argument, it is called on each entry of self.values().
408
        If `func` has two arguments, the function is called with the key, value pairs as per
409
        self.items() instead.
410
        """
411
        if func is None:
1✔
412
            func = self.algebra.simp_func
1✔
413
        if hasattr(func, '__code__') and func.__code__.co_argcount == 2:
1✔
414
            keysvalues = tuple((k, v) for k, v in self.items() if func(k, v))
1✔
415
        else:
416
            keysvalues = tuple((k, v) for k, v in self.items() if func(v))
1✔
417
        if not keysvalues:
1✔
418
            return self.fromkeysvalues(self.algebra, keys=tuple(), values=list())
1✔
419
        keys, values = zip(*keysvalues)
1✔
420
        return self.fromkeysvalues(self.algebra, keys=keys, values=list(values))
1✔
421

422
    @cached_property
1✔
423
    def _callable(self):
1✔
424
        """ Return the callable function for this MV. """
425
        return _lambdify_mv(self)
1✔
426

427
    def __call__(self, *args, **kwargs):
1✔
428
        if args and kwargs:
1✔
429
            raise Exception('Please provide all input either as positional arguments or as keywords arguments, not both.')
×
430

431
        if not self.free_symbols:
1✔
432
            return self
1✔
433
        keys_out, func = self._callable
1✔
434
        if kwargs:
1✔
435
            args = [v for k, v in sorted(kwargs.items(), key=lambda x: x[0])]
1✔
436
        values = func(args)
1✔
437
        return self.fromkeysvalues(self.algebra, keys_out, values)
1✔
438

439
    def asmatrix(self):
1✔
440
        """ Returns a matrix representation of this multivector. """
441
        bin2index = {k: i for i, k in enumerate(self.algebra.canon2bin.values())}
1✔
442
        return sum(v * self.algebra.matrix_basis[bin2index[k]] for k, v in self.items())
1✔
443

444
    def asfullmv(self, canonical=True):
1✔
445
        """
446
        Returns a full version of the same multivector.
447

448
        :param canonical: If True (default) the values are in canonical order,
449
          even if the mutivector was already dense.
450
        """
451
        if canonical:
1✔
452
            keys = self.algebra.indices_for_grades(tuple(range(self.algebra.d + 1)))
1✔
453
        else:
454
            keys = tuple(range(len(self.algebra)))
1✔
455
        values = [getattr(self, self.algebra.bin2canon[k]) for k in keys]
1✔
456
        return self.fromkeysvalues(self.algebra, keys=keys, values=values)
1✔
457

458
    def gp(self, other):
1✔
459
        return self.algebra.gp(self, other)
1✔
460

461
    __mul__ = gp
1✔
462

463
    def __rmul__(self, other):
1✔
464
        return self.algebra.gp(other, self)
1✔
465

466
    def sw(self, other):
1✔
467
        """
468
        Apply :code:`x := self` to :code:`y := other` under conjugation:
469
        :code:`x.sw(y) = x*y*~x`.
470
        """
471
        return self.algebra.sw(self, other)
1✔
472

473
    __rshift__ = sw
1✔
474

475
    def __rrshift__(self, other):
1✔
476
        return self.algebra.sw(other, self)
1✔
477

478
    def proj(self, other):
1✔
479
        """
480
        Project :code:`x := self` onto :code:`y := other`: :code:`x @ y = (x | y) * ~y`.
481
        For correct behavior, :code:`x` and :code:`y` should be normalized (k-reflections).
482
        """
483
        return self.algebra.proj(self, other)
1✔
484

485
    __matmul__ = proj
1✔
486

487
    def __rmatmul__(self, other):
1✔
488
        return self.algebra.proj(other, self)
1✔
489

490
    def cp(self, other):
1✔
491
        """
492
        Calculate the commutator product of :code:`x := self` and :code:`y := other`:
493
        :code:`x.cp(y) = 0.5*(x*y-y*x)`.
494
        """
495
        return self.algebra.cp(self, other)
1✔
496

497
    def acp(self, other):
1✔
498
        """
499
        Calculate the anti-commutator product of :code:`x := self` and :code:`y := other`:
500
        :code:`x.cp(y) = 0.5*(x*y+y*x)`.
501
        """
502
        return self.algebra.acp(self, other)
1✔
503

504
    def ip(self, other):
1✔
505
        return self.algebra.ip(self, other)
1✔
506

507
    __or__ = ip
1✔
508

509
    def __ror__(self, other):
1✔
510
        return self.algebra.ip(other, self)
1✔
511

512
    def op(self, other):
1✔
513
        return self.algebra.op(self, other)
1✔
514

515
    __xor__ = __rxor__ = op
1✔
516

517
    def lc(self, other):
1✔
518
        return self.algebra.lc(self, other)
1✔
519

520
    def rc(self, other):
1✔
521
        return self.algebra.rc(self, other)
1✔
522

523
    def sp(self, other):
1✔
524
        r""" Scalar product: :math:`\langle x \cdot y \rangle`. """
525
        return self.algebra.sp(self, other)
1✔
526

527
    def rp(self, other):
1✔
528
        return self.algebra.rp(self, other)
1✔
529

530
    __and__ = rp
1✔
531

532
    def __rand__(self, other):
1✔
533
        return self.algebra.rp(other, self)
1✔
534

535
    def __pow__(self, power, modulo=None):
1✔
536
        # TODO: this should also be taken care of via codegen, but for now this workaround is ok.
537
        if power == 0:
1✔
538
            return self.algebra.scalar((1,))
×
539
        elif power < 0:
1✔
540
            res = x = self.inv()
1✔
541
            power *= -1
1✔
542
        else:
543
            res = x = self
1✔
544

545
        if power == 0.5:
1✔
546
            return res.sqrt()
1✔
547

548
        for i in range(1, power):
1✔
549
            res = res.gp(x)
1✔
550
        return res
1✔
551

552
    def outerexp(self):
1✔
553
        return self.algebra.outerexp(self)
1✔
554

555
    def outersin(self):
1✔
556
        return self.algebra.outersin(self)
1✔
557

558
    def outercos(self):
1✔
559
        return self.algebra.outercos(self)
1✔
560

561
    def outertan(self):
1✔
562
        return self.algebra.outertan(self)
1✔
563

564
    def exp(self, cosh=None, sinhc=None, sqrt=None):
1✔
565
        r"""
566
        Calculate the exponential of simple elements, meaning an element that squares to a scalar.
567
        Works for python float, int and complex dtypes, and for symbolic expressions using sympy.
568
        For more control, it is possible to explicitly provide a `cosh`, `sinhc`, and `sqrt` function.
569
        If you provide one, you must provide all.
570

571
        The argument to `sqrt` is the scalar :math:`s = \langle x^2 \rangle_0`, while the input to the
572
        `cosh` and `sinhc` functions is the output of the sqrt function applied to :math:`s`.
573

574
        For example, for a simple rotation `kingdon`'s implementation is equivalent to
575

576
        .. code-block ::
577

578
            alg = Algebra(2)
579
            x = alg.bivector(e12=1)
580
            x.exp(
581
                cosh=np.cos,
582
                sinhc=np.sinc,
583
                sqrt=lambda s: (-s)**0.5,
584
            )
585
        """
586
        ll = (self * self).filter()
1✔
587
        if ll.grades and ll.grades != (0,):
1✔
588
            raise NotImplementedError(
×
589
                'Currently only elements that square to a scalar (i.e. are simple) can be exponentiated.'
590
            )
591

592
        ll = ll.e
1✔
593
        if sqrt is None and cosh is None and sinhc is None:
1✔
594
            if isinstance(ll, Expr):
1✔
595
                sqrt = lambda x: (-x) ** 0.5
1✔
596
                cosh = cos
1✔
597
                sinhc = sinc
1✔
598
            elif isinstance(ll, (float, int)) and ll > 0:
1✔
599
                sqrt = lambda x: x ** 0.5
×
600
                import numpy as np
×
601
                cosh = np.cosh
×
602
                sinhc = lambda x: np.sinh(x) / x
×
603
            elif isinstance(ll, (float, int)) and ll == 0:
1✔
604
                sqrt = lambda x: x ** 0.5
1✔
605
                import numpy as np
1✔
606
                cosh = sinhc = lambda x: 1
1✔
607
            else:
608
                # Assume numpy
609
                sqrt = lambda x: (-x) ** 0.5
1✔
610
                import numpy as np
1✔
611
                cosh = np.cos
1✔
612
                sinhc = lambda x: np.sinc(x / np.pi)
1✔
613

614
        l = sqrt(ll)
1✔
615
        return self * sinhc(l) + cosh(l)
1✔
616

617
    def polarity(self):
1✔
618
        return self.algebra.polarity(self)
1✔
619

620
    def unpolarity(self):
1✔
621
        return self.algebra.unpolarity(self)
1✔
622

623
    def hodge(self):
1✔
624
        return self.algebra.hodge(self)
1✔
625

626
    def unhodge(self):
1✔
627
        return self.algebra.unhodge(self)
1✔
628

629
    def dual(self, kind='auto'):
1✔
630
        """
631
        Compute the dual of `self`. There are three different kinds of duality in common usage.
632
        The first is polarity, which is simply multiplying by the inverse PSS from the right. This is the only game in
633
        town for non-degenerate metrics (Algebra.r = 0). However, for degenerate spaces this no longer works, and we
634
        have two popular options: Poincaré and Hodge duality.
635

636
        By default, :code:`kingdon` will use polarity in non-degenerate spaces, and Hodge duality for spaces with
637
        `Algebra.r = 1`. For spaces with `r > 2`, little to no literature exists, and you are on your own.
638

639
        :param kind: if 'auto' (default), :code:`kingdon` will try to determine the best dual on the
640
            basis of the signature of the space. See explenation above.
641
            To ensure polarity, use :code:`kind='polarity'`, and to ensure Hodge duality,
642
            use :code:`kind='hodge'`.
643
        """
644
        if kind == 'polarity' or kind == 'auto' and self.algebra.r == 0:
1✔
645
            return self.polarity()
1✔
646
        elif kind == 'hodge' or kind == 'auto' and self.algebra.r == 1:
1✔
647
            return self.hodge()
1✔
648
        elif kind == 'auto':
1✔
649
            raise Exception('Cannot select a suitable dual in auto mode for this algebra.')
×
650
        else:
651
            raise ValueError(f'No dual found for kind={kind}.')
1✔
652

653
    def undual(self, kind='auto'):
1✔
654
        """
655
        Compute the undual of `self`. See :class:`~kingdon.multivector.MultiVector.dual` for more information.
656
        """
657
        if kind == 'polarity' or kind == 'auto' and self.algebra.r == 0:
1✔
658
            return self.unpolarity()
×
659
        elif kind == 'hodge' or kind == 'auto' and self.algebra.r == 1:
1✔
660
            return self.unhodge()
1✔
661
        elif kind == 'auto':
×
662
            raise Exception('Cannot select a suitable undual in auto mode for this algebra.')
×
663
        else:
664
            raise ValueError(f'No undual found for kind={kind}.')
×
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