Vectors¶
This page documents the exact-rational vector library in httk.core.vectors and the Vector
backend/view family that lets the same tensor data be viewed as the exact representation, as
plain nested sequences, or (optionally) as numpy arrays.
Exact rational arithmetic¶
FracVector is an immutable N-dimensional tensor stored as a nested tuple of integer nominators
over a single shared integer denominator. All arithmetic is exact — there is no floating-point
rounding anywhere — which makes it well suited to crystallography, where cell vectors and reduced
coordinates are naturally rational.
from httk.core import FracVector
cell = FracVector.create([["8.04", "0.0", "0.0"],
["0.0", "3.72", "0.0"],
["0.0", "0.0", "7.38"]])
# Exact reciprocal cell (rows are the reciprocal vectors, without the 2*pi factor):
recip = cell.reciprocal()
assert recip.noms == ((3431700, 0, 0), (0, 7416900, 0), (0, 0, 3738600))
assert recip.denom == 27590868
The raw representation is always a nested tuple of integer nominators (.noms) over one shared
integer denominator (.denom); the tensor is (1/denom) * noms. Equality (==) compares
numerical value, so the denominator need not match:
from httk.core import FracVector
assert FracVector([[1]], 2) == FracVector([[2]], 4) # both are 1/2
Creation from every numeric type¶
FracVector.create(...) accepts nested lists/tuples whose leaves are any of the following:
leaf type |
example |
becomes |
|---|---|---|
|
|
|
|
|
|
|
|
|
|
|
best rational in the interval |
|
|
|
|
|
|
|
|
the exact binary rational |
import decimal
import fractions
from httk.core import FracVector
# int, str, Decimal and Fraction leaves all land on their exact rational value:
assert FracVector.create([decimal.Decimal("0.25"), decimal.Decimal("2.125")]).to_tuple() == (8, (2, 17))
assert FracVector.create([fractions.Fraction(2, 3), fractions.Fraction(3, 4)]).to_tuple() == (12, (8, 9))
assert FracVector.create("2/3").to_fraction() == fractions.Fraction(2, 3)
String input is parsed for significant digits, so a written decimal is taken at its stated precision (and an explicit standard deviation is honored) rather than as an exact binary float:
import fractions
from httk.core import FracVector
FracVector.create("0.33") # -> 33/100 (0.33 assumed to mean 0.3300)
FracVector.create("0.3333") # -> 1/3 (enough digits to imply 1/3)
FracVector.create("0.33", min_accuracy=None) # -> 33/100, converted exactly
FracVector.create(["0.33342(10)"]) # -> 1/3, using the explicit standard deviation
assert FracVector.create("0.33").to_fraction() == fractions.Fraction(33, 100)
assert FracVector.create("0.3333").to_fraction() == fractions.Fraction(1, 3)
assert FracVector.create("0.3333", min_accuracy=None).to_fraction() == fractions.Fraction(3333, 10000)
assert FracVector.create(["0.33342(10)"]) == FracVector.create(["1/3"])
The binary-rational caveat: a Python float literal is already a binary rational, so
8.04 is not 804/100. FracVector.create([8.04]) therefore stores fractions.Fraction(8.04)
(a value with a huge denominator), not 804/100. Use the string form "8.04" (or an explicit
Decimal) when you mean the decimal value:
import fractions
from httk.core import FracVector
assert FracVector.create([8.04]).to_fractions() == [fractions.Fraction(8.04)]
assert FracVector.create([8.04]) != FracVector.create(["8.04"])
The laziness / simplify contract¶
Most operations deliberately return an un-simplified result — they do not reduce to the
smallest denominator. This keeps intermediate arithmetic cheap while staying exact. Call
.simplify() at the end of a computation when you want the reduced form:
from httk.core import FracVector
third = FracVector.create("1/3")
product = third * 3 # value is 1, but stored un-simplified
assert product.simplify() == 1
assert product.simplify().denom == 1
messy = FracVector(((2, 4), (6, 8)), 4)
assert messy.simplify().to_tuple() == (2, ((1, 2), (3, 4)))
.simplify() is idempotent and value-preserving.
Denominator control¶
Besides simplify, three methods control the denominator explicitly:
create(..., denom=D)divides every nominator by an extra common factorD.set_denominator(D)re-expresses each element as the closest fraction over the fixed denominatorD(reduced resolution).limit_denominator(max_denom)replaces each element with the closest fraction whose denominator is at mostmax_denom— the exact-rational analogue offractions.Fraction.limit_denominator, and the tool that recovers a small rational from a value that has passed through float.
import fractions
from httk.core import FracVector
assert FracVector.create([[1, 2, 3]], 6).to_tuple() == (6, ((1, 2, 3),))
assert FracVector.create([["1/3", "2/7"]]).set_denominator(1000).to_tuple() == (1000, ((333, 286),))
binary = fractions.Fraction(6004799503160661, 18014398509481984) # float64 of 1/3
assert FracVector.create([[binary]]).limit_denominator(1000) == FracVector.create([["1/3"]])
Linear algebra tour¶
* is matrix multiplication; +/-// are element-wise; A ** -1 is the matrix inverse
(A.inv()) and A ** n is the repeated matrix product (including negative n). Every result is
exact:
from httk.core import FracVector
a = FracVector.create([[2, 3, 5], [3, 5, 4], [4, 6, 7]])
# determinant (3x3 and 4x4), transpose, inverse:
assert a.det() == -3
assert FracVector.create([[1, 2, 3], [4, 5, 6]]).T() == FracVector.create([[1, 4], [2, 5], [3, 6]])
assert a.inv().simplify().to_tuple() == (3, ((-11, -9, 13), (5, 6, -7), (2, 0, -1)))
assert (a * a.inv()).simplify() == FracVector.eye((3, 3))
assert (a ** -2).simplify() == (a.inv() * a.inv()).simplify()
Vector products and the metric:
from httk.core import FracVector
u = FracVector.create([1, 2, 3])
v = FracVector.create([4, 5, 6])
assert u.dot(v) == 32 # A . B (== A * B.T())
assert u.cross(v) == FracVector.create([-3, 6, -3]) # A x B
assert FracVector.create([3, 4, 12]).lengthsqr() == 169 # A * A.T()
# reciprocal cell (rows are reciprocal vectors) and the metric product vecA * M * vecB.T():
metric = FracVector.create([[2, 0, 0], [0, 3, 0], [0, 0, 4]])
ones = FracVector.create([1, 1, 1])
assert metric.metric_product(ones, ones) == 9
Other staples: normalize() (shift each element into [0, 1)), normalize_half() (into
[-1/2, 1/2), useful for the shortest vector under periodic boundary conditions), floor/ceil/
sign on scalars, and max/min/argmax/argmin/nargmax/nargmin.
MutableFracVector¶
MutableFracVector is a list-backed variant of FracVector. Reach for it when you need to build
or edit a tensor in place — element and slice assignment and the set_* mutators — rather
than threading copies through a computation. Its inherited (non-set_) methods still return
copies, exactly like FracVector.
import fractions
from httk.core import FracVector, MutableFracVector
m = MutableFracVector.from_FracVector(FracVector.create([[1, 2, 3], [4, 5, 6]]))
m[1, 1:] = [40, 50] # slice assignment
assert m.noms == [[1, 2, 3], [4, 40, 50]]
# assignment puts both sides on a common denominator automatically:
half = MutableFracVector.from_FracVector(FracVector.create([["1/2", "1/2"]]))
half[0, 1] = fractions.Fraction(1, 3)
assert half.to_FracVector().simplify() == FracVector.create([["1/2", "1/3"]])
The set_* mutators replace the vector in place. set_inv() leaves the vector numerically equal
to FracVector.inv(), set_simplify() reduces to the smallest (integer) denominator, and there
are set_T, set_negative, set_normalize, set_normalize_half, set_set_denominator too:
from httk.core import FracVector, MutableFracVector
a = FracVector.create([[2, 3, 5], [3, 5, 4], [4, 6, 7]])
m = MutableFracVector.from_FracVector(a)
m.set_inv()
assert m.to_FracVector() == a.inv()
assert m.denom == 3 and isinstance(m.denom, int)
Convert freely between the two: MutableFracVector.from_FracVector(fv) and m.to_FracVector()
(which returns a plain immutable FracVector). FracVector.use(x) normalizes any input to an
immutable FracVector, converting a MutableFracVector through its to_FracVector():
from httk.core import FracVector, MutableFracVector
m = MutableFracVector.from_FracVector(FracVector.create([[1, 2], [3, 4]]))
fv = FracVector.use(m)
assert type(fv) is FracVector
assert fv == m # mutable and immutable compare equal by value
FracScalar is the scalar specialization (a single nom/denom), used to make it explicit when a
scalar fracvector is expected.
exactmath and vectormath¶
See Exact math on rationals and decimals for the full standalone guide to the exact math functions.
The exact transcendental helpers live in httk.core.vectors.exactmath: type-preserving
sqrt/cos/sin/exp/pi/log/… . For fractions.Fraction input they return exact
rationals computed to a target precision prec (given as a Fraction) that approximate the true
(generally irrational) value to within prec; perfect cases come back exactly. For
decimal.Decimal input (or when digits= is passed) they instead return a correctly-rounded
Decimal.
import decimal
import fractions
import math
from httk.core.vectors import exactmath
F = fractions.Fraction
assert exactmath.sqrt(F(9, 4)) == F(3, 2) # perfect square: exact
approx = exactmath.sqrt(F(2), prec=F(1, 10**8)) # irrational: rational approx
assert abs(float(approx) - math.sqrt(2)) < 1e-8
pi = exactmath.pi(prec=F(1, 10**10))
assert abs(float(pi) - math.pi) < 1e-9
# Decimal domain: correctly-rounded to a number of significant digits.
assert exactmath.sqrt(decimal.Decimal(2), digits=12) == decimal.Decimal("1.41421356237")
httk.core.vectors.vectormath provides math-style functional wrappers that dispatch to a
FracVector’s own (exact, element-wise) method when given one and fall back to math on plain
scalars:
from httk.core import FracVector
from httk.core.vectors import vectormath
# element-wise on a FracVector (exact):
assert vectormath.sqrt(FracVector.create([4, 9])).simplify() == FracVector.create([2, 3])
# plain scalar falls back to math:
assert vectormath.floor(2.7) == 2
Exact radicals: SurdVector¶
Symmetry lives in fractional coordinates, and there it needs no radicals: point-group
operations are integer matrices and translations are rational, so FracVector closes the whole
algebra exactly. Radicals appear only when you move to a Cartesian frame — the hexagonal basis
carries a \(\sqrt3\), Cartesian rotations carry \(\sqrt2\)/\(\sqrt3\), and a bond length is
\(\sqrt{\text{rational}}\) under a rational metric. SurdVector is the exact-arithmetic model for
that Cartesian layer: it closes the rationals and the square roots of arbitrary positive
rationals under +, -, *, /. Its values form the field
\(\mathbb{Q}[\sqrt n : n\ \text{squarefree}]\) — finite sums \(\sum_r q_r\sqrt r\) with rational
coefficients — stored canonically as a {squarefree radicand → rational coefficient} map (unique,
because the \(\sqrt r\) are linearly independent over \(\mathbb{Q}\)), so equality and zero-detection
are exact.
Purpose boundary — magnitudes vs. linear structure
For pure magnitude questions — comparing, sorting, or equating distances — stay with the
squared representation: a squared length lengthsqr is rational and a Gram/metric matrix
G = B*B.T() is rational, so “is bond A shorter than bond B?” is an exact rational comparison,
already fully supported by FracVector, and it is the cheaper recommended fast path.
Reach for SurdVector only when radicals sit inside additive / linear structure, where
squaring fails: components are signed and additive so \((a+b)^2\neq a^2+b^2\) (the cross term is
itself a radical); a metric fixes a basis only up to an orthogonal transformation, so squaring
discards orientation and chirality; and even scalar sums are not closed, \((\sqrt2+\sqrt3)^2 =
5+2\sqrt6\). SurdVector composes with the squared strategy rather than replacing it: whenever a
value lands back in the rationals its canonical form collapses to the plain rational again.
Square roots, products, and the nested-radical limit¶
SurdVector.sqrt_of(q) takes the exact square root of any nonnegative rational — a plain rational
when q is a perfect square, otherwise a canonical single-radical surd. Products combine radicands
(\(\sqrt r\,\sqrt s = c\,\sqrt t\)), and a value that squares back to a rational is rational again:
import fractions
from httk.core import SurdVector
F = fractions.Fraction
assert SurdVector.sqrt_of(8) == SurdVector.from_radicand_map({2: 2}) # 2*sqrt(2)
assert SurdVector.sqrt_of(F(4, 9)) == SurdVector.create(F(2, 3)) # perfect square: rational
assert SurdVector.sqrt_of(F(1, 2)) == SurdVector.sqrt_of(2) * SurdVector.create(F(1, 2)) # sqrt(2)/2
assert SurdVector.sqrt_of(2) * SurdVector.sqrt_of(3) == SurdVector.sqrt_of(6)
assert SurdVector.sqrt_of(2) * SurdVector.sqrt_of(2) == SurdVector.create(2)
assert str(SurdVector.sqrt_of(3) * SurdVector.create(F(1, 2))) == "(1/2)*sqrt(3)"
The field is closed under +, -, *, and full / (division rationalizes the denominator by
iterated conjugation), but not under sqrt of a surd — there are no nested radicals. So
length() is exact precisely when lengthsqr() is rational, and it raises otherwise:
from httk.core import SurdVector
# 1/(1 + sqrt2 + sqrt3) is a genuine field inverse (verify by multiplying back):
d = SurdVector.one() + SurdVector.sqrt_of(2) + SurdVector.sqrt_of(3)
assert (SurdVector.one() / d) * d == SurdVector.one()
Exact comparison¶
Ordering and sign are decided exactly, by refining rational bounds on each \(\sqrt r\) until the sign resolves (a nonzero surd is never zero, so this always terminates):
from httk.core import SurdVector
lhs = SurdVector.sqrt_of(2) + SurdVector.sqrt_of(3) # ~3.1463
rhs = SurdVector.sqrt_of(10) # ~3.1623
assert lhs < rhs
A hexagonal Cartesian basis¶
The standard hexagonal basis \(B = \begin{bmatrix} a & 0 & 0\\ -a/2 & a\sqrt3/2 & 0\\ 0 & 0 & c\end{bmatrix}\)
has rational a, c but an irrational entry \(a\sqrt3/2\). Its determinant, inverse, and the metric
\(G = B\,B^\mathsf{T}\) are all exact, and a Cartesian bond length comes back as an exact radical:
import fractions
from httk.core import FracVector, SurdVector
F = fractions.Fraction
a, c = F(3), F(5)
# Assemble B from its rational part (radicand 1) and its sqrt(3) part (radicand 3):
B = SurdVector.from_radicand_map({
1: [[a, 0, 0], [-a / 2, 0, 0], [0, 0, c]],
3: [[0, 0, 0], [0, a / 2, 0], [0, 0, 0]],
})
identity = SurdVector.create(FracVector.eye((3, 3)))
assert B.det() == SurdVector.from_radicand_map({3: F(45, 2)}) # (45/2)*sqrt(3)
assert B * B.inv() == identity # exact inverse
assert (B * B.T()).is_rational # the metric is rational
# The second basis row is a Cartesian vector of exact length a:
row1 = SurdVector.from_radicand_map({1: [-a / 2, 0, 0], 3: [0, a / 2, 0]})
assert row1.lengthsqr() == SurdVector.create(a * a)
assert row1.length() == SurdVector.create(a)
# A vector whose squared length is irrational (3 + 2*sqrt(2)) has a nested-radical length:
v = SurdVector.from_radicand_map({1: [1, 0, 0], 2: [1, 0, 0]})
assert not v.lengthsqr().is_rational
try:
v.length()
raise AssertionError("expected a ValueError")
except ValueError:
pass
Decimal rendering¶
A SurdScalar renders as a correctly-rounded Decimal, reusing the same exactmath Ziv machinery
as Exact math on rationals and decimals: a rational surd renders its exact finite expansion, an irrational one is
correctly rounded to the requested significant digits, and repeated calls are identical:
import decimal
from httk.core import SurdVector
assert SurdVector.sqrt_of(2).to_decimal(digits=30) == decimal.Decimal("1.41421356237309504880168872421")
assert SurdVector.sqrt_of(8).to_decimal(digits=6) == decimal.Decimal("2.82843")
assert SurdVector.sqrt_of(9).to_decimal(digits=30) == decimal.Decimal("3") # rational: exact
Exact degree trigonometry (Niven’s theorem)¶
Crystallographic cell angles are given in whole degrees, and for the common ones the cosine is an
exact surd. SurdScalar.cos_degrees / sin_degrees return that exact value whenever the angle —
reduced modulo 360 — is a multiple of 15 or of 36 degrees, and None otherwise. 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\}\) — i.e. the multiples of 15° (like
\(\cos 15° = \tfrac{\sqrt6+\sqrt2}{4}\)) and of 36° (the golden-ratio family,
\(\cos 36° = \tfrac{1+\sqrt5}{4}\)). Niven’s theorem is the rational-value special case of this
classification, so a None result is a proof that the exact value is outside the field — the
caller then falls back to exactmath.cos(..., degrees=True) for a deterministic rational
approximation. SurdScalar.acos_degrees is the exact reverse lookup, returning the angle in
degrees over \([0, 180]\) when the value is one of the table cosines:
import fractions
from httk.core import SurdVector, SurdScalar
F = fractions.Fraction
# Exact for multiples of 15 / 36 degrees; angle reduced mod 360; int / Fraction / str accepted:
assert SurdScalar.cos_degrees(30) == SurdVector.sqrt_of(3) / 2 # (1/2)*sqrt(3)
assert SurdScalar.sin_degrees(60) == SurdVector.sqrt_of(3) / 2
assert SurdScalar.cos_degrees(15) == (SurdVector.sqrt_of(6) + SurdVector.sqrt_of(2)) / 4
assert SurdScalar.cos_degrees(36) == (SurdVector.one() + SurdVector.sqrt_of(5)) / 4
assert SurdScalar.cos_degrees(120) == SurdVector.create(F(-1, 2))
assert SurdScalar.cos_degrees("390") == SurdScalar.cos_degrees(30) # reduced mod 360
# sin^2 + cos^2 == 1 holds EXACTLY in the field:
c, s = SurdScalar.cos_degrees(30), SurdScalar.sin_degrees(30)
assert c * c + s * s == SurdVector.one()
# The 36-degree family is exact for cos only: sin(36) = cos(54) is NOT a surd (54 is a multiple
# of neither 15 nor 36), while the 15-degree family is exact for both:
assert SurdScalar.sin_degrees(36) is None
assert SurdScalar.sin_degrees(15) == SurdScalar.cos_degrees(75)
# A non-special angle has a cosine outside the field -> None (a proof of inexactness):
assert SurdScalar.cos_degrees(20) is None
# Exact reverse lookup over [0, 180]; a non-table value reverses to None:
assert (SurdVector.sqrt_of(3) / 2)._as_scalar().acos_degrees() == F(30)
assert ((SurdVector.one() + SurdVector.sqrt_of(5)) / 4)._as_scalar().acos_degrees() == F(36)
assert SurdVector.create(F(1, 3))._as_scalar().acos_degrees() is None
Representations and views¶
Vectors get the same backend/view treatment as the rest of httk₂. A backend wraps one
concrete representation; a view presents any backend through a chosen interface. Functions
accept the VectorLike union and normalize immediately to the view they want.
kind |
backend |
view |
the view is a … |
|---|---|---|---|
|
|
|
|
|
|
|
|
|
|
|
nested |
|
|
|
|
from httk.core import FracVector, VectorFracView, VectorNativeView
from httk.core.vectors import VectorBackend, VectorFrac, VectorNative
# Dispatch by input type:
assert isinstance(VectorBackend.create([[1, 2], [3, 4]]), VectorNative)
assert isinstance(VectorBackend.create(FracVector.create([[1, 2], [3, 4]])), VectorFrac)
# A frac view is a genuine FracVector, so the full exact algebra is available:
assert VectorFracView([[2, 3, 5], [3, 5, 4], [4, 6, 7]]).det() == -3
# A native view is a genuine nested tuple. By default it presents natively-held leaves verbatim
# (see "Element types" below); a leaf codec, e.g. leaf="fraction", converts them:
import fractions
assert VectorNativeView([["1/3", "2/3"]]) == (("1/3", "2/3"),)
assert VectorNativeView([["1/3", "2/3"]], leaf="fraction") == ((fractions.Fraction(1, 3), fractions.Fraction(2, 3)),)
The canonical interchange between representations is exactness-preserving: every backend
exposes .fractions (a nested tuple of fractions.Fraction, or a bare Fraction for a scalar)
and .dim. Because numpy float64 values are themselves binary rationals, even a numpy array
produces .fractions exactly.
On top of the hub, the VectorAPI contract guarantees the float renderings to_floats()
(nested plain-float lists, matching the numpy.ndarray.tolist() convention and directly
JSON-serializable; a bare float for a scalar) and to_float() (scalars only) on every backend —
and the exact value types FracVector and SurdVector honor the same contract with their own
implementations, with float(...) additionally working on every exact scalar (FracVector,
SurdScalar). So whatever object the family hands you, .to_floats() works — domain libraries
built on the family (e.g. httk₂’s atomistic structures) therefore return exact vector objects
from their accessors and add no ad-hoc float-conversion methods of their own:
import fractions
from httk.core import FracVector, SurdVector
from httk.core.vectors import VectorBackend
assert FracVector.create([["1/2", "1/4"]]).to_floats() == [[0.5, 0.25]]
assert VectorBackend.create([[1, 2], [3, 4]]).to_floats() == [[1.0, 2.0], [3.0, 4.0]]
assert float(SurdVector.sqrt_of(4)) == 2.0 # exact scalars support float()
assert SurdVector.sqrt_of(2).to_float() == float(SurdVector.sqrt_of(2))
The surd backend is the one representation whose values are not all rational, so it sits at the
edges of that exactness table asymmetrically. A rational → surd conversion is exact (every
rational is the radicand-1 term of a surd), and a VectorSurdView of a frac/native/numpy backend
therefore round-trips exactly:
import fractions
from httk.core import SurdVector, VectorSurdView
from httk.core.vectors import VectorBackend, VectorSurd
from httk.core.views import unwrap
# Dispatch by input type; VectorSurd wraps a SurdVector exactly:
assert isinstance(VectorBackend.create(SurdVector.sqrt_of(2)), VectorSurd)
# rational -> surd view is exact:
view = VectorSurdView([["1/3", "2/5"]])
assert view == SurdVector.create([["1/3", "2/5"]]) and view.is_rational
# unwrap returns the exact original SurdVector, radicals intact:
surd = SurdVector.sqrt_of(2)
assert unwrap(VectorSurd(surd)) is surd
Going the other way, a genuinely irrational surd → rational representation (its .fractions
hub, or any frac/native/numpy view) is lossy but deterministic: the value is reduced to a
reproducible rational approximation at the active decimal-context precision plus a small guard, and
the exact original is always recoverable from the backend via unwrap. Its lossiness row therefore
reads: rational → surd exact; surd → rational lossy-deterministic (exact original recoverable).
Element types (leaves)¶
The backends above choose the container (a FracVector, a nested tuple, a numpy.ndarray).
A leaf codec chooses the orthogonal element domain — what a single leaf value is presented as
(an int, a fractions.Fraction, a float, a decimal.Decimal, …). The two axes are
independent: any container can carry any leaf type. Leaf codecs live in
httk.core.vectors.leaf_codecs and are an extensible registry, exactly like the compression codecs.
The key policy is that a view never raises on the data. A backend always keeps the exact original, so any lossy view is fully recoverable simply by re-viewing; therefore a leaf codec that cannot represent a value exactly still produces a view, applying its documented default conversion. Only configuration errors — an unknown codec name or an invalid option — raise, and they raise eagerly at view construction.
The preserve-original default¶
The default native view of natively-held data now presents its leaves verbatim — the identical
objects, with only the list/tuple containers tuple-ized. Decimals in, the same Decimals out; no
silent Fraction-ization:
import decimal
from httk.core import VectorNativeView
d = decimal.Decimal("1.5")
nv = VectorNativeView([[d, 2], [3, 4]])
assert nv == ((decimal.Decimal("1.5"), 2), (3, 4))
assert nv[0][0] is d # the very same Decimal object
assert [type(x) for x in nv[0]] == [decimal.Decimal, int]
Because the leaves are untouched, string leaves stay strings under the default — ask for a codec
when you want them converted (see below). When the source instead crosses representations (a
frac or numpy backend, which holds no native leaves to preserve), the default is the "exact"
codec — int when integral, otherwise Fraction, never a float, exactly as before:
import fractions
from httk.core import FracVector, VectorNativeView
crossed = VectorNativeView(FracVector.create([["1/3", "2/3"], [1, 2]]))
assert crossed == ((fractions.Fraction(1, 3), fractions.Fraction(2, 3)), (1, 2))
Choosing a leaf codec¶
Pass leaf= (plus any codec options) to convert every element from the backend’s exact fractions
hub. The built-ins:
|
leaf type |
exactness contract / default conversion |
|---|---|---|
|
|
always exact — |
|
|
always exact |
|
|
exact when integral; else |
|
|
inherently lossy: nearest IEEE-754 binary double |
|
|
exact for |
The "int" codec’s default rounding is nearest with ties to even (matching Python’s round on a
Fraction); rounding= also accepts "floor", "ceil", and "trunc":
from httk.core import FracVector, VectorNativeView
halves = FracVector.create([["5/2", "7/2", "-5/2", "-7/2"]])
assert VectorNativeView(halves, leaf="int") == ((2, 4, -2, -4),) # half-even
assert VectorNativeView(halves, leaf="int", rounding="floor") == ((2, 3, -3, -4),)
assert VectorNativeView(halves, leaf="int", rounding="ceil") == ((3, 4, -2, -3),)
assert VectorNativeView(halves, leaf="int", rounding="trunc") == ((2, 3, -2, -3),)
The "decimal" codec is exact whenever the reduced denominator is of the form 2**a * 5**b (a
finite decimal expansion) — regardless of the active decimal context precision. Otherwise the
value has no finite decimal expansion, so it is quantized to digits= significant digits (default:
the active context precision) with round-half-even:
import decimal
from httk.core import FracVector, VectorNativeView
# 1/8 has a finite expansion: exact.
assert VectorNativeView(FracVector.create([["1/8"]]), leaf="decimal") == ((decimal.Decimal("0.125"),),)
# 1/3 does not: quantized to the requested number of significant digits.
assert VectorNativeView(FracVector.create([["1/3"]]), leaf="decimal", digits=6) == ((decimal.Decimal("0.333333"),),)
And leaf="float" is the lossy-by-design bridge to plain floats (leaf="fraction" converts the
preserved strings from the first example):
import fractions
from httk.core import FracVector, VectorNativeView
assert VectorNativeView(FracVector.create([["1/3"]]), leaf="float") == ((1.0 / 3.0,),)
assert VectorNativeView([["1/3", "2/3"]], leaf="fraction") == ((fractions.Fraction(1, 3), fractions.Fraction(2, 3)),)
Only configuration errors raise, and eagerly — an unknown codec name or an option a codec does not accept:
from httk.core import VectorNativeView
for bad in [dict(leaf="nope"), dict(leaf="int", rounding="sideways"), dict(leaf="float", digits=3)]:
try:
VectorNativeView([[1, 2]], **bad)
raise AssertionError("expected a ValueError")
except ValueError:
pass
The invariant¶
Viewing is non-destructive: after any lossy view, unwrap() still returns the identical original
object, and a fresh exact view reproduces the exact values.
import decimal
from httk.core import VectorNative, VectorNativeView
from httk.core.views import unwrap
raw = [["1/3", decimal.Decimal("2.5"), 4]]
backend = VectorNative(raw)
lossy = VectorNativeView(backend, leaf="int") # 1/3 -> 0, 5/2 -> 2 (half-even)
assert lossy == ((0, 2, 4),)
assert unwrap(backend) is raw # the original object, untouched
import fractions
assert VectorNativeView(backend, leaf="exact") == ((fractions.Fraction(1, 3), fractions.Fraction(5, 2), 4),)
Numpy dtypes¶
The numpy view carries the same idea through dtype= (default float64). For an integer dtype,
each element is rounded through the "int" codec’s default (nearest, half-even) via the exact
Fraction hub before the array is built — so numpy never silently truncates a fractional value:
import numpy
from httk.core import FracVector, VectorNumpyView
exact = VectorNumpyView([[1, 2], [3, 4]], dtype=numpy.int64)
assert exact.dtype == numpy.int64 and exact.tolist() == [[1, 2], [3, 4]]
rounded = VectorNumpyView(FracVector.create([["1/2", "3/2", "5/2", "7/2"]]), dtype=numpy.int64)
assert rounded.tolist() == [[0, 2, 2, 4]] # half-even, not truncation
Registering a custom leaf codec¶
A leaf codec is a LeafCodec — a name, a from_fraction(value, **options) conversion from the
canonical Fraction hub, and an option validator — registered with register_leaf_codec. Once
registered it is usable through the leaf= hint like any built-in:
import fractions
from httk.core import FracVector, LeafCodec, VectorNativeView, known_leaf_codecs, register_leaf_codec
def _to_percent(value: fractions.Fraction) -> str:
return f"{float(value * 100):g}%"
def _no_options(options: dict) -> None:
if options:
raise ValueError("percent codec takes no options")
register_leaf_codec(LeafCodec("percent", _to_percent, _no_options))
assert "percent" in known_leaf_codecs()
assert VectorNativeView(FracVector.create([["1/4", "1/2"]]), leaf="percent") == (("25%", "50%"),)
The numpy view, exact capture, and lossiness¶
numpy support is an optional dependency; install it with the extra:
pip install "httk-core[numpy]"
VectorNumpyView(fracvector) produces a plain float64 matrix — the fast path into numeric code.
It is the one lossy step (rationals become their nearest binary value), so a 3×3 exact cell
matrix converts to a plain float matrix identically to FracVector.to_floats():
import numpy
from httk.core import FracVector, VectorNumpyView
cell = FracVector.create([["8.04", "0.0", "0.0"],
["0.0", "3.72", "0.0"],
["0.0", "0.0", "7.38"]])
arr = VectorNumpyView(cell) # a genuine float64 ndarray
assert isinstance(arr, numpy.ndarray)
assert arr.dtype == numpy.float64 and arr.shape == (3, 3)
assert arr.tolist() == cell.to_floats() # same float matrix as to_floats()
Going the other way, VectorFracView(ndarray) captures a float64 array’s values as exact
binary rationals. A value that has genuinely passed through a raw float64 array therefore does not
return as its original decimal fraction — but limit_denominator recovers the intended small
rational:
import numpy
from httk.core import FracVector, VectorNumpyView, VectorFracView
one_third = FracVector.create([["1/3"]])
detached = numpy.asarray(VectorNumpyView(one_third)) # a detached plain float64 array
back = VectorFracView(detached) # captures the binary rational exactly
assert back.simplify() != one_third # it is the float64 value of 1/3
assert back.limit_denominator(100).simplify() == one_third
(This mirrors the orientation-lossiness note for cell parameters in httk-atomistic.)
When numpy is not installed, the numpy backend and view are simply not registered (the numpy view
module subclasses numpy.ndarray, so it cannot even be imported without numpy). Everything else —
the exact library and the frac/native family — works unchanged, and dispatch just never selects a
numpy backend.
The numeric presentation (NumericVector)¶
The backends above each commit to one container type. One level up sits a convenience presentation
for callers who do not need exact arithmetic and simply want plain numpy numbers to compute with.
to_numeric(obj) delivers exactly that, and NumericVector is the generic name for what comes out:
a base-class float64 numpy.ndarray for a tensor, a plain float for a scalar (shape ()) —
never a view subclass, never a 0-d array.
The numeric presentation is numpy-backed, so a caller always knows the concrete type it gets.
numpy is an optional dependency of httk-core (the httk-core[numpy] extra), so to_numeric
requires numpy and raises ImportError (naming the extra) when it is not installed — uniformly,
regardless of the input shape. The one exception is to_numeric_scalar, which converts a single
value to a float: that needs no numpy and so works unconditionally.
import fractions
import numpy
from httk.core import FracVector, SurdVector, to_numeric, to_numeric_scalar
F = fractions.Fraction
cell = FracVector.create([["8.04", "0.0", "0.0"],
["0.0", "3.72", "0.0"],
["0.0", "0.0", "7.38"]])
# A tensor becomes a plain float64 ndarray (the base class, not a view subclass):
arr = to_numeric(cell)
assert type(arr) is numpy.ndarray
assert arr.dtype == numpy.float64
assert arr.tolist() == cell.to_floats()
# A scalar — an exact rational or radical — becomes a plain float, never a 0-d array:
assert to_numeric(F(1, 4)) == 0.25
assert type(to_numeric(SurdVector.sqrt_of(2))) is float
# to_numeric_scalar is the numpy-free single-value helper:
assert to_numeric_scalar("1/3") == to_numeric(F(1, 3))
Use to_numeric when you just want numpy numbers; reach for a specific view (VectorNumpyView,
VectorNativeView) when you need control over the exact container type, the numpy dtype=, or the
leaf codec.
unwrap¶
unwrap(obj) returns the most raw representation available: the wrapped FracVector for a frac
backend, the original nested sequence for a native backend, and the underlying ndarray for a
numpy backend. For anything that is not a view/backend it returns the object unchanged.