Skip to content

sumofsqrts

__all__ = ('SumOfSqrts',) module-attribute

SumOfSqrts dataclass

Element of the quadratic rationals \(\mathbb{K}\left(\sqrt{2}, \sqrt{3}, \dots\right)\).

An instance represents an exact algebraic rational of the form

\[ \sum_iv_i\sqrt{k_i} = v_1+v_2\sqrt{2}+v_3+\sqrt{3}+\cdots \qquad v_i\in\mathbb{K} \]

where currently \(\mathbb{K}\) is \(\mathbb{Z}\) (int) or \(\mathbb{Q}\) (fractions.Fraction).

The immutable class supports exact conversion, ordering, algebraic conjugation, norm computation and arithmetic.

Addition, subtraction & multiplication is closed, mixed coefficients are promoted. Division is promoted to rationals.

Parameters:

  • n (dict[int, int | Fraction] or int or Fraction, default: 0 ) –

    Mapping of radicands \(k_i\) to factors \(v_i\).

References
Source code in radicalfield\sumofsqrts.py
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
@total_ordering
@dataclass(eq=False, frozen=True, slots=True) #make slots, immutability & repr
class SumOfSqrts:
    r"""Element of the quadratic rationals $\mathbb{K}\left(\sqrt{2}, \sqrt{3}, \dots\right)$.

    An instance represents an exact algebraic rational of the form

    $$
        \sum_iv_i\sqrt{k_i} = v_1+v_2\sqrt{2}+v_3+\sqrt{3}+\cdots \qquad v_i\in\mathbb{K}
    $$

    where currently $\mathbb{K}$ is $\mathbb{Z}$ (`int`)
    or $\mathbb{Q}$ (`fractions.Fraction`).

    The immutable class supports exact conversion, ordering,
    algebraic conjugation, norm computation and arithmetic.

    Addition, subtraction & multiplication is closed,
    mixed coefficients are promoted.
    Division is promoted to rationals.

    Parameters
    ----------
    n : dict[int,int|Fraction] or int or Fraction, default 0
        Mapping of radicands $k_i$ to factors $v_i$.

    References
    ----------
    - [Wikipedia - Quadratic integers](https://en.wikipedia.org/wiki/Quadratic_integer)
    - [Wikipedia - Sum of radicals](https://en.wikipedia.org/wiki/Sum_of_radicals)
    """
    n:Final[dict[int,int|Fraction]] = field(default_factory=dict)



    @staticmethod
    def from_expr(e:sp.Expr) -> 'SumOfSqrts':
        r"""Construct a `SumOfSqrts` from a `sympy.Expr`.

        Parameters
        ----------
        e
            Expression to convert.

        Returns
        -------
        SumOfSqrts
            Expression as `SumOfSqrts`.

        Raises
        ------
        ValueError
            If the expression could not be converted.
        """
        if not isinstance(e, sp.Expr):
            raise TypeError('e must be a sympy.Expr')

        def rat_to_int_or_frac(r:sp.Integer|sp.Rational) -> int|Fraction:
            if isinstance(r, sp.Integer):
                return int(r)
            elif isinstance(r, sp.Rational):
                return Fraction(int(r.p), int(r.q))
            else:
                raise ValueError(f'not in ℚ: {r}')

        n:defaultdict[int,int|Fraction] = defaultdict(int)
        for term in e.as_ordered_terms():
            v, radical = term.as_coeff_Mul()
            radicand:sp.Integer = radical**2
            n[int(radicand)] += rat_to_int_or_frac(v)

        return SumOfSqrts(n)

    @staticmethod
    def sqrtOf(n:int|Fraction) -> 'SumOfSqrts':
        if isinstance(n, int):
            return SumOfSqrts({n:1})
        else:
            return SumOfSqrts({n.numerator*n.denominator:Fraction(1, n.denominator)})

    @staticmethod
    def _create_directly(n:dict[int,int|Fraction]) -> 'SumOfSqrts':
        #avoid normalisation
        s = object.__new__(SumOfSqrts)
        object.__setattr__(s, 'n', MappingProxyType(n))
        return s

    @staticmethod
    def normalize(d:dict[int,int|Fraction]) -> dict[int,int|Fraction]:
        r"""Normalise a dict of radicals and rational factors into squarefree form.

        $$
            \sum_iv_i\sqrt{k_i}
        $$

        Explicitly, each term is transformed as

        $$
            v\sqrt{k} = v\sqrt{s^2r} = vs\sqrt{r} \qquad \text{where $r$ is squarefree}
        $$

        and for the whole sum:

        - zero terms are filtered out,
        - type and value checked,
        - square components in the radicands are pulled out &
        - the result is sorted by increasing radicand.

        TODO: inplace version?

        Parameters
        ----------
        d : dict[int,int|Fraction]
            Mapping of radicands $k_i$ to factors $v_i$.

        Returns
        -------
        dict[int,int|Fraction]
            Mathematically equivalent but cleaner copy.

        Raises
        ------
        TypeError
            If a radicand is not an integer or a coefficient is not an
            integer or fraction.
        ValueError
            If a radicand is negative.
        """
        n:defaultdict[int,int|Fraction] = defaultdict(int)
        for k, v in d.items():
            if k and v: #filter v_0\sqrt{0} and 0\sqrt{k}
                if not (isinstance(k, int) and isinstance(v, (int, Fraction))):
                    raise TypeError('radicands must be integers, factors must be integers or fractions')
                if not k>=0:
                    raise ValueError('radicands must be non-negative')
                f:dict[int,int] = factorint(k)
                s:int = prod(p**(e//2) for p, e in f.items())
                k:int = prod(p**(e%2) for p, e in f.items())
                n[k] += s * v
                if not n[k]:
                    del n[k]
        return dict(sorted(n.items()))

    def __post_init__(self) -> None:
        if isinstance(self.n, (int, Fraction)):
            n:dict[int,int|Fraction] = {1:self.n} if self.n else {}
        elif isinstance(self.n, dict):
            n:dict[int,int|Fraction] = SumOfSqrts.normalize(self.n)
        else:
            raise TypeError('argument must be dict, int or fraction')

        object.__setattr__(self, 'n', MappingProxyType(n))



    #container
    def __len__(self) -> int:
        """Return the number of terms.

        Returns
        -------
        int
            Number of terms.
        """
        return len(self.n)

    #clashes with numpy.array([x]) because it trys to unfold SumOfSqrts x forever
    #@overload
    #def __getitem__(self, key:int) -> int|Fraction: ...
    #def __getitem__(self, key:Any) -> int|Fraction:
    #    """Return the factor of the given radicand.
    #    
    #    Values not set default to zero.
    #    
    #    Parameters
    #    ----------
    #    key : int
    #        Radicand.
    #    
    #    Returns
    #    -------
    #    int or Fraction
    #        Factor of the given radicand.
    #    """
    #    return self.n.get(key, 0)

    def get(self, key:int) -> int|Fraction:
        return self.n.get(key, 0)

    def keys(self) -> KeysView[int]:
        return self.n.keys()

    def values(self) -> ValuesView[int|Fraction]:
        return self.n.values()

    def items(self) -> ItemsView[int,int|Fraction]:
        return self.n.items()



    #conversion
    def __bool__(self) -> bool:
        """Return whether this element is unequal zero.

        Returns
        -------
        bool
            Whether this element is unequal zero.
        """
        return bool(self.n)

    def is_rational(self) -> bool:
        r"""Return whether this element has no radical component.

        Notes
        -----
        Not a property to be consistent with `fractions.Fraction.is_integer()`.

        Returns
        -------
        bool
            Whether this element has no radical component.
        """
        return set(self.keys()) <= {1}

    def as_fraction(self) -> Fraction:
        """Return this element as a fraction.

        Returns
        -------
        Fraction
            This element as a fraction.

        Raises
        ------
        ValueError
            If this element is not a fraction.
        """
        if not self.is_rational():
            raise ValueError('not a fraction')
        return Fraction(self.get(1))

    def is_integer(self) -> bool:
        """Return whether this element is an integer.

        Notes
        -----
        Not a property to be consistent with `fractions.Fraction.is_integer()`.

        Returns
        -------
        bool
            Whether this element is an integer.
        """
        return set(self.keys())<={1} and isinstance(self.get(1), int)

    def __int__(self) -> int:
        """Return this element as an integer.

        Returns
        -------
        int
            This element as an integer.

        Raises
        ------
        ValueError
            If this element is not an integer.
        """
        if not self.is_integer():
            raise ValueError('not an integer')
        return self.get(1)

    def __float__(self) -> float:
        """Return this element as a float.

        Returns
        -------
        float
            This element as a float.
        """
        #explicitly float as sumprod([], []) == int(0)
        return float(sumprod(self.values(), map(sqrt, self.keys())))

    def _sympy_(self) -> sp.Expr:
        return sum((v*sp.sqrt(k) for k, v in self.items()), sp.Integer(0))



    #ordering
    @overload
    def __eq__(self, other:Self) -> bool: ...
    @overload
    def __eq__(self, other:int) -> bool: ...
    @overload
    def __eq__(self, other:Fraction) -> bool: ...
    def __eq__(self, other:Any) -> bool|NotImplementedType:
        if isinstance(other, SumOfSqrts):
            return self.n == other.n
        elif isinstance(other, (int, Fraction)):
            return self.n == ({1:other} if other else {})
        return NotImplemented

    @overload
    def __lt__(self, other:Self) -> bool: ...
    @overload
    def __lt__(self, other:int) -> bool: ...
    @overload
    def __lt__(self, other:Fraction) -> bool: ...
    def __lt__(self, other:Any) -> bool|NotImplementedType:
        """Return whether this element is less than the other.

        Notes
        -----
        Repeatedly finds the largest prime factors in the radicands,
        separates these terms onto one side and squares
        until a rational inequality is left.
        Extremely slow and terrrible complexity.

        Parameters
        ----------
        other: SumOfSqrts or int or Fraction
            Operand to compare to.

        Returns
        -------
        bool
            Whether this element is less than the other.

        References
        ----------
        - [StackExchange - Determine sign of sum of square roots](https://math.stackexchange.com/a/1076510)
        - [Wikipedia - Square-root sum problem](https://en.wikipedia.org/wiki/Square-root_sum_problem)
        """
        if isinstance(other, (SumOfSqrts, int, Fraction)):
            l:SumOfSqrts = self - other

            while not l.is_rational():
                p:int = max(chain.from_iterable(map(primefactors, l.keys())))

                r:SumOfSqrts = SumOfSqrts._create_directly({k//p:-v for k, v in l.items() if k%p==0})
                l:SumOfSqrts = SumOfSqrts._create_directly({k:v for k, v in l.items() if k%p!=0})
                #https://math.stackexchange.com/a/2347212
                l, r = l*abs(l), r*abs(r)*p

                l -= r

            return l.as_fraction() < 0
        return NotImplemented

    def __abs__(self):
        return +self if self>=0 else -self


    #arithmetic
    #make all following methods non-recursive/leaves,
    #except inversion as it is otherwise to complicated
    def norm(self) -> int|Fraction:
        r"""Return the algebraic norm.

        $$
            \begin{aligned}
                N\left(\sum_iv_i\sqrt{k_i}\right)
                = &\left(+v_1+v_2\sqrt{2}+v_3\sqrt{3}+\cdots,\right) \\
                &\left(+v_1-v_2\sqrt{2}+v_3\sqrt{3}+\cdots,\right) \\
                &\left(+v_1+v_2\sqrt{2}-v_3\sqrt{3}+\cdots,\right) \\
                &\left(+v_1-v_2\sqrt{2}-v_3\sqrt{3}+\cdots, \dots\right) \\
                &\cdots
            \end{aligned}
        $$

        Product of all conjugates.

        Returns
        -------
        int or Fraction
            The algebraic norm.

        References
        ----------
        [Wikipedia - Quadratic integers - Norm and conjugation](https://en.wikipedia.org/wiki/Quadratic_integer#Norm_and_conjugation)
        """
        return prod(islice(self.conjugate(), 2**len(self))).get(1)

    def conjugate(self) -> Generator[Self]:
        r"""Yield the algebraic conjugations.

        $$
            \begin{aligned}
                &\left(+v_1+v_2\sqrt{2}+v_3\sqrt{3}+\cdots,\right. \\
                &\left.+v_1-v_2\sqrt{2}+v_3\sqrt{3}+\cdots,\right. \\
                &\left.-v_1+v_2\sqrt{2}+v_3\sqrt{3}+\cdots,\right. \\
                &\left.-v_1-v_2\sqrt{2}+v_3\sqrt{3}+\cdots, \dots\right)
            \end{aligned}
        $$

        The algebraic conjugates are all sign flip permutations.

        Yields
        -------
        SumOfSqrts
            The algebraic conjugations.

        References
        ----------
        [Wikipedia - Quadratic integers - Norm and conjugation](https://en.wikipedia.org/wiki/Quadratic_integer#Norm_and_conjugation)
        """
        for s in product((True, False), repeat=len(self)):
            yield SumOfSqrts._create_directly({k:(+v if s else -v) for k, v, s in zip(self.keys(), self.values(), s)})

    def __pos__(self) -> Self:
        r"""Return itself.

        $$
            +\sum_iv_i\sqrt{k_i} = \sum_i+v_i\sqrt{k_i}
        $$

        Returns
        -------
        SumOfSqrts
            Itself.
        """
        return SumOfSqrts._create_directly({k:+v for k, v in self.items()})

    def __neg__(self) -> Self:
        r"""Return the negation.

        $$
            -\sum_iv_i\sqrt{k_i} = \sum_i-v_i\sqrt{k_i}
        $$

        Returns
        -------
        SumOfSqrts
            The negation.
        """
        return SumOfSqrts._create_directly({k:-v for k, v in self.items()})


    @overload
    def __add__(self, other:Self) -> Self: ...
    @overload
    def __add__(self, other:int) -> Self: ...
    @overload
    def __add__(self, other:Fraction) -> Self: ...
    def __add__(self, other:Any) -> Self|NotImplementedType:
        r"""Return the sum.

        $$
            \sum_iv_i\sqrt{k_i} + \sum_iw_i\sqrt{k_i}
            = \sum_i(v_i+w_i)\sqrt{k_i}
        $$

        Parameters
        ----------
        other: SumOfSqrts or int or Fraction
            Other summand.

        Returns
        -------
        SumOfSqrts
            The sum.
        """
        if isinstance(other, (int, Fraction)):
            other:SumOfSqrts = SumOfSqrts(other)
        if isinstance(other, SumOfSqrts):
            n:defaultdict[int,int|Fraction] = defaultdict(int, self.n)
            for k, v in other.items():
                n[k] += v
                if not n[k]:
                    del n[k]
            return SumOfSqrts._create_directly(n)
        return NotImplemented

    @overload
    def __radd__(self, other:int) -> Self: ...
    @overload
    def __radd__(self, other:Fraction) -> Self: ...
    def __radd__(self, other:Any) -> Self|NotImplementedType:
        return self + other #__add__

    @overload
    def __sub__(self, other:Self) -> Self: ...
    @overload
    def __sub__(self, other:int) -> Self: ...
    @overload
    def __sub__(self, other:Fraction) -> Self: ...
    def __sub__(self, other:Any) -> Self|NotImplementedType:
        r"""Return the difference.

        $$
            \sum_iv_i\sqrt{k_i} - \sum_iw_i\sqrt{k_i}
            = \sum_i(v_i-w_i)\sqrt{k_i}
        $$

        Parameters
        ----------
        other: SumOfSqrts or int or Fraction
            The subtrahend.

        Returns
        -------
        SumOfSqrts
            The difference.
        """
        if isinstance(other, (int, Fraction)):
            other:SumOfSqrts = SumOfSqrts(other)
        if isinstance(other, SumOfSqrts):
            n:defaultdict[int,int|Fraction] = defaultdict(int, self.n)
            for k, v in other.items():
                n[k] -= v
                if not n[k]:
                    del n[k]
            return SumOfSqrts._create_directly(n)
        return NotImplemented

    @overload
    def __rsub__(self, other:int) -> Self: ...
    @overload
    def __rsub__(self, other:Fraction) -> Self: ...
    def __rsub__(self, other:Any) -> Self|NotImplementedType:
        return (-self) + other #__add__


    @overload
    def __mul__(self, other:Self) -> Self: ...
    @overload
    def __mul__(self, other:int) -> Self: ...
    @overload
    def __mul__(self, other:Fraction) -> Self: ...
    def __mul__(self, other:Any) -> Self|NotImplementedType:
        r"""Return the product.

        $$
            \sum_iv_i\sqrt{k_i}\sum_jw_j\sqrt{l_j}
            = \sum_{ij}v_iw_i\sqrt{k_il_j}
        $$

        Parameters
        ----------
        other: SumOfSqrts or int or Fraction
            The other factor.

        Returns
        -------
        SumOfSqrts
            The product.
        """
        if isinstance(other, SumOfSqrts):
            n:defaultdict[int,int|Fraction] = defaultdict(int)
            for ki, vi in self.items():
                for kj, vj in other.items():
                    n[ki*kj] += vi * vj
            return SumOfSqrts(n)
        elif isinstance(other, (int, Fraction)):
            n:dict[int,int|Fraction] = {k:v*other for k, v in self.items()} if other else {}
            return SumOfSqrts._create_directly(n)
        return NotImplemented

    @overload
    def __rmul__(self, other:int) -> Self: ...
    @overload
    def __rmul__(self, other:Fraction) -> Self: ...
    def __rmul__(self, other:Any) -> Self|NotImplementedType:
        return self * other #__mul__


    def inv(self) -> Self:
        r"""Return the multiplicative inverse.

        Notes
        -----
        Starts with $\frac{1}{x}$ and then repeatedly multiplies
        $\frac{\overline{x}^i}{\overline{x}^i}$
        where $\overline{x}^i$ denotes the $i$-th conjugate
        ($i$-th permutation of signs flipped). For half of all conjugates
        (first sign doesn't have to be flipped)
        because then has the denominator become rational.

        Returns
        -------
        SumOfSqrts
            The multiplicative inverse element.

        Raises
        ------
        ZeroDivisionError
            If the norm is zero.

        See also
        --------
        [`SumOfSqrts.norm`][radicalfield.sumofsqrts.SumOfSqrts.norm]
        """
        if not self:
            raise ZeroDivisionError('division by zero')
        n:SumOfSqrts = SumOfSqrts(1)
        d:SumOfSqrts = self
        for f in islice(self.conjugate(), 1, 2**len(self)):
            n *= f
            d *= f
        return n / d.as_fraction()

    @overload
    def __truediv__(self, other:Self) -> Self: ...
    @overload
    def __truediv__(self, other:int) -> Self: ...
    @overload
    def __truediv__(self, other:Fraction) -> Self: ...
    def __truediv__(self, other:Any) -> Self|NotImplementedType:
        r"""Return the quotient.

        $$
            \frac{\left(a+b\sqrt{2}\right)}{\left(c+d\sqrt{2}\right)}
            = \frac{\left(a+b\sqrt{2}\right)\left(c-d\sqrt{2}\right)}{\left(c+d\sqrt{2}\right)\left(c-d\sqrt{2}\right)}
            = \frac{\left(ac-2bd\right)+\left(bc-ad\right)\sqrt{2}}{c^2-2d^2}
        $$

        More often than necessary promoted to rationals.

        Parameters
        ----------
        other: SumOfSqrts or int or Fraction
            The denominator.

        Returns
        -------
        SumOfSqrts
            The quotient.

        Raises
        ------
        ZeroDivisionError
            If the norm of the denominator is zero.
        """
        if isinstance(other, SumOfSqrts):
            return self * other.inv()
        elif isinstance(other, (int, Fraction)):
            other:Fraction = Fraction(other)
            return SumOfSqrts._create_directly({k:v/other for k, v in self.items()})
        return NotImplemented

    @overload
    def __rtruediv__(self, other:int) -> Self: ...
    @overload
    def __rtruediv__(self, other:Fraction) -> Self: ...
    def __rtruediv__(self, other:Any) -> Self|NotImplementedType:
        if isinstance(other, (int, Fraction)):
            return other * self.inv()
        return NotImplemented



    #IO
    def __repr__(self) -> str:
        n:tuple[str,...] = tuple(f'{v:+}{chr(0x221A)}{k}' for k, v in self.items())
        return ''.join(n) if n else '0'

    def _repr_latex_(self) -> str:
        n:tuple[str,...] = tuple(f'{v:+d}\\sqrt{{{k}}}' for k, v in self.items())
        return ''.join(n) if n else '0'

__init__(n=dict())

n = field(default_factory=dict) class-attribute instance-attribute

from_expr(e) staticmethod

Construct a SumOfSqrts from a sympy.Expr.

Parameters:

  • e (Expr) –

    Expression to convert.

Returns:

Raises:

  • ValueError

    If the expression could not be converted.

Source code in radicalfield\sumofsqrts.py
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
@staticmethod
def from_expr(e:sp.Expr) -> 'SumOfSqrts':
    r"""Construct a `SumOfSqrts` from a `sympy.Expr`.

    Parameters
    ----------
    e
        Expression to convert.

    Returns
    -------
    SumOfSqrts
        Expression as `SumOfSqrts`.

    Raises
    ------
    ValueError
        If the expression could not be converted.
    """
    if not isinstance(e, sp.Expr):
        raise TypeError('e must be a sympy.Expr')

    def rat_to_int_or_frac(r:sp.Integer|sp.Rational) -> int|Fraction:
        if isinstance(r, sp.Integer):
            return int(r)
        elif isinstance(r, sp.Rational):
            return Fraction(int(r.p), int(r.q))
        else:
            raise ValueError(f'not in ℚ: {r}')

    n:defaultdict[int,int|Fraction] = defaultdict(int)
    for term in e.as_ordered_terms():
        v, radical = term.as_coeff_Mul()
        radicand:sp.Integer = radical**2
        n[int(radicand)] += rat_to_int_or_frac(v)

    return SumOfSqrts(n)

sqrtOf(n) staticmethod

Source code in radicalfield\sumofsqrts.py
92
93
94
95
96
97
@staticmethod
def sqrtOf(n:int|Fraction) -> 'SumOfSqrts':
    if isinstance(n, int):
        return SumOfSqrts({n:1})
    else:
        return SumOfSqrts({n.numerator*n.denominator:Fraction(1, n.denominator)})

normalize(d) staticmethod

Normalise a dict of radicals and rational factors into squarefree form.

\[ \sum_iv_i\sqrt{k_i} \]

Explicitly, each term is transformed as

\[ v\sqrt{k} = v\sqrt{s^2r} = vs\sqrt{r} \qquad \text{where $r$ is squarefree} \]

and for the whole sum:

  • zero terms are filtered out,
  • type and value checked,
  • square components in the radicands are pulled out &
  • the result is sorted by increasing radicand.

TODO: inplace version?

Parameters:

  • d (dict[int, int | Fraction]) –

    Mapping of radicands \(k_i\) to factors \(v_i\).

Returns:

  • dict[int, int | Fraction]

    Mathematically equivalent but cleaner copy.

Raises:

  • TypeError

    If a radicand is not an integer or a coefficient is not an integer or fraction.

  • ValueError

    If a radicand is negative.

Source code in radicalfield\sumofsqrts.py
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
@staticmethod
def normalize(d:dict[int,int|Fraction]) -> dict[int,int|Fraction]:
    r"""Normalise a dict of radicals and rational factors into squarefree form.

    $$
        \sum_iv_i\sqrt{k_i}
    $$

    Explicitly, each term is transformed as

    $$
        v\sqrt{k} = v\sqrt{s^2r} = vs\sqrt{r} \qquad \text{where $r$ is squarefree}
    $$

    and for the whole sum:

    - zero terms are filtered out,
    - type and value checked,
    - square components in the radicands are pulled out &
    - the result is sorted by increasing radicand.

    TODO: inplace version?

    Parameters
    ----------
    d : dict[int,int|Fraction]
        Mapping of radicands $k_i$ to factors $v_i$.

    Returns
    -------
    dict[int,int|Fraction]
        Mathematically equivalent but cleaner copy.

    Raises
    ------
    TypeError
        If a radicand is not an integer or a coefficient is not an
        integer or fraction.
    ValueError
        If a radicand is negative.
    """
    n:defaultdict[int,int|Fraction] = defaultdict(int)
    for k, v in d.items():
        if k and v: #filter v_0\sqrt{0} and 0\sqrt{k}
            if not (isinstance(k, int) and isinstance(v, (int, Fraction))):
                raise TypeError('radicands must be integers, factors must be integers or fractions')
            if not k>=0:
                raise ValueError('radicands must be non-negative')
            f:dict[int,int] = factorint(k)
            s:int = prod(p**(e//2) for p, e in f.items())
            k:int = prod(p**(e%2) for p, e in f.items())
            n[k] += s * v
            if not n[k]:
                del n[k]
    return dict(sorted(n.items()))

__post_init__()

Source code in radicalfield\sumofsqrts.py
162
163
164
165
166
167
168
169
170
def __post_init__(self) -> None:
    if isinstance(self.n, (int, Fraction)):
        n:dict[int,int|Fraction] = {1:self.n} if self.n else {}
    elif isinstance(self.n, dict):
        n:dict[int,int|Fraction] = SumOfSqrts.normalize(self.n)
    else:
        raise TypeError('argument must be dict, int or fraction')

    object.__setattr__(self, 'n', MappingProxyType(n))

__len__()

Return the number of terms.

Returns:

  • int

    Number of terms.

Source code in radicalfield\sumofsqrts.py
175
176
177
178
179
180
181
182
183
def __len__(self) -> int:
    """Return the number of terms.

    Returns
    -------
    int
        Number of terms.
    """
    return len(self.n)

get(key)

Source code in radicalfield\sumofsqrts.py
205
206
def get(self, key:int) -> int|Fraction:
    return self.n.get(key, 0)

keys()

Source code in radicalfield\sumofsqrts.py
208
209
def keys(self) -> KeysView[int]:
    return self.n.keys()

values()

Source code in radicalfield\sumofsqrts.py
211
212
def values(self) -> ValuesView[int|Fraction]:
    return self.n.values()

items()

Source code in radicalfield\sumofsqrts.py
214
215
def items(self) -> ItemsView[int,int|Fraction]:
    return self.n.items()

__bool__()

Return whether this element is unequal zero.

Returns:

  • bool

    Whether this element is unequal zero.

Source code in radicalfield\sumofsqrts.py
220
221
222
223
224
225
226
227
228
def __bool__(self) -> bool:
    """Return whether this element is unequal zero.

    Returns
    -------
    bool
        Whether this element is unequal zero.
    """
    return bool(self.n)

is_rational()

Return whether this element has no radical component.

Notes

Not a property to be consistent with fractions.Fraction.is_integer().

Returns:

  • bool

    Whether this element has no radical component.

Source code in radicalfield\sumofsqrts.py
230
231
232
233
234
235
236
237
238
239
240
241
242
def is_rational(self) -> bool:
    r"""Return whether this element has no radical component.

    Notes
    -----
    Not a property to be consistent with `fractions.Fraction.is_integer()`.

    Returns
    -------
    bool
        Whether this element has no radical component.
    """
    return set(self.keys()) <= {1}

as_fraction()

Return this element as a fraction.

Returns:

  • Fraction

    This element as a fraction.

Raises:

  • ValueError

    If this element is not a fraction.

Source code in radicalfield\sumofsqrts.py
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
def as_fraction(self) -> Fraction:
    """Return this element as a fraction.

    Returns
    -------
    Fraction
        This element as a fraction.

    Raises
    ------
    ValueError
        If this element is not a fraction.
    """
    if not self.is_rational():
        raise ValueError('not a fraction')
    return Fraction(self.get(1))

is_integer()

Return whether this element is an integer.

Notes

Not a property to be consistent with fractions.Fraction.is_integer().

Returns:

  • bool

    Whether this element is an integer.

Source code in radicalfield\sumofsqrts.py
261
262
263
264
265
266
267
268
269
270
271
272
273
def is_integer(self) -> bool:
    """Return whether this element is an integer.

    Notes
    -----
    Not a property to be consistent with `fractions.Fraction.is_integer()`.

    Returns
    -------
    bool
        Whether this element is an integer.
    """
    return set(self.keys())<={1} and isinstance(self.get(1), int)

__int__()

Return this element as an integer.

Returns:

  • int

    This element as an integer.

Raises:

  • ValueError

    If this element is not an integer.

Source code in radicalfield\sumofsqrts.py
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
def __int__(self) -> int:
    """Return this element as an integer.

    Returns
    -------
    int
        This element as an integer.

    Raises
    ------
    ValueError
        If this element is not an integer.
    """
    if not self.is_integer():
        raise ValueError('not an integer')
    return self.get(1)

__float__()

Return this element as a float.

Returns:

  • float

    This element as a float.

Source code in radicalfield\sumofsqrts.py
292
293
294
295
296
297
298
299
300
301
def __float__(self) -> float:
    """Return this element as a float.

    Returns
    -------
    float
        This element as a float.
    """
    #explicitly float as sumprod([], []) == int(0)
    return float(sumprod(self.values(), map(sqrt, self.keys())))

__eq__(other)

__eq__(other: Self) -> bool
__eq__(other: int) -> bool
__eq__(other: Fraction) -> bool
Source code in radicalfield\sumofsqrts.py
315
316
317
318
319
320
def __eq__(self, other:Any) -> bool|NotImplementedType:
    if isinstance(other, SumOfSqrts):
        return self.n == other.n
    elif isinstance(other, (int, Fraction)):
        return self.n == ({1:other} if other else {})
    return NotImplemented

__lt__(other)

__lt__(other: Self) -> bool
__lt__(other: int) -> bool
__lt__(other: Fraction) -> bool

Return whether this element is less than the other.

Notes

Repeatedly finds the largest prime factors in the radicands, separates these terms onto one side and squares until a rational inequality is left. Extremely slow and terrrible complexity.

Parameters:

  • other (Any) –

    Operand to compare to.

Returns:

  • bool

    Whether this element is less than the other.

References
Source code in radicalfield\sumofsqrts.py
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
def __lt__(self, other:Any) -> bool|NotImplementedType:
    """Return whether this element is less than the other.

    Notes
    -----
    Repeatedly finds the largest prime factors in the radicands,
    separates these terms onto one side and squares
    until a rational inequality is left.
    Extremely slow and terrrible complexity.

    Parameters
    ----------
    other: SumOfSqrts or int or Fraction
        Operand to compare to.

    Returns
    -------
    bool
        Whether this element is less than the other.

    References
    ----------
    - [StackExchange - Determine sign of sum of square roots](https://math.stackexchange.com/a/1076510)
    - [Wikipedia - Square-root sum problem](https://en.wikipedia.org/wiki/Square-root_sum_problem)
    """
    if isinstance(other, (SumOfSqrts, int, Fraction)):
        l:SumOfSqrts = self - other

        while not l.is_rational():
            p:int = max(chain.from_iterable(map(primefactors, l.keys())))

            r:SumOfSqrts = SumOfSqrts._create_directly({k//p:-v for k, v in l.items() if k%p==0})
            l:SumOfSqrts = SumOfSqrts._create_directly({k:v for k, v in l.items() if k%p!=0})
            #https://math.stackexchange.com/a/2347212
            l, r = l*abs(l), r*abs(r)*p

            l -= r

        return l.as_fraction() < 0
    return NotImplemented

__abs__()

Source code in radicalfield\sumofsqrts.py
369
370
def __abs__(self):
    return +self if self>=0 else -self

norm()

Return the algebraic norm.

\[ \begin{aligned} N\left(\sum_iv_i\sqrt{k_i}\right) = &\left(+v_1+v_2\sqrt{2}+v_3\sqrt{3}+\cdots,\right) \\ &\left(+v_1-v_2\sqrt{2}+v_3\sqrt{3}+\cdots,\right) \\ &\left(+v_1+v_2\sqrt{2}-v_3\sqrt{3}+\cdots,\right) \\ &\left(+v_1-v_2\sqrt{2}-v_3\sqrt{3}+\cdots, \dots\right) \\ &\cdots \end{aligned} \]

Product of all conjugates.

Returns:

  • int or Fraction

    The algebraic norm.

References

Wikipedia - Quadratic integers - Norm and conjugation

Source code in radicalfield\sumofsqrts.py
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
def norm(self) -> int|Fraction:
    r"""Return the algebraic norm.

    $$
        \begin{aligned}
            N\left(\sum_iv_i\sqrt{k_i}\right)
            = &\left(+v_1+v_2\sqrt{2}+v_3\sqrt{3}+\cdots,\right) \\
            &\left(+v_1-v_2\sqrt{2}+v_3\sqrt{3}+\cdots,\right) \\
            &\left(+v_1+v_2\sqrt{2}-v_3\sqrt{3}+\cdots,\right) \\
            &\left(+v_1-v_2\sqrt{2}-v_3\sqrt{3}+\cdots, \dots\right) \\
            &\cdots
        \end{aligned}
    $$

    Product of all conjugates.

    Returns
    -------
    int or Fraction
        The algebraic norm.

    References
    ----------
    [Wikipedia - Quadratic integers - Norm and conjugation](https://en.wikipedia.org/wiki/Quadratic_integer#Norm_and_conjugation)
    """
    return prod(islice(self.conjugate(), 2**len(self))).get(1)

conjugate()

Yield the algebraic conjugations.

\[ \begin{aligned} &\left(+v_1+v_2\sqrt{2}+v_3\sqrt{3}+\cdots,\right. \\ &\left.+v_1-v_2\sqrt{2}+v_3\sqrt{3}+\cdots,\right. \\ &\left.-v_1+v_2\sqrt{2}+v_3\sqrt{3}+\cdots,\right. \\ &\left.-v_1-v_2\sqrt{2}+v_3\sqrt{3}+\cdots, \dots\right) \end{aligned} \]

The algebraic conjugates are all sign flip permutations.

Yields:

References

Wikipedia - Quadratic integers - Norm and conjugation

Source code in radicalfield\sumofsqrts.py
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
def conjugate(self) -> Generator[Self]:
    r"""Yield the algebraic conjugations.

    $$
        \begin{aligned}
            &\left(+v_1+v_2\sqrt{2}+v_3\sqrt{3}+\cdots,\right. \\
            &\left.+v_1-v_2\sqrt{2}+v_3\sqrt{3}+\cdots,\right. \\
            &\left.-v_1+v_2\sqrt{2}+v_3\sqrt{3}+\cdots,\right. \\
            &\left.-v_1-v_2\sqrt{2}+v_3\sqrt{3}+\cdots, \dots\right)
        \end{aligned}
    $$

    The algebraic conjugates are all sign flip permutations.

    Yields
    -------
    SumOfSqrts
        The algebraic conjugations.

    References
    ----------
    [Wikipedia - Quadratic integers - Norm and conjugation](https://en.wikipedia.org/wiki/Quadratic_integer#Norm_and_conjugation)
    """
    for s in product((True, False), repeat=len(self)):
        yield SumOfSqrts._create_directly({k:(+v if s else -v) for k, v, s in zip(self.keys(), self.values(), s)})

__pos__()

Return itself.

\[ +\sum_iv_i\sqrt{k_i} = \sum_i+v_i\sqrt{k_i} \]

Returns:

Source code in radicalfield\sumofsqrts.py
429
430
431
432
433
434
435
436
437
438
439
440
441
def __pos__(self) -> Self:
    r"""Return itself.

    $$
        +\sum_iv_i\sqrt{k_i} = \sum_i+v_i\sqrt{k_i}
    $$

    Returns
    -------
    SumOfSqrts
        Itself.
    """
    return SumOfSqrts._create_directly({k:+v for k, v in self.items()})

__neg__()

Return the negation.

\[ -\sum_iv_i\sqrt{k_i} = \sum_i-v_i\sqrt{k_i} \]

Returns:

Source code in radicalfield\sumofsqrts.py
443
444
445
446
447
448
449
450
451
452
453
454
455
def __neg__(self) -> Self:
    r"""Return the negation.

    $$
        -\sum_iv_i\sqrt{k_i} = \sum_i-v_i\sqrt{k_i}
    $$

    Returns
    -------
    SumOfSqrts
        The negation.
    """
    return SumOfSqrts._create_directly({k:-v for k, v in self.items()})

__add__(other)

__add__(other: Self) -> Self
__add__(other: int) -> Self
__add__(other: Fraction) -> Self

Return the sum.

\[ \sum_iv_i\sqrt{k_i} + \sum_iw_i\sqrt{k_i} = \sum_i(v_i+w_i)\sqrt{k_i} \]

Parameters:

  • other (Any) –

    Other summand.

Returns:

Source code in radicalfield\sumofsqrts.py
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
def __add__(self, other:Any) -> Self|NotImplementedType:
    r"""Return the sum.

    $$
        \sum_iv_i\sqrt{k_i} + \sum_iw_i\sqrt{k_i}
        = \sum_i(v_i+w_i)\sqrt{k_i}
    $$

    Parameters
    ----------
    other: SumOfSqrts or int or Fraction
        Other summand.

    Returns
    -------
    SumOfSqrts
        The sum.
    """
    if isinstance(other, (int, Fraction)):
        other:SumOfSqrts = SumOfSqrts(other)
    if isinstance(other, SumOfSqrts):
        n:defaultdict[int,int|Fraction] = defaultdict(int, self.n)
        for k, v in other.items():
            n[k] += v
            if not n[k]:
                del n[k]
        return SumOfSqrts._create_directly(n)
    return NotImplemented

__radd__(other)

__radd__(other: int) -> Self
__radd__(other: Fraction) -> Self
Source code in radicalfield\sumofsqrts.py
497
498
def __radd__(self, other:Any) -> Self|NotImplementedType:
    return self + other #__add__

__sub__(other)

__sub__(other: Self) -> Self
__sub__(other: int) -> Self
__sub__(other: Fraction) -> Self

Return the difference.

\[ \sum_iv_i\sqrt{k_i} - \sum_iw_i\sqrt{k_i} = \sum_i(v_i-w_i)\sqrt{k_i} \]

Parameters:

  • other (Any) –

    The subtrahend.

Returns:

Source code in radicalfield\sumofsqrts.py
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
def __sub__(self, other:Any) -> Self|NotImplementedType:
    r"""Return the difference.

    $$
        \sum_iv_i\sqrt{k_i} - \sum_iw_i\sqrt{k_i}
        = \sum_i(v_i-w_i)\sqrt{k_i}
    $$

    Parameters
    ----------
    other: SumOfSqrts or int or Fraction
        The subtrahend.

    Returns
    -------
    SumOfSqrts
        The difference.
    """
    if isinstance(other, (int, Fraction)):
        other:SumOfSqrts = SumOfSqrts(other)
    if isinstance(other, SumOfSqrts):
        n:defaultdict[int,int|Fraction] = defaultdict(int, self.n)
        for k, v in other.items():
            n[k] -= v
            if not n[k]:
                del n[k]
        return SumOfSqrts._create_directly(n)
    return NotImplemented

__rsub__(other)

__rsub__(other: int) -> Self
__rsub__(other: Fraction) -> Self
Source code in radicalfield\sumofsqrts.py
539
540
def __rsub__(self, other:Any) -> Self|NotImplementedType:
    return (-self) + other #__add__

__mul__(other)

__mul__(other: Self) -> Self
__mul__(other: int) -> Self
__mul__(other: Fraction) -> Self

Return the product.

\[ \sum_iv_i\sqrt{k_i}\sum_jw_j\sqrt{l_j} = \sum_{ij}v_iw_i\sqrt{k_il_j} \]

Parameters:

  • other (Any) –

    The other factor.

Returns:

Source code in radicalfield\sumofsqrts.py
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
def __mul__(self, other:Any) -> Self|NotImplementedType:
    r"""Return the product.

    $$
        \sum_iv_i\sqrt{k_i}\sum_jw_j\sqrt{l_j}
        = \sum_{ij}v_iw_i\sqrt{k_il_j}
    $$

    Parameters
    ----------
    other: SumOfSqrts or int or Fraction
        The other factor.

    Returns
    -------
    SumOfSqrts
        The product.
    """
    if isinstance(other, SumOfSqrts):
        n:defaultdict[int,int|Fraction] = defaultdict(int)
        for ki, vi in self.items():
            for kj, vj in other.items():
                n[ki*kj] += vi * vj
        return SumOfSqrts(n)
    elif isinstance(other, (int, Fraction)):
        n:dict[int,int|Fraction] = {k:v*other for k, v in self.items()} if other else {}
        return SumOfSqrts._create_directly(n)
    return NotImplemented

__rmul__(other)

__rmul__(other: int) -> Self
__rmul__(other: Fraction) -> Self
Source code in radicalfield\sumofsqrts.py
582
583
def __rmul__(self, other:Any) -> Self|NotImplementedType:
    return self * other #__mul__

inv()

Return the multiplicative inverse.

Notes

Starts with \(\frac{1}{x}\) and then repeatedly multiplies \(\frac{\overline{x}^i}{\overline{x}^i}\) where \(\overline{x}^i\) denotes the \(i\)-th conjugate (\(i\)-th permutation of signs flipped). For half of all conjugates (first sign doesn't have to be flipped) because then has the denominator become rational.

Returns:

  • SumOfSqrts

    The multiplicative inverse element.

Raises:

  • ZeroDivisionError

    If the norm is zero.

See also

SumOfSqrts.norm

Source code in radicalfield\sumofsqrts.py
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
def inv(self) -> Self:
    r"""Return the multiplicative inverse.

    Notes
    -----
    Starts with $\frac{1}{x}$ and then repeatedly multiplies
    $\frac{\overline{x}^i}{\overline{x}^i}$
    where $\overline{x}^i$ denotes the $i$-th conjugate
    ($i$-th permutation of signs flipped). For half of all conjugates
    (first sign doesn't have to be flipped)
    because then has the denominator become rational.

    Returns
    -------
    SumOfSqrts
        The multiplicative inverse element.

    Raises
    ------
    ZeroDivisionError
        If the norm is zero.

    See also
    --------
    [`SumOfSqrts.norm`][radicalfield.sumofsqrts.SumOfSqrts.norm]
    """
    if not self:
        raise ZeroDivisionError('division by zero')
    n:SumOfSqrts = SumOfSqrts(1)
    d:SumOfSqrts = self
    for f in islice(self.conjugate(), 1, 2**len(self)):
        n *= f
        d *= f
    return n / d.as_fraction()

__truediv__(other)

__truediv__(other: Self) -> Self
__truediv__(other: int) -> Self
__truediv__(other: Fraction) -> Self

Return the quotient.

\[ \frac{\left(a+b\sqrt{2}\right)}{\left(c+d\sqrt{2}\right)} = \frac{\left(a+b\sqrt{2}\right)\left(c-d\sqrt{2}\right)}{\left(c+d\sqrt{2}\right)\left(c-d\sqrt{2}\right)} = \frac{\left(ac-2bd\right)+\left(bc-ad\right)\sqrt{2}}{c^2-2d^2} \]

More often than necessary promoted to rationals.

Parameters:

  • other (Any) –

    The denominator.

Returns:

Raises:

  • ZeroDivisionError

    If the norm of the denominator is zero.

Source code in radicalfield\sumofsqrts.py
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
def __truediv__(self, other:Any) -> Self|NotImplementedType:
    r"""Return the quotient.

    $$
        \frac{\left(a+b\sqrt{2}\right)}{\left(c+d\sqrt{2}\right)}
        = \frac{\left(a+b\sqrt{2}\right)\left(c-d\sqrt{2}\right)}{\left(c+d\sqrt{2}\right)\left(c-d\sqrt{2}\right)}
        = \frac{\left(ac-2bd\right)+\left(bc-ad\right)\sqrt{2}}{c^2-2d^2}
    $$

    More often than necessary promoted to rationals.

    Parameters
    ----------
    other: SumOfSqrts or int or Fraction
        The denominator.

    Returns
    -------
    SumOfSqrts
        The quotient.

    Raises
    ------
    ZeroDivisionError
        If the norm of the denominator is zero.
    """
    if isinstance(other, SumOfSqrts):
        return self * other.inv()
    elif isinstance(other, (int, Fraction)):
        other:Fraction = Fraction(other)
        return SumOfSqrts._create_directly({k:v/other for k, v in self.items()})
    return NotImplemented

__rtruediv__(other)

__rtruediv__(other: int) -> Self
__rtruediv__(other: Fraction) -> Self
Source code in radicalfield\sumofsqrts.py
664
665
666
667
def __rtruediv__(self, other:Any) -> Self|NotImplementedType:
    if isinstance(other, (int, Fraction)):
        return other * self.inv()
    return NotImplemented

__repr__()

Source code in radicalfield\sumofsqrts.py
672
673
674
def __repr__(self) -> str:
    n:tuple[str,...] = tuple(f'{v:+}{chr(0x221A)}{k}' for k, v in self.items())
    return ''.join(n) if n else '0'