httk.core.vectors.surdvector¶
Exact square-root arithmetic: SurdVector and SurdScalar.
Where FracVector closes the rationals under +,-,*,/,
SurdVector closes the rationals and the square roots of arbitrary positive rationals
under those same operations. It is the exact-arithmetic model for Cartesian crystallographic
geometry, where FracVector alone is not enough.
Why a square-root extension, and where¶
Symmetry operations and Wyckoff coordinates live in fractional coordinates and are already
exactly closed over FracVector: point-group operations are integer matrices and
translations are rationals, so no radicals ever appear. Radicals appear only when one moves to a
Cartesian frame: the hexagonal/trigonal basis carries a \(\sqrt 3\), Cartesian rotation
matrices carry \(\sqrt 2\)/\(\sqrt 3\), and a bond length is \(\sqrt{\text{rational}}\)
under a rational metric. SurdVector is exactly the arithmetic closure needed there.
The purpose boundary (magnitudes vs. linear structure)¶
For pure magnitude questions — comparing, sorting or testing equality of distances — the
squared representation is exact, cheaper, and already fully supported by FracVector: a
squared length lengthsqr is rational and a metric/Gram matrix G = B*B^T is rational, so
“is bond A shorter than bond B?” is an exact rational comparison of lengthsqr values. That
remains the recommended fast path.
Squaring fails, however, as soon as radicals sit inside additive / linear structure, and that
is exactly where SurdVector earns its keep:
components are signed and additive, and \((a+b)^2 \neq a^2 + b^2\) — the cross term is itself a radical;
a metric determines a basis only up to an orthogonal transformation, so squaring throws away orientation and chirality;
even scalar sums are not closed under squaring: \((\sqrt2+\sqrt3)^2 = 5 + 2\sqrt6\).
So SurdVector composes with the squared strategy rather than replacing it: its canonical
form collapses to the plain rational whenever a value happens to be rational, so a computation that
lands back in the rationals is transparently rational again.
The mathematical model¶
The values form the field \(\mathbb{Q}[\sqrt n : n \text{ squarefree}]\) — finite sums
\(\sum_r q_r\sqrt r\) with rational coefficients \(q_r\), the rationals being the radicand
\(r = 1\) term. The canonical form is the map {squarefree radicand -> rational coefficient}
with no zero entries; it is unique because the \(\sqrt r\) (over squarefree \(r\)) are
linearly independent over \(\mathbb{Q}\). Consequences used throughout:
Equality and zero-detection are exact: equal values have equal coefficient maps.
Products combine radicands: \(\sqrt r\,\sqrt s = c\,\sqrt t\) where \(r s = c^2 t\) with \(t\) squarefree (see
square_part).Division is a true field inverse via iterated per-prime conjugation: for each prime \(p\) dividing a radicand of the denominator, multiply numerator and denominator by the conjugate that flips the sign of every \(\sqrt r\) with \(p \mid r\); each such step eliminates \(p\). The coefficient count can grow like \(2^k\) in the number \(k\) of distinct primes among the denominator’s radicands — tiny in practice (crystallographic denominators involve one or two radicands).
Sign is exactly decidable: refine rational lower/upper bounds on each \(\sqrt r\) (via
integer_sqrt()at increasing precision) until the summed interval excludes zero. This terminates because a nonzero surd is never zero.Square roots are closed over positive rationals but NOT over surds (no nested radicals):
sqrt_of()takes any nonnegative rational to an exact surd, but there is no exact \(\sqrt{1+\sqrt2}\). Hencelength()is exact precisely whenlengthsqris rational — which canonical arithmetic guarantees whenever the geometry is metric-rational (the crystallographic case); otherwise it raises.
Classes¶
An immutable exact tensor over the squarefree-radical field |
|
A scalar |
Module Contents¶
- class httk.core.vectors.surdvector.SurdVector(components: dict[int, httk.core.vectors.fracvector.FracVector], dim: tuple[int, Ellipsis])[source]¶
An immutable exact tensor over the squarefree-radical field \(\mathbb{Q}[\sqrt n : n\ \text{squarefree}]\).
A SurdVector is a map
{squarefree radicand -> FracVector coefficient}(all coefficients sharing onedim); radicand1is the rational part. It is stored canonically — coefficients simplified, all-zero coefficients dropped — so the representation is unique and equality/zero-detection are exact. LikeFracVectorit is immutable and hashable.See the module docstring for the field facts, the fractional-vs-Cartesian motivation, and the magnitude-vs-linear-structure purpose boundary.
- classmethod create(obj: Any) SurdVector[source]¶
Create a SurdVector from a
FracVector, nested rationals, or another SurdVector.A SurdVector is returned unchanged; anything else is built through
create()and embedded exactly at radicand 1 (so every rational value is representable, and a rational input stays rational).
- classmethod from_radicand_map(mapping: dict[int, Any]) SurdVector[source]¶
Compose a SurdVector from a
{radicand -> coefficient}mapping.Radicands are positive integers and need not be squarefree — each is normalized via
square_part(sqrt(radicand) = s*sqrt(r)) and the coefficients (FracVector-like, all of one shape) folded together canonically.
- classmethod sqrt_of(q: Any) SurdScalar[source]¶
Return the exact square root of a nonnegative rational
qas aSurdScalar.The result is a plain rational when
qis a perfect square (e.g.sqrt_of(4/9) == 2/3) and otherwise a single-radical surd (sqrt_of(8) == 2*sqrt(2)).sqrt(p/q)is normalized assqrt(p*q)/qso the stored radicand is always a positive squarefree integer (sqrt_of(1/2) == sqrt(2)/2). RaisesValueErroron a negative argument — there is no exact square root of a surd (no nested radicals), only of a rational.
- classmethod zero(dim: tuple[int, Ellipsis] = ()) SurdVector[source]¶
The zero SurdVector of shape
dim(aSurdScalarfor the default()).
- classmethod one() SurdScalar[source]¶
The scalar
1.
- property is_rational: bool[source]¶
True iff the value is purely rational (only the radicand-1 term is present).
- property radicands: tuple[int, Ellipsis][source]¶
The sorted squarefree radicands present in the canonical form.
- coefficient(radicand: int) httk.core.vectors.fracvector.FracVector[source]¶
The FracVector coefficient of
sqrt(radicand)(a zero tensor when absent).
- T() SurdVector[source]¶
Return the transpose, transposing each radicand’s coefficient tensor.
- dot(other: Any) SurdScalar[source]¶
Vector dot product of two 1-D SurdVectors (
sum a_i b_i).
- lengthsqr() SurdScalar[source]¶
Return the squared length
A * A^Tas aSurdScalar.
- length() SurdScalar[source]¶
Return the exact length
sqrt(lengthsqr)as aSurdScalar.Exact precisely when
lengthsqris rational — which canonical arithmetic guarantees for a difference of Cartesian sites under a rational metric (the crystallographic case). Whenlengthsqris itself irrational the length would be a nested radical (sqrt(a + b*sqrt(c))), which is outside the field, so this raisesValueError.
- det() SurdScalar[source]¶
Return the determinant of a 3x3 SurdVector as a
SurdScalar.
- inv() SurdVector[source]¶
Return the inverse of a 3x3 SurdVector via the adjugate and the scalar field inverse.
- to_fractions_approx(prec: fractions.Fraction = fractions.Fraction(1, 10**30)) Any[source]¶
A deterministic nested list of
fractions.Fractionwithinprecof the true value.Exact (not merely within
prec) whenever the value is rational. This is thecompute(prec)-shaped rational approximation reused by the Decimal rendering.
- to_floats(prec: fractions.Fraction = fractions.Fraction(1, 10**30)) Any[source]¶
A nested list of floats (via a high-precision exact rational approximation).
- class httk.core.vectors.surdvector.SurdScalar(components: dict[int, httk.core.vectors.fracvector.FracVector], dim: tuple[int, Ellipsis])[source]¶
Bases:
SurdVectorA scalar
SurdVector(shape()): a single field element \(\sum_r q_r\sqrt r\).Adds the scalar-only operations — the field inverse, exact sign and ordering, and Decimal rendering — that need a single value rather than a tensor.
- inverse() SurdScalar[source]¶
The multiplicative inverse
1/self(raisesZeroDivisionErroron zero).
- sign() int[source]¶
Return the exact sign of the value:
-1,0or1.For an irrational value the sign is decided by refining rational lower/upper bounds on each
sqrt(r)(frominteger_sqrt()at increasing precision) and summing the weighted intervals until the total interval excludes zero — which always happens in finitely many steps because a nonzero surd is bounded away from zero.
- classmethod cos_degrees(q: Any) SurdScalar | None[source]¶
Return
cos(q degrees)as an exactSurdScalar, or None when it is not a surd.The value lies in the squarefree-radical field precisely when the angle, reduced modulo 360, is a multiple of 15 or of 36 degrees — e.g. \(\cos 30° = \tfrac{\sqrt3}2\), \(\cos 15° = \tfrac{\sqrt6+\sqrt2}4\), \(\cos 36° = \tfrac{1+\sqrt5}4\).
qmay be an int,Fraction, or numeric string (parsed viaany_to_fraction()).That list is complete: \(\cos(2\pi a/b)\) lies in a field generated by square roots of rationals iff the Galois group \((\mathbb{Z}/b)^\times/\{\pm1\}\) of \(\mathbb{Q}(\cos 2\pi/b)\) has exponent at most 2, which holds exactly for \(b \in \{1,2,3,4,5,6,8,10,12,24\}\) — the rational-degree angles that are multiples of 15° or 36°. (Niven’s theorem is the rational-value special case of this classification.) A
Noneresult is therefore a proof that the exact cosine lies outside \(\mathbb{Q}[\sqrt n]\) — usecos()withdegrees=Truefor a deterministic rational approximation in that case.
- classmethod sin_degrees(q: Any) SurdScalar | None[source]¶
Return
sin(q degrees)as an exactSurdScalar, or None when it is irrational.Computed as
cos(90 - q)degrees, so exactness follows the same classification ascos_degrees()applied to90 - q: exact for all multiples of 15 degrees, otherwise None (a proof that the exact sine is outside the field). Note the asymmetry withcos_degrees()for the 36° family: \(\cos 36°\) is an exact surd but \(\sin 36° = \cos 54°\) is not (54 is a multiple of neither 15 nor 36).
- acos_degrees() fractions.Fraction | None[source]¶
Return the exact
arccosof this value in degrees over \([0, 180]\), or None.This is the reverse table lookup: the result is an exact rational number of degrees precisely when the value equals the cosine of a multiple of 15° or 36° (the complete set of rational-degree angles with surd cosines — see
cos_degrees()), decided by exact surd equality; otherwise None (the exact angle is then irrational in degrees). RaisesValueError— decided exactly viasign()— when the value lies outside \([-1, 1]\).
- to_float(prec: fractions.Fraction = fractions.Fraction(1, 10**30)) float[source]¶
The value as a float (via a high-precision exact rational approximation).
- to_decimal(digits: int | None = None, rounding: str = 'half_even', max_refinements: int | None = None) Any[source]¶
Render the value as a correctly-rounded
decimal.Decimal.Reuses the exact-math module’s Ziv refinement loop (
_to_decimal): a rational value renders exactly (its finite expansion when it fits, else quantized), and an irrational surd — never on a rational rounding boundary — is rendered by refining the rational approximation until the rounding is determined.digits(significant digits; default: the active decimal context precision),rounding("half_even"/"down") andmax_refinementsmatchsqrt()in Decimal mode.