Skip to content

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.


use std.math.trig_int

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 entire sine approximation is driven by 65 pre-computed i8 values spanning [0, π/2]:

index 0 → sin(0) = 0
index 32 → sin(π/4) = 90 (scaled to i8 range)
index 64 → sin(π/2) = 127

Symmetry rules handle the remaining three quadrants. Comptime-generated; no runtime allocation.


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]
}

Error set for cartesian_to_polar_i8 when the input is degenerate.

error PolarConvertError {
ZeroVector, // both x and y are zero — angle is undefined
}

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) -> u8

Angle 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) returns 0. Callers that need to detect the zero-vector case should use cartesian_to_polar_i8 instead, which surfaces PolarConvertError.ZeroVector.

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) -> u8

The result is clamped to [0, 255]. For i8 inputs in [-128, 127], the true L2 magnitude fits in approximately 181 – well within u8 range.

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) -> SinCosI8

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 }

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)
end
use std.math.trig_int
func signal_strength(x: i8, y: i8) -> u8 do
return trig_int.magnitude_i8(x, y)
end
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),
}
end
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)
end

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. Calls cartesian_to_polar_i8 across 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_i8 at 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.