std.math.trig_int
std.math.trig_int
Section titled “std.math.trig_int”Pure-integer trigonometry for environments where floating-point is unavailable, undesirable, or simply too slow. Written entirely in native Janus; no graft c, no libm, no FPU instructions generated.
This module was built to serve the BitNet × PolarQuant inference pipeline – where i8 weights flow from BitNet quantisation directly into polar conversion without ever touching a floating-point register. If you are on a microcontroller, an NPU without FPU, or on the hot path of a multiplication-free inference kernel, this is your module.
Import
Section titled “Import”use std.math.trig_intWhy Integer Trig?
Section titled “Why Integer Trig?”Standard trig (libm, std.math.trig) uses f64 arithmetic and an FPU. For BitNet i8 → polar conversion:
- Weights are i8 values in [-128, 127].
- You need angle and magnitude – not Cartesian coordinates.
- The conversion happens millions of times per inference pass.
- An FPU stall on a constrained NPU adds ~12 cycles per conversion.
Integer trig replaces that with a quarter-wave sine table and fixed-point arithmetic. The table is 65 bytes – it fits in a single cache line. Lookup + interpolation costs three integer operations.
Estimated throughput: ~3.3× faster than libm on ARM Cortex-A without NEON, based on cycle-count projections. Benchmarks against hardware will ship with SPEC-070 Phase 1.
The Quarter-Wave Table
Section titled “The Quarter-Wave Table”The entire sine approximation is driven by 65 pre-computed i8 values spanning [0, π/2]:
index 0 → sin(0) = 0index 32 → sin(π/4) = 90 (scaled to i8 range)index 64 → sin(π/2) = 127Symmetry rules handle the remaining three quadrants. Comptime-generated; no runtime allocation.
SinCosI8
Section titled “SinCosI8”Return type for sincos_u8. Carries both components in a single struct to avoid two separate table lookups.
struct SinCosI8 { sin: i8, // scaled sine in [-127, 127] cos: i8, // scaled cosine in [-127, 127]}PolarConvertError
Section titled “PolarConvertError”Error set for cartesian_to_polar_i8 when the input is degenerate.
error PolarConvertError { ZeroVector, // both x and y are zero — angle is undefined}Function Reference
Section titled “Function Reference”atan2_i8
Section titled “atan2_i8”Four-quadrant arctangent for i8 inputs. Returns an angle as a u8 angle unit where the full circle is [0, 255] – no floating-point involved.
func atan2_i8(y: i8, x: i8) -> u8Angle encoding: 0 = 0°, 64 = 90°, 128 = 180°, 192 = 270°. This encoding maps cleanly to polar representation where 256 steps cover a full circle.
Special cases:
atan2_i8(0, 0)returns0. Callers that need to detect the zero-vector case should usecartesian_to_polar_i8instead, which surfacesPolarConvertError.ZeroVector.
magnitude_i8
Section titled “magnitude_i8”L2 magnitude of an i8 vector, returned as u8. Uses a fast integer square root – no division, no FPU.
func magnitude_i8(x: i8, y: i8) -> u8The result is clamped to [0, 255]. For i8 inputs in [-128, 127], the true L2 magnitude fits in approximately 181 – well within u8 range.
sincos_u8
Section titled “sincos_u8”Sine and cosine for a u8 angle (full circle = 256 steps). Returns SinCosI8. Both components are computed from the same quarter-wave table lookup, so the cost is one lookup + two symmetry tests.
func sincos_u8(angle: u8) -> SinCosI8cartesian_to_polar_i8
Section titled “cartesian_to_polar_i8”Converts an (x, y) i8 pair to polar form: magnitude + angle. Returns PolarConvertError.ZeroVector when x == 0 and y == 0.
func cartesian_to_polar_i8(x: i8, y: i8) -> PolarConvertError!{ mag: u8, angle: u8 }Code Examples
Section titled “Code Examples”atan2_i8
Section titled “atan2_i8”use std.math.trig_int
func main() do // Weight pair from a BitNet layer let wx: i8 = 64 let wy: i8 = 64
let angle = trig_int.atan2_i8(wy, wx) // angle ≈ 32 (roughly 45° in 256-step encoding) println("angle unit: ", angle)endmagnitude_i8
Section titled “magnitude_i8”use std.math.trig_int
func signal_strength(x: i8, y: i8) -> u8 do return trig_int.magnitude_i8(x, y)endsincos_u8
Section titled “sincos_u8”use std.math.trig_int
func reconstruct(angle: u8, radius: u8) -> { x: i8, y: i8 } do let sc = trig_int.sincos_u8(angle) let r = as[i32](radius) return { x: as[i8]((r * as[i32](sc.cos)) >> 7), y: as[i8]((r * as[i32](sc.sin)) >> 7), }endcartesian_to_polar_i8
Section titled “cartesian_to_polar_i8”use std.math.trig_int
func encode_weight(x: i8, y: i8) -> trig_int.PolarConvertError!{ mag: u8, angle: u8 } do return try trig_int.cartesian_to_polar_i8(x, y)end
func main() do let polar = encode_weight(100, 60) or |err| do println("zero vector – skipping") return end println("mag=", polar.mag, " angle=", polar.angle)endConnection to PolarQuant and SPEC-070
Section titled “Connection to PolarQuant and SPEC-070”trig_int is the low-level engine underneath two higher-level systems:
- PolarQuant (
std.compute.polar) – SPEC-070 Phase 0. Converts full embedding vectors from Cartesian to polar form. Callscartesian_to_polar_i8across each adjacent dimension pair. - SASA Kenya inference – The 6× throughput target in SASA v0.2.0 is predicated on running the BitNet weight stream through
cartesian_to_polar_i8at i8 precision before any matrix multiply. The polar representation enables multiplication-free dot products via angle-domain arithmetic (Phase 1 of SPEC-070).
If you are building a custom inference kernel, import trig_int directly and keep the FPU cold. The std.compute.polar layer handles the higher-level semantics; trig_int handles the raw cycles.
Next: std.compute.polar — SPEC-070 Phase 0 polar embedding primitives built on top of this module.