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

tBuLi / kingdon / 3714168179

pending completion
3714168179

Pull #17

github

GitHub
Merge 4a14ec2b7 into 64fbd1124
Pull Request #17: Operator dict improvements

122 of 122 new or added lines in 4 files covered. (100.0%)

591 of 653 relevant lines covered (90.51%)

0.91 hits per line

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

91.5
/kingdon/multivector.py
1
import operator
1✔
2
from collections.abc import Mapping
1✔
3
from dataclasses import dataclass, field
1✔
4
from functools import reduce, cached_property
1✔
5

6
from sympy import Symbol, Expr, sympify
1✔
7

8
from kingdon.codegen import _lambdify_mv
1✔
9

10

11
@dataclass(init=False)
1✔
12
class MultiVector:
1✔
13
    algebra: "Algebra"
1✔
14
    _values: tuple = field(default_factory=tuple)
1✔
15
    _keys: tuple = field(default_factory=tuple)
1✔
16

17
    def __new__(cls, algebra, values=None, keys=None, *, name=None, grades=None, symbolcls=Symbol):
1✔
18
        """
19
        :param algebra: Instance of :class:`~kingdon.algebra.Algebra`.
20
        :param keys: Keys corresponding to the basis blades in binary rep.
21
        :param values: Values of the multivector. If keys are provided, then keys and values should
22
            satisfy :code:`len(keys) == len(values)`. If no keys nor grades are provided, :code:`len(values)`
23
            should equal :code:`len(algebra)`, i.e. a full multivector. If grades is provided,
24
            then :code:`len(values)` should be identical to the number of values in a multivector
25
            of that grade.
26
        :param name: Base string to be used as the name for symbolic values.
27
        :param grades: Optional, :class:`tuple` of grades in this multivector.
28
            If present, :code:`keys` is checked against these grades.
29
        :param symbolcls: Optional, class to be used for symbol creation. This is a :class:`sympy.Symbol` by default,
30
            but could be e.g. :class:`symfit.Variable` or :class:`symfit.Parameter` when the goal is to use this
31
            multivector in a fitting problem.
32
        """
33
        # Sanitize input
34
        values = values if values is not None else tuple()
1✔
35
        name = name if name is not None else ''
1✔
36
        if grades is not None:
1✔
37
            if not all(0 <= grade <= algebra.d for grade in grades):
1✔
38
                raise ValueError(f'Each grade in `grades` needs to be a value between 0 and {algebra.d}.')
1✔
39
        else:
40
            grades = tuple(range(algebra.d + 1))
1✔
41

42
        # Construct a new MV on the basis of the kind of input we received.
43
        if isinstance(values, Mapping):
1✔
44
            keys, values = zip(*values.items()) if values else (tuple(), tuple())
1✔
45
        elif len(values) == len(algebra) and not keys:
1✔
46
            keys = tuple(range(len(values)))
1✔
47
        elif len(values) == len(algebra.indices_for_grades[grades]) and not keys:
1✔
48
            keys = algebra.indices_for_grades[grades]
1✔
49
        elif name and not values:
1✔
50
            # values was not given, but we do have a name. So we are in symbolic mode.
51
            keys = algebra.indices_for_grades[grades] if not keys else keys
1✔
52
            values = tuple(symbolcls(f'{name}{algebra.bin2canon[k][1:]}') for k in keys)
1✔
53
        elif len(keys) != len(values):
1✔
54
            raise TypeError(f'Length of `keys` and `values` have to match.')
1✔
55

56
        if not all(isinstance(k, int) for k in keys):
1✔
57
            keys = tuple(key if key in algebra.bin2canon else algebra.canon2bin[key]
1✔
58
                         for key in keys)
59
        if any(isinstance(v, str) for v in values):
1✔
60
            values = tuple(val if not isinstance(val, str) else sympify(val)
1✔
61
                           for val in values)
62

63
        if not set(keys) <= set(algebra.indices_for_grades[grades]):
1✔
64
            raise ValueError(f"All keys should be of grades {grades}.")
1✔
65

66
        return cls.fromkeysvalues(algebra, keys, values)
1✔
67

68
    @classmethod
1✔
69
    def fromkeysvalues(cls, algebra, keys, values):
1✔
70
        """
71
        Initiate a multivector from a sequence of keys and a sequence of values.
72
        """
73
        obj = object.__new__(cls)
1✔
74
        obj.algebra = algebra
1✔
75
        obj._values = values
1✔
76
        obj._keys = keys
1✔
77
        return obj
1✔
78

79
    @classmethod
1✔
80
    def frommatrix(cls, algebra, matrix):
1✔
81
        """
82
        Initiate a multivector from a matrix. This matrix is assumed to be
83
        generated by :class:`~kingdon.multivector.MultiVector.asmatrix`, and
84
        thus we only read the first column of the input matrix.
85
        """
86
        obj = cls(algebra=algebra, values=matrix[:, 0])
1✔
87
        return obj
1✔
88

89
    def keys(self):
1✔
90
        return self._keys
1✔
91

92
    def values(self):
1✔
93
        return self._values
1✔
94

95
    def items(self):
1✔
96
        return zip(self._keys, self._values)
1✔
97

98
    def __len__(self):
1✔
99
        return len(self._values)
1✔
100

101
    @cached_property
1✔
102
    def grades(self):
1✔
103
        """ Tuple of the grades present in `self`. """
104
        return tuple(sorted({bin(ind).count('1') for ind in self.keys()}))
1✔
105

106
    def grade(self, grades):
1✔
107
        """
108
        Returns a new  :class:`~kingdon.multivector.MultiVector` instance with
109
        only the selected `grades` from `self`.
110

111
        :param grades: tuple or int, grades to select.
112
        """
113
        if isinstance(grades, int):
1✔
114
            grades = (grades,)
×
115
        elif not isinstance(grades, tuple):
1✔
116
            grades = tuple(grades)
×
117

118
        vals = {k: self[k]
1✔
119
                for k in self.algebra.indices_for_grades[grades] if k in self.keys()}
120
        return self.fromkeysvalues(self.algebra, tuple(vals.keys()), tuple(vals.values()))
1✔
121

122
    @cached_property
1✔
123
    def issymbolic(self):
1✔
124
        """ True if this mv contains Symbols, False otherwise. """
125
        return any(isinstance(v, Expr) for v in self.values())
1✔
126

127
    def __neg__(self):
1✔
128
        try:
1✔
129
            values = - self.values()
1✔
130
        except TypeError:
1✔
131
            values = tuple(-v for v in self.values())
1✔
132
        return self.fromkeysvalues(self.algebra, self.keys(), values)
1✔
133

134
    def __invert__(self):  # reversion
1✔
135
        values = tuple((-1)**(bin(k).count("1") // 2) * v for k, v in self.items())
1✔
136
        return self.fromkeysvalues(self.algebra, self.keys(), values)
1✔
137

138
    def normsq(self):
1✔
139
        return self.algebra.normsq(self)
1✔
140

141
    def normalized(self):
1✔
142
        """ Normalized version of this multivector. """
143
        normsq = self.normsq()
1✔
144
        if normsq.grades == (0,):
1✔
145
            return self / normsq[0] ** 0.5
1✔
146
        else:
147
            raise NotImplementedError
1✔
148

149
    def inv(self):
1✔
150
        """ Inverse of this multivector. """
151
        return self.algebra.inv(self)
1✔
152

153
    def __add__(self, other):
1✔
154
        if not isinstance(other, MultiVector):
1✔
155
            other = self.fromkeysvalues(self.algebra, (0,), (other,))
1✔
156
        vals = dict(self.items())
1✔
157
        for k, v in other.items():
1✔
158
            if k in vals:
1✔
159
                vals[k] += v
1✔
160
            else:
161
                vals[k] = v
1✔
162
        return self.fromkeysvalues(self.algebra, tuple(vals.keys()), tuple(vals.values()))
1✔
163

164
    __radd__ = __add__
1✔
165

166
    def __sub__(self, other):
1✔
167
        return self + (-other)
1✔
168

169
    def __rsub__(self, other):
1✔
170
        return other + (-self)
×
171

172
    def __truediv__(self, other):
1✔
173
        return self.algebra.div(self, other)
1✔
174

175
    def __str__(self):
1✔
176
        if len(self.values()):
1✔
177
            canon_sorted_vals = sorted(self.items(), key=lambda x: (len(self.algebra.bin2canon[x[0]]), self.algebra.bin2canon[x[0]]))
1✔
178
            return ' + '.join([f'({val}) * {self.algebra.bin2canon[key]}' for key, val in canon_sorted_vals])
1✔
179
        else:
180
            return '0'
×
181

182
    def __format__(self, format_spec):
1✔
183
        if format_spec == 'keys_binary':
×
184
            iden = '_'.join(''.join('1' if i in self.keys() else '0' for i in bin_blades)
×
185
                            for bin_blades in self.algebra.indices_for_grade.values())
186
            return iden
×
187

188
    def __getitem__(self, item):
1✔
189
        if isinstance(item, tuple):
1✔
190
            key, slicing = item
1✔
191
        else:
192
            key, slicing = item, None
1✔
193

194
        if key == slice(None, None, None):
1✔
195
            values = self.values()
1✔
196
        else:
197
            # Convert key from a basis-blade in binary rep to a valid index in values.
198
            key = key if key in self.algebra.bin2canon else self.algebra.canon2bin[key]
1✔
199
            try:
1✔
200
                key = self.keys().index(key)
1✔
201
            except ValueError:
1✔
202
                return 0
1✔
203
            else:
204
                values = self.values()
1✔
205

206
        if slicing is None:
1✔
207
            return values[key]
1✔
208
        else:
209
            return values[key][slicing] if isinstance(values, (tuple, list)) else values[key, slicing]
1✔
210

211
    def __contains__(self, item):
1✔
212
        item = item if item in self.algebra.bin2canon else self.algebra.canon2bin[item]
1✔
213
        return item in self._keys
1✔
214

215
    @cached_property
1✔
216
    def free_symbols(self):
1✔
217
        return reduce(operator.or_, (v.free_symbols for v in self.values() if hasattr(v, "free_symbols")))
1✔
218

219
    @cached_property
1✔
220
    def _callable(self):
1✔
221
        """ Return the callable function for this MV. """
222
        return _lambdify_mv(sorted(self.free_symbols, key=lambda x: x.name), self)
1✔
223

224
    def __call__(self, *args, **kwargs):
1✔
225
        if not self.free_symbols:
1✔
226
            return self
×
227
        keys_out, func = self._callable
1✔
228
        values = func(*args, **kwargs)
1✔
229
        return self.fromkeysvalues(self.algebra, keys_out, values)
1✔
230

231
    def asmatrix(self):
1✔
232
        """ Returns a matrix representation of this multivector. """
233
        return sum(v * self.algebra.matrix_basis[k] for k, v in self.items())
1✔
234

235
    def gp(self, other):
1✔
236
        return self.algebra.gp(self, other)
1✔
237

238
    __mul__ = __rmul__ = gp
1✔
239

240
    def conj(self, other):
1✔
241
        """ Apply `x := self` to `y := other` under conjugation: `x*y*~x`. """
242
        return self.algebra.conj(self, other)
1✔
243

244
    def proj(self, other):
1✔
245
        """
246
        Project :code:`x := self` onto :code:`y := other`: :code:`x @ y = (x | y) * ~y`.
247
        For correct behavior, :code:`x` and :code:`y` should be normalized (k-reflections).
248
        """
249
        return self.algebra.proj(self, other)
1✔
250

251
    __matmul__ = proj
1✔
252

253
    def cp(self, other):
1✔
254
        """
255
        Calculate the commutator product of :code:`x := self` and :code:`y := other`:
256
        :code:`x.cp(y) = 0.5*(x*y-y*x)`.
257
        """
258
        return self.algebra.cp(self, other)
1✔
259

260
    def acp(self, other):
1✔
261
        """
262
        Calculate the anti-commutator product of :code:`x := self` and :code:`y := other`:
263
        :code:`x.cp(y) = 0.5*(x*y+y*x)`.
264
        """
265
        return self.algebra.acp(self, other)
1✔
266

267
    def ip(self, other):
1✔
268
        return self.algebra.ip(self, other)
1✔
269

270
    __or__ = ip
1✔
271

272
    def op(self, other):
1✔
273
        return self.algebra.op(self, other)
1✔
274

275
    __xor__ = __rxor__ = op
1✔
276

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

280
    __lshift__ = lc
1✔
281

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

285
    __rshift__ = rc
1✔
286

287
    def sp(self, other):
1✔
288
        """ Scalar product: :math:`\langle x \cdot y \rangle`. """
289
        return self.algebra.sp(self, other)
1✔
290

291
    def rp(self, other):
1✔
292
        return self.algebra.rp(self, other)
1✔
293

294
    __and__ = rp
1✔
295

296
    def __pow__(self, power, modulo=None):
1✔
297
        # TODO: this should also be taken care of via codegen, but for now this workaround is ok.
298
        if power == 2:
×
299
            return self.algebra.gp(self, self)
×
300
        else:
301
            raise NotImplementedError
×
302

303
    def outerexp(self):
1✔
304
        return self.algebra.outerexp(self)
1✔
305

306
    def outersin(self):
1✔
307
        return self.algebra.outersin(self)
1✔
308

309
    def outercos(self):
1✔
310
        return self.algebra.outercos(self)
1✔
311

312
    def outertan(self):
1✔
313
        return self.algebra.outertan(self)
×
314

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

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

325
        :param kind: if 'auto' (default), :code:`kingdon` will try to determine the best dual on the
326
            basis of the signature of the space. See explenation above.
327
            To ensure polarity, use :code:`kind='polarity'`, and to ensure Hodge duality,
328
            use :code:`kind='hodge'`.
329
        """
330
        if kind == 'polarity' or kind == 'auto' and self.algebra.r == 0:
1✔
331
            return self / self.algebra.pss
1✔
332
        elif kind == 'hodge' or kind == 'auto' and self.algebra.r == 1:
1✔
333
            return self.algebra.multivector(
1✔
334
                {len(self.algebra) - 1 - eI: self.algebra.signs[eI, len(self.algebra) - 1 - eI] * val
335
                 for eI, val in self.items()}
336
            )
337
        elif kind == 'auto':
1✔
338
            raise Exception('Cannot select a suitable dual in auto mode for this algebra.')
×
339
        else:
340
            raise ValueError(f'No dual found for kind={kind}.')
1✔
341

342
    def undual(self, kind='auto'):
1✔
343
        """
344
        Compute the undual of `self`. See :class:`~kingdon.multivector.MultiVector.dual` for more information.
345
        """
346
        if kind == 'polarity' or kind == 'auto' and self.algebra.r == 0:
1✔
347
            return self * self.algebra.pss
×
348
        elif kind == 'hodge' or kind == 'auto' and self.algebra.r == 1:
1✔
349
            return self.algebra.multivector(
1✔
350
                {len(self.algebra) - 1 - eI: self.algebra.signs[len(self.algebra) - 1 - eI, eI] * val
351
                 for eI, val in self.items()}
352
            )
353
        elif kind == 'auto':
×
354
            raise Exception('Cannot select a suitable undual in auto mode for this algebra.')
×
355
        else:
356
            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

© 2025 Coveralls, Inc