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}\). Hence length() is exact precisely when lengthsqr is rational — which canonical arithmetic guarantees whenever the geometry is metric-rational (the crystallographic case); otherwise it raises.

Classes

SurdVector

An immutable exact tensor over the squarefree-radical field

SurdScalar

A scalar SurdVector (shape ()): a single field element

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 one dim); radicand 1 is the rational part. It is stored canonically — coefficients simplified, all-zero coefficients dropped — so the representation is unique and equality/zero-detection are exact. Like FracVector it 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 q as a SurdScalar.

The result is a plain rational when q is 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 as sqrt(p*q)/q so the stored radicand is always a positive squarefree integer (sqrt_of(1/2) == sqrt(2)/2). Raises ValueError on 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 (a SurdScalar for the default ()).

classmethod one() SurdScalar[source]

The scalar 1.

property dim: tuple[int, Ellipsis][source]

The shape tuple, as for dim.

property is_rational: bool[source]

True iff the value is purely rational (only the radicand-1 term is present).

is_zero() bool[source]

True iff the value is exactly zero (empty canonical form).

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^T as a SurdScalar.

length() SurdScalar[source]

Return the exact length sqrt(lengthsqr) as a SurdScalar.

Exact precisely when lengthsqr is rational — which canonical arithmetic guarantees for a difference of Cartesian sites under a rational metric (the crystallographic case). When lengthsqr is itself irrational the length would be a nested radical (sqrt(a + b*sqrt(c))), which is outside the field, so this raises ValueError.

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.Fraction within prec of the true value.

Exact (not merely within prec) whenever the value is rational. This is the compute(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: SurdVector

A 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 (raises ZeroDivisionError on zero).

sign() int[source]

Return the exact sign of the value: -1, 0 or 1.

For an irrational value the sign is decided by refining rational lower/upper bounds on each sqrt(r) (from integer_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 exact SurdScalar, 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\). q may be an int, Fraction, or numeric string (parsed via any_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 None result is therefore a proof that the exact cosine lies outside \(\mathbb{Q}[\sqrt n]\) — use cos() with degrees=True for a deterministic rational approximation in that case.

classmethod sin_degrees(q: Any) SurdScalar | None[source]

Return sin(q degrees) as an exact SurdScalar, or None when it is irrational.

Computed as cos(90 - q) degrees, so exactness follows the same classification as cos_degrees() applied to 90 - q: exact for all multiples of 15 degrees, otherwise None (a proof that the exact sine is outside the field). Note the asymmetry with cos_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 arccos of 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). Raises ValueError — decided exactly via sign() — 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") and max_refinements match sqrt() in Decimal mode.